From a4c6add6797cda5c62ae187269bb37e09420f7f5 Mon Sep 17 00:00:00 2001 From: Renan Butkeraites Date: Wed, 29 Jan 2025 10:19:40 -0300 Subject: [PATCH 1/3] Enhance EveryAction activist code handling with caching and improved lookup --- target_everyaction/client.py | 3 +- target_everyaction/sinks.py | 73 +++++++++++++++++++++++++++++++----- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/target_everyaction/client.py b/target_everyaction/client.py index 073fca7..9b2024e 100644 --- a/target_everyaction/client.py +++ b/target_everyaction/client.py @@ -39,13 +39,14 @@ def validate_response(self, response: requests.Response) -> None: msg = self.response_error_message(response) raise FatalAPIError(msg) - def request_api(self, method, request_data, endpoint): + def request_api(self, method, endpoint, request_data=None, params=None): url = f"{self.base_url}{endpoint}" LOGGER.info(self.__auth) response = requests.request( method, url, json=request_data, + params=params, auth=self.__auth, headers={"Content-Type": "application/json", "Accept": "application/json"}, ) diff --git a/target_everyaction/sinks.py b/target_everyaction/sinks.py index 0e42da1..7362ba0 100644 --- a/target_everyaction/sinks.py +++ b/target_everyaction/sinks.py @@ -101,6 +101,61 @@ def _get_or_create_code(self, code_payload: dict) -> Optional[str]: request_data=code_payload) return response.json() if response.ok else None + def _get_all_activist_codes(self) -> dict: + """Get all activist codes and cache them in memory.""" + all_codes = {} + params = { + "statuses": "Active,Archived", + "$top": 200 + } + + response = self.request_api("GET", endpoint="activistCodes", params=params) + while True: + if response.ok: + data = response.json() + # Store codes with lowercase names for case-insensitive lookup + for item in data["items"]: + all_codes[item["name"].lower()] = item["activistCodeId"] + + if "nextPageLink" not in data: + break + + next_page = data["nextPageLink"].split("?")[1] + response = self.request_api("GET", endpoint=f"activistCodes?{next_page}") + else: + break + + return all_codes + + def _process_activist_codes(self, person_id: int, code_names: list, cached_codes: dict): + """Process activist codes for a person.""" + missing_codes = [] + + # Build payload for existing codes + responses = [] + for code_name in code_names: + code_id = cached_codes.get(code_name.lower()) + if code_id: + responses.append({ + "activistCodeId": code_id, + "action": "Apply", + "type": "ActivistCode" + }) + else: + missing_codes.append(code_name) + LOGGER.warning(f"Activist code '{code_name}' not found") + + # Apply existing codes in a single request if any exist + if responses: + payload = {"responses": responses} + self.request_api( + "POST", + endpoint=f"people/{person_id}/canvassResponses", + request_data=payload + ) + + return missing_codes + def upsert_record(self, record: dict, context: dict): method = "POST" state_dict = dict() @@ -113,16 +168,14 @@ def upsert_record(self, record: dict, context: dict): if hasattr(self, "pending_codes"): # Activist codes if self.pending_codes.get("activist"): - for code in self.pending_codes["activist"]: - payload = { - "responses": [{ - "activistCodeId": code, - "action": "Apply", - "type": "ActivistCode" - }] - } - self.request_api("POST", endpoint=f"people/{id}/canvassResponses", - request_data=payload) + # Get all activist codes once + cached_codes = self._get_all_activist_codes() + # Process all codes for this person + missing_codes = self._process_activist_codes( + id, + self.pending_codes["activist"], + cached_codes + ) # Handle both Source Codes and Tags for code_type, codes in self.pending_codes.items(): From 339cf451c4235086efa7651b6b3f9cae909c1dd1 Mon Sep 17 00:00:00 2001 From: Renan Butkeraites Date: Wed, 29 Jan 2025 10:56:36 -0300 Subject: [PATCH 2/3] Improve activist code pagination handling in ContactsSink --- target_everyaction/sinks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/target_everyaction/sinks.py b/target_everyaction/sinks.py index 7362ba0..51231bf 100644 --- a/target_everyaction/sinks.py +++ b/target_everyaction/sinks.py @@ -117,9 +117,11 @@ def _get_all_activist_codes(self) -> dict: for item in data["items"]: all_codes[item["name"].lower()] = item["activistCodeId"] - if "nextPageLink" not in data: + # Check if there are more pages + if not data.get("nextPageLink"): break + # Extract next page URL and make request next_page = data["nextPageLink"].split("?")[1] response = self.request_api("GET", endpoint=f"activistCodes?{next_page}") else: From bd01e4cbbfc31da9cff191de56118b95030a6922 Mon Sep 17 00:00:00 2001 From: Renan Butkeraites Date: Wed, 29 Jan 2025 11:06:46 -0300 Subject: [PATCH 3/3] Fix ContactsSink pagination logic for activist codes --- target_everyaction/sinks.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/target_everyaction/sinks.py b/target_everyaction/sinks.py index 51231bf..57075fe 100644 --- a/target_everyaction/sinks.py +++ b/target_everyaction/sinks.py @@ -83,10 +83,11 @@ def _get_or_create_code(self, code_payload: dict) -> Optional[str]: item["name"].lower(): item["codeId"] for item in data["items"] }) - if "nextPageLink" not in data: + if "nextPageLink" in data and data["nextPageLink"]: + response = self.request_api("GET", + endpoint=f"codes?{data['nextPageLink'].split('?')[1]}") + else: break - response = self.request_api("GET", - endpoint=f"codes?{data['nextPageLink'].split('?')[1]}") else: break @@ -117,13 +118,12 @@ def _get_all_activist_codes(self) -> dict: for item in data["items"]: all_codes[item["name"].lower()] = item["activistCodeId"] - # Check if there are more pages - if not data.get("nextPageLink"): - break - # Extract next page URL and make request - next_page = data["nextPageLink"].split("?")[1] - response = self.request_api("GET", endpoint=f"activistCodes?{next_page}") + if "nextPageLink" in data and data["nextPageLink"]: + next_page = data["nextPageLink"].split("?")[1] + response = self.request_api("GET", endpoint=f"activistCodes?{next_page}") + else: + break else: break