From 9d2d32aa80d49f8a6e49c1ab25b1386c07d92ad9 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 5 Jul 2026 18:31:04 -0400 Subject: [PATCH 01/94] Sort extras in pyproj --- pyproject.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d637fe65cb..f5257f8e1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,17 +121,17 @@ scytl = ["defusedxml >= 0.7"] sftp = ["paramiko >= 3.0"] slack = ["slack-sdk >= 3.26"] smtp = ["email-validator >= 2.2"] +ssh = [ + "sshtunnel >= 0.4", + "psycopg2-binary >= 2.9.11", + "sqlalchemy >= 1.4", +] targetsmart = [ "defusedxml >= 0.7", "paramiko >= 3.0", "xmltodict >= 1.0" ] twilio = ["twilio >= 9.0"] -ssh = [ - "sshtunnel >= 0.4", - "psycopg2-binary >= 2.9.11", - "sqlalchemy >= 1.4", -] all = [ "parsons[airtable]", "parsons[alchemer]", @@ -163,9 +163,9 @@ all = [ "parsons[sftp]", "parsons[slack]", "parsons[smtp]", + "parsons[ssh]", "parsons[targetsmart]", "parsons[twilio]", - "parsons[ssh]", ] [project.urls] From 49ea730bdd9d21aaabf5ccddb40f62cdb184be18 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 5 Jul 2026 18:32:20 -0400 Subject: [PATCH 02/94] add solidarity-tech extra --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index f5257f8e1a..0473ef6d3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,6 +121,7 @@ scytl = ["defusedxml >= 0.7"] sftp = ["paramiko >= 3.0"] slack = ["slack-sdk >= 3.26"] smtp = ["email-validator >= 2.2"] +solidarity-tech = ["pyrate-limiter >= 4.4"] ssh = [ "sshtunnel >= 0.4", "psycopg2-binary >= 2.9.11", @@ -163,6 +164,7 @@ all = [ "parsons[sftp]", "parsons[slack]", "parsons[smtp]", + "parsons[solidarity-tech]", "parsons[ssh]", "parsons[targetsmart]", "parsons[twilio]", From 9be850f8c35c01e1a3a227fa08935cee18aa887e Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 26 Jul 2026 22:05:28 -0400 Subject: [PATCH 03/94] Allow passing additional headers to APIConnector --- parsons/utilities/api_connector.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/parsons/utilities/api_connector.py b/parsons/utilities/api_connector.py index e5a699768f..5179db9994 100644 --- a/parsons/utilities/api_connector.py +++ b/parsons/utilities/api_connector.py @@ -92,6 +92,7 @@ def request( data: _Data | None = None, params: _Params | None = None, raise_on_error: bool = True, + additional_headers: _Headers | None = None, **kwargs, ) -> requests.Response: """ @@ -124,11 +125,16 @@ def request( """ full_url = urllib.parse.urljoin(self.uri, url) + complete_headers = ( + {**self.headers, **additional_headers} + if (self.headers and additional_headers) + else (self.headers or additional_headers) + ) resp = requests.request( req_type, full_url, - headers=self.headers, + headers=complete_headers, auth=self.auth, json=json, data=data, From 38201c27713a9258f9fa8f76a291902865c0b8ac Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 26 Jul 2026 22:05:57 -0400 Subject: [PATCH 04/94] Initial work on SolidarityTech connector --- parsons/solidarity_tech/__init__.py | 3 + .../ratelimited_api_connector.py | 20 ++ parsons/solidarity_tech/solidarity_tech.py | 37 +++ .../solidarity_tech_activities.py | 63 ++++++ .../solidarity_tech_agent_assignments.py | 193 ++++++++++++++++ .../solidarity_tech_automation_enrollments.py | 51 +++++ .../solidarity_tech/solidarity_tech_base.py | 78 +++++++ .../solidarity_tech/solidarity_tech_calls.py | 54 +++++ .../solidarity_tech_chapter_phone_numbers.py | 53 +++++ .../solidarity_tech_chapters.py | 50 ++++ .../solidarity_tech_custom_user_properties.py | 214 ++++++++++++++++++ .../solidarity_tech_donation_charges.py | 80 +++++++ .../solidarity_tech_email_blasts.py | 79 +++++++ .../solidarity_tech_email_senders.py | 43 ++++ .../solidarity_tech/solidarity_tech_emails.py | 83 +++++++ .../solidarity_tech_exceptions.py | 13 ++ 16 files changed, 1114 insertions(+) create mode 100644 parsons/solidarity_tech/__init__.py create mode 100644 parsons/solidarity_tech/ratelimited_api_connector.py create mode 100644 parsons/solidarity_tech/solidarity_tech.py create mode 100644 parsons/solidarity_tech/solidarity_tech_activities.py create mode 100644 parsons/solidarity_tech/solidarity_tech_agent_assignments.py create mode 100644 parsons/solidarity_tech/solidarity_tech_automation_enrollments.py create mode 100644 parsons/solidarity_tech/solidarity_tech_base.py create mode 100644 parsons/solidarity_tech/solidarity_tech_calls.py create mode 100644 parsons/solidarity_tech/solidarity_tech_chapter_phone_numbers.py create mode 100644 parsons/solidarity_tech/solidarity_tech_chapters.py create mode 100644 parsons/solidarity_tech/solidarity_tech_custom_user_properties.py create mode 100644 parsons/solidarity_tech/solidarity_tech_donation_charges.py create mode 100644 parsons/solidarity_tech/solidarity_tech_email_blasts.py create mode 100644 parsons/solidarity_tech/solidarity_tech_email_senders.py create mode 100644 parsons/solidarity_tech/solidarity_tech_emails.py create mode 100644 parsons/solidarity_tech/solidarity_tech_exceptions.py diff --git a/parsons/solidarity_tech/__init__.py b/parsons/solidarity_tech/__init__.py new file mode 100644 index 0000000000..6db955fb2b --- /dev/null +++ b/parsons/solidarity_tech/__init__.py @@ -0,0 +1,3 @@ +from parsons.solidarity_tech.solidarity_tech import SolidarityTech + +__all__ = ["SolidarityTech"] diff --git a/parsons/solidarity_tech/ratelimited_api_connector.py b/parsons/solidarity_tech/ratelimited_api_connector.py new file mode 100644 index 0000000000..778c72029d --- /dev/null +++ b/parsons/solidarity_tech/ratelimited_api_connector.py @@ -0,0 +1,20 @@ +import requests +from pyrate_limiter import Duration, Limiter, Rate + +from parsons.utilities.api_connector import APIConnector + +rates = [ + Rate(60, Duration.SECOND * 30), # 60 requests per 30 seconds +] +limiter = Limiter(rates) + + +class RateLimitedAPIConnector(APIConnector): + @limiter.as_decorator(name="api_call", weight=1) + def request( + self, + *args, + **kwargs, + ) -> requests.Response: + """Make a request with pyrate-limiter.""" + return super().request(*args, **kwargs) diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py new file mode 100644 index 0000000000..154f6b3546 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -0,0 +1,37 @@ +import logging + +from parsons.solidarity_tech.solidarity_tech_activities import SolidarityTechActivities +from parsons.solidarity_tech.solidarity_tech_agent_assignments import SolidarityTechAgentAssignments +from parsons.solidarity_tech.solidarity_tech_automation_enrollments import ( + SolidarityTechAutomationEnrollments, +) +from parsons.solidarity_tech.solidarity_tech_calls import SolidarityTechCalls +from parsons.solidarity_tech.solidarity_tech_chapter_phone_numbers import ( + SolidarityTechChapterPhoneNumbers, +) +from parsons.solidarity_tech.solidarity_tech_chapters import SolidarityTechChapters +from parsons.solidarity_tech.solidarity_tech_custom_user_properties import ( + SolidarityTechCustomUserProperties, +) +from parsons.solidarity_tech.solidarity_tech_donation_charges import SolidarityTechDonationCharges +from parsons.solidarity_tech.solidarity_tech_email_blasts import SolidarityTechEmailBlasts +from parsons.solidarity_tech.solidarity_tech_email_senders import SolidarityTechEmailSenders +from parsons.solidarity_tech.solidarity_tech_emails import SolidarityTechEmails + +logger = logging.getLogger(__name__) + + +class SolidarityTech( + SolidarityTechActivities, + SolidarityTechAgentAssignments, + SolidarityTechAutomationEnrollments, + SolidarityTechCalls, + SolidarityTechChapterPhoneNumbers, + SolidarityTechChapters, + SolidarityTechCustomUserProperties, + SolidarityTechDonationCharges, + SolidarityTechEmailBlasts, + SolidarityTechEmailSenders, + SolidarityTechEmails, +): + pass diff --git a/parsons/solidarity_tech/solidarity_tech_activities.py b/parsons/solidarity_tech/solidarity_tech_activities.py new file mode 100644 index 0000000000..d7aa35e39d --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_activities.py @@ -0,0 +1,63 @@ +import logging +from datetime import datetime + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechActivities(SolidarityTechBase): + def get_activities( + self, + limit: int = 20, + cursor: int | None = None, + since: int | datetime | None = 0, + include_count: bool = False, + user_id: int | None = None, + ) -> str: + """ + Retrieve a list of activities. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + cursor: + Keyset pagination cursor. + Pass the meta.next_cursor value from the previous response to fetch the next (older) page. + This is the recommended way to paginate; it stays fast at any depth. + Records are returned newest first (descending id). + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + include_count: + When true, meta.total_count is populated with the full result count. + Off by default because counting an entire history is expensive. + Omit it for normal paging. + user_id: + User ID to filter activities for a specific user. + + Returns: + All the activities entries. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "activities", + limit=limit, + cursor=cursor, + since=since, + include_count=include_count or None, + user_id=user_id, + additional_headers={"accept": "application/json"}, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text diff --git a/parsons/solidarity_tech/solidarity_tech_agent_assignments.py b/parsons/solidarity_tech/solidarity_tech_agent_assignments.py new file mode 100644 index 0000000000..29e58a29a7 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_agent_assignments.py @@ -0,0 +1,193 @@ +import logging +from datetime import datetime + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechAgentAssignments(SolidarityTechBase): + def get_agent_assignments( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + user_id: int | None = None, + agent_user_id: int | None = None, + ) -> str: + """ + Retrieve a list of agent assignments. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + user_id: + User ID to filter agent assignments related to a specific user. + agent_user_id: + Agent User ID to filter agent user assignments related to a specific agent user. + + Returns: + All the agent assignment entries. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "agent_assignments", + limit=limit, + offset=offset, + since=since, + user_id=user_id, + agent_user_id=agent_user_id, + ) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_agent_assignment( + self, + id: int, + ) -> str: + """ + Retrieve a single agent assignment. + + Args: + id: + ID of the agent assignment to retrieve. + + Returns: + A single agent assignment entry. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("agent_assignments", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_agent_assignment( + self, + user_id: int, + agent_user_id: int, + is_active: bool | None = None, + ) -> bool: + """ + Create an agent assignment with specified details. + + Args: + user_id: + Identifier for the user. + agent_user_id: + Identifier for the agent user. + is_active: + Whether the assignment is currently active. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = {"user_id": user_id, "agent_user_id": agent_user_id, "is_active": is_active} + res = self._post_request( + "agent_assignments", payload, additional_headers={"content-type": "application/json"} + ) + + if res.status_code not in (201, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 201 + + def update_agent_assignment( + self, + id: int, + user_id: int, + agent_user_id: int, + is_active: bool | None = None, + ) -> bool: + """ + Update an agent assignment with specified details. + + Args: + id: + Identifier for the agent assignment to update. + user_id: + Identifier for the user. + agent_user_id: + Identifier for the agent user. + is_active: + Whether the assignment is currently active. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = {"user_id": user_id, "agent_user_id": agent_user_id, "is_active": is_active} + res = self._put_request( + "agent_assignments", + id, + payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code not in (200, 404, 422): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 + + def delete_agent_assignment( + self, + id: int, + ) -> bool: + """ + Update an agent assignment with specified details. + + Args: + id: + Identifier for the agent assignment to update. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._del_request("agent_assignments", id) + + if res and res.status_code != 404: + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 404 diff --git a/parsons/solidarity_tech/solidarity_tech_automation_enrollments.py b/parsons/solidarity_tech/solidarity_tech_automation_enrollments.py new file mode 100644 index 0000000000..8c283d5302 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_automation_enrollments.py @@ -0,0 +1,51 @@ +import logging + +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechAutomationEnrollments(SolidarityTechBase): + def enroll_user_in_automation( + self, + automation_id: int, + user_id: int, + ) -> bool: + """ + Retrieve a list of agent assignments. + + Args: + automation_id: + The ID of the automation to enroll the user in. + user_id: + The ID of the user to enroll. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: Operation failed because automation is inactive. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = {"automation_id": automation_id, "user_id": user_id} + res = self._post_request( + "automation_enrollments", + payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code not in (201, 403, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 422: + raise HTTPError("Automation is inactive") + + return res.status_code == 201 diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py new file mode 100644 index 0000000000..1b66200301 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -0,0 +1,78 @@ +import logging +from collections.abc import Mapping +from datetime import datetime + +import requests + +from parsons.solidarity_tech.ratelimited_api_connector import RateLimitedAPIConnector +from parsons.utilities import check_env + +logger = logging.getLogger(__name__) + + +class SolidarityTechBase: + def __init__(self, api_token: str | None = None) -> None: + """ + Instantiate the SolidarityTech class. + + Args: + api_token: + A valid Bearer token for authorization. + Not required if the `SOLIDARITY_TECH_TOKEN` env variable is set. + + """ + self.api_token: str = check_env.check("SOLIDARITY_TECH_TOKEN", api_token) + self.headers = {"authorization": f"Bearer {self.api_token}"} + self.api_url = "https://api.solidarity.tech/v1/" + self.api = RateLimitedAPIConnector(self.api_url, headers=self.headers) + + def _get_resources(self, endpoint: str, limit: int, **kwargs) -> requests.Response: + """Process parameters and handle GET requests for lists of resources.""" + since = kwargs.get("since") + if isinstance(since, datetime): + kwargs["since"] = int(since.timestamp()) + + param_mapping = { + "limit": "_limit", + "cursor": "_cursor", + "offset": "_offset", + "since": "_since", + "include_count": "_include_count", + } + params = {"_limit": limit} + for key, value in kwargs.items(): + if value is None: + continue + query_key = param_mapping.get(key, key) + params[query_key] = value + del kwargs[key] + + logger.debug("Processing GET request at endpoint: %s", endpoint, extra=params) + return self.api.request(url=endpoint, req_type="GET", params=params, **kwargs) + + def _get_single_resource(self, endpoint: str, id: int, **kwargs) -> requests.Response: + """Handle GET requests for single resources.""" + complete_endpoint = f"{endpoint}/{id}" + logger.debug("Processing GET request at endpoint: %s", complete_endpoint) + return self.api.request(url=complete_endpoint, req_type="GET", **kwargs) + + def _post_request( + self, endpoint: str, payload: Mapping[str, str | int] | None = None, **kwargs + ) -> requests.Response: + """Handle POST requests.""" + logger.debug("Processing POST request at endpoint: %s", endpoint, extra=payload) + return self.api.request(url=endpoint, req_type="POST", json=payload, **kwargs) + + def _put_request( + self, endpoint: str, id: int, payload: Mapping[str, str | int] | None = None, **kwargs + ) -> requests.Response: + """Handle PUT requests.""" + complete_endpoint = f"{endpoint}/{id}" + logger.debug("Processing PUT request at endpoint: %s", complete_endpoint, extra=payload) + return self.api.request(url=complete_endpoint, req_type="PUT", json=payload, **kwargs) + + def _del_request(self, endpoint: str, id: int | str, **kwargs) -> requests.Response: + """Handle DEL requests.""" + complete_endpoint = f"{endpoint}/{id}" + logger.debug("Processing DEL request at endpoint: %s", complete_endpoint) + return self.api.request(url=complete_endpoint, req_type="DEL", **kwargs) diff --git a/parsons/solidarity_tech/solidarity_tech_calls.py b/parsons/solidarity_tech/solidarity_tech_calls.py new file mode 100644 index 0000000000..4da98b9acd --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_calls.py @@ -0,0 +1,54 @@ +import logging +from datetime import datetime + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechCalls(SolidarityTechBase): + def get_calls( + self, + user_id: int | None = None, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + ) -> str: + """ + Retrieve a list of calls. + + Args: + user_id: + User ID to filter calls related to a specific user. + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + + Returns: + All the calls entries. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "calls", + user_id=user_id, + limit=limit, + offset=offset, + since=since, + additional_headers={"accept": "application/json"}, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text diff --git a/parsons/solidarity_tech/solidarity_tech_chapter_phone_numbers.py b/parsons/solidarity_tech/solidarity_tech_chapter_phone_numbers.py new file mode 100644 index 0000000000..dcdfccfa0f --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_chapter_phone_numbers.py @@ -0,0 +1,53 @@ +import logging +from datetime import datetime + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechChapterPhoneNumbers(SolidarityTechBase): + def get_chapter_phone_numbers( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + chapter_id: int = 0, + ) -> str: + """ + Retrieve a list of chapter phone numbers. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + chapter_id: + Filters chapter phone numbers by chapter_id within the accessible scope. + + Returns: + All the chapter phone numbers entries. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "chapter_phone_numbers", + limit=limit, + offset=offset, + since=since, + chapter_id=chapter_id, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text diff --git a/parsons/solidarity_tech/solidarity_tech_chapters.py b/parsons/solidarity_tech/solidarity_tech_chapters.py new file mode 100644 index 0000000000..e71f8310e0 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_chapters.py @@ -0,0 +1,50 @@ +import logging +from datetime import datetime + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechChapters(SolidarityTechBase): + def get_chapters( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + ) -> str: + """ + Retrieve a list of chapters. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter chapters created after this time. + + Returns: + All the chapters entries. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "chapters", + limit=limit, + offset=offset, + since=since, + additional_headers={"accept": "application/json"}, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text diff --git a/parsons/solidarity_tech/solidarity_tech_custom_user_properties.py b/parsons/solidarity_tech/solidarity_tech_custom_user_properties.py new file mode 100644 index 0000000000..67cdab34a0 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_custom_user_properties.py @@ -0,0 +1,214 @@ +import logging +from datetime import datetime +from typing import Literal + +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechCustomUserProperties(SolidarityTechBase): + def get_custom_user_properties( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + scope_id: int | None = None, + scope_type: Literal["Organization", "Chapter"] | None = None, + ) -> str: + """ + Retrieve a list of custom user properties. + + Args: + limit: + Limits the number of items returned. Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + scope_id: + ID of the scope to filter custom user properties by. + scope_type: + Type of the scope to filter custom user properties by. + + Returns: + All the custom user properties entries + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "custom_user_properties", + limit=limit, + offset=offset, + since=since, + scope_id=scope_id, + scope_type=scope_type, + additional_headers={"accept": "application/json"}, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_custom_user_property( + self, + label: str, + field_type: Literal[ + "input", "textarea", "number", "date", "checkbox", "select", "radios", "checkboxes" + ], + description: str | None = None, + options: list[dict[str, str | dict[str, str]]] | None = None, + scope_type: Literal["Organization", "Chapter"] | None = None, + scope_id: int | None = None, + ) -> bool: + """ + Create a custom user property. + + Args: + label: + Display label for the property. + description: + Optional description of the property. + field_type: + Type of field for data entry. + options: + Options for select, radios, checkbox, or checkboxes field types. + See documentation. + scope_type: + Type of scope for the property. + scope_id: + ID of the scope for the property. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: Validation failed. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "label": label, + "description": description, + "field_type": field_type, + "options": options, + "scope_type": scope_type, + "scope_id": scope_id, + } + res = self._post_request( + "custom_user_properties", + payload=payload, + additional_headers={"accept": "application/json", "content-type": "application/json"}, + ) + + if res.status_code not in (201, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 422: + raise HTTPError("Validation failed") + + return res.status_code == 201 + + def delete_custom_user_property_option( + self, + custom_user_property_id: int, + id: str, + ) -> bool: + """ + Remove an option from a custom user property. + + Args: + custom_user_property_id: + Custom user property ID + id: + Value of the option to remove + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: Validation failed. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._del_request( + "custom_user_properties", + f"{custom_user_property_id}/options/{id}", + additional_headers={"accept": "application/json"}, + ) + + if res.status_code not in (200, 404, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 404: + raise HTTPError("Option or custom user property not found") + + if res.status_code == 422: + raise HTTPError("Validation failed") + + return res.status_code == 200 + + def create_custom_user_property_option( + self, + id: int, + label: list[dict[str, str | dict[str, str]]], + value: str | None = None, + ) -> bool: + """ + Create an option for a custom user property. + + Args: + id: + Custom user property ID + label: + Multi-language labels for the option + See documentation + value: + Internal value for the option (will be auto-generated if not provided) + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: Validation failed. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "label": label, + "value": value, + } + res = self._post_request( + f"custom_user_properties/{id}/options", + payload=payload, + additional_headers={"accept": "application/json", "content-type": "application/json"}, + ) + + if res.status_code not in (201, 404, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 404: + raise HTTPError("Custom user property not found") + + if res.status_code == 422: + raise HTTPError("Validation failed") + + return res.status_code == 201 diff --git a/parsons/solidarity_tech/solidarity_tech_donation_charges.py b/parsons/solidarity_tech/solidarity_tech_donation_charges.py new file mode 100644 index 0000000000..90a3af59db --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_donation_charges.py @@ -0,0 +1,80 @@ +import logging +from datetime import datetime + +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechDonationCharges(SolidarityTechBase): + def get_donation_charges( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + ) -> str: + """ + Retrieve a list of donation charges. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + + Returns: + All the donation charges. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "donation_charges", + limit=limit, + offset=offset, + since=since, + additional_headers={"accept": "application/json"}, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_donation_charge( + self, + id: int, + ) -> str: + """ + Retrieve a single donation charge. + + Args: + id: + ID of the donation charge to retrieve. + + Returns: + A single agent assignment entry. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("donation_charges", id) + + if res.status_code == 404: + raise HTTPError("Donation charge not found") + + if res.status_code: + raise STUnexpectedResponseCodeError(res) + + return res.text diff --git a/parsons/solidarity_tech/solidarity_tech_email_blasts.py b/parsons/solidarity_tech/solidarity_tech_email_blasts.py new file mode 100644 index 0000000000..e72989cc0a --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_email_blasts.py @@ -0,0 +1,79 @@ +import logging +from datetime import datetime + +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechEmailBlasts(SolidarityTechBase): + def get_email_blasts( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + ) -> str: + """ + Retrieve a list of email blasts. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + + Returns: + All the email blasts. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "email_blasts", + limit=limit, + offset=offset, + since=since, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_email_blast( + self, + id: int, + ) -> str: + """ + Retrieve a single email blast. + + Args: + id: + ID of the email blast to retrieve. + + Returns: + A single email blast entry. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("email_blasts", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 404: + raise HTTPError("Email blast not found") + + return res.text diff --git a/parsons/solidarity_tech/solidarity_tech_email_senders.py b/parsons/solidarity_tech/solidarity_tech_email_senders.py new file mode 100644 index 0000000000..fefcae4f81 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_email_senders.py @@ -0,0 +1,43 @@ +import logging + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechEmailSenders(SolidarityTechBase): + def get_email_senders( + self, + limit: int = 20, + offset: int = 0, + ) -> str: + """ + Returns a list of email senders available for the API key's scope. + Use these sender IDs when sending emails via the POST /emails endpoint. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + + Returns: + All the email senders. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "email_senders", + limit=limit, + offset=offset, + additional_headers={"accept": "application/json"}, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text diff --git a/parsons/solidarity_tech/solidarity_tech_emails.py b/parsons/solidarity_tech/solidarity_tech_emails.py new file mode 100644 index 0000000000..243ffd01b6 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_emails.py @@ -0,0 +1,83 @@ +import logging + +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechEmails(SolidarityTechBase): + def send_one_off_email( + self, + user_id: int, + subject: str, + body_html: str, + body_plain: str | None = None, + email_sender_id: int | None = None, + reply_to: str | None = None, + attachment_urls: list[str] | None = None, + track_opens: bool = True, + track_clicks: bool = True, + ) -> str: + """ + Returns a list of email senders available for the API key's scope. + Use these sender IDs when sending emails via the POST /emails endpoint. + + Args: + user_id: + ID of the user to send email to. + subject: + Email subject line (supports Liquid templating). + body_html: + HTML content of the email (supports Liquid templating). + body_plain: + Plain text fallback content. + email_sender_id: + ID of configured email sender (uses org default if omitted). + reply_to: + Reply-to email address. + attachment_urls: + Array of URLs to files to attach (max 5). + track_opens: + Enable open tracking. Default is True. + track_clicks: + Enable click tracking. Default is True. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: Missing required parameters. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + email_params = { + "user_id": user_id, + "subject": subject, + "body_html": body_html, + "body_plain": body_plain, + "email_sender_id": email_sender_id, + "reply_to": reply_to, + "attachment_urls": attachment_urls, + "track_opens": track_opens, + "track_clicks": track_clicks, + } + + res = self._post_request("emails", params=email_params) + + if res.status_code not in (201, 404, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 404: + raise HTTPError("User not found") + + if res.status_code == 422: + raise HTTPError("Missing required parameters") + + return res.text diff --git a/parsons/solidarity_tech/solidarity_tech_exceptions.py b/parsons/solidarity_tech/solidarity_tech_exceptions.py new file mode 100644 index 0000000000..e6d6ddcfc0 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_exceptions.py @@ -0,0 +1,13 @@ +import requests +from requests.exceptions import HTTPError + + +class STUnexpectedResponseCodeError(HTTPError): + """Status code is not expected.""" + + def __init__(self, res: requests.Response) -> None: + super() + self.status_code = res.status_code + + def __str__(self): + return f"Received unexpected response. (Status Code: {self.status_code})" From 198df60faa948322752342e3e7f42bb22d69e702 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 26 Jul 2026 22:12:01 -0400 Subject: [PATCH 05/94] Add SolidarityTech initialization tests --- test/test_solidarity_tech/conftest.py | 8 ++++++++ .../test_solidarity_tech_init.py | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 test/test_solidarity_tech/conftest.py create mode 100644 test/test_solidarity_tech/test_solidarity_tech_init.py diff --git a/test/test_solidarity_tech/conftest.py b/test/test_solidarity_tech/conftest.py new file mode 100644 index 0000000000..415896e671 --- /dev/null +++ b/test/test_solidarity_tech/conftest.py @@ -0,0 +1,8 @@ +import pytest + +from parsons.solidarity_tech.solidarity_tech import SolidarityTech + + +@pytest.fixture +def st() -> SolidarityTech: + return SolidarityTech(api_token="SOME_API_KEY") diff --git a/test/test_solidarity_tech/test_solidarity_tech_init.py b/test/test_solidarity_tech/test_solidarity_tech_init.py new file mode 100644 index 0000000000..eb989cfa17 --- /dev/null +++ b/test/test_solidarity_tech/test_solidarity_tech_init.py @@ -0,0 +1,20 @@ +import os +from unittest import mock + +import pytest + +from parsons.solidarity_tech.solidarity_tech import SolidarityTech + + +def test_init_with_arg() -> None: + SolidarityTech(api_token="SOME_API_KEY") + + +@mock.patch.dict(os.environ, {"SOLIDARITY_TECH_TOKEN": "SOME_API_KEY"}) +def test_init_with_env() -> None: + SolidarityTech() + + +def test_init_with_no_api_token() -> None: + with pytest.raises(KeyError, match="No 'SOLIDARITY_TECH_TOKEN' found."): + SolidarityTech() From 8e9bb73d209b54b6a36f57528f6f97b52955d2fd Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 26 Jul 2026 22:18:02 -0400 Subject: [PATCH 06/94] Ensure api token is saved in connector --- .../test_solidarity_tech/test_solidarity_tech_init.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/test_solidarity_tech/test_solidarity_tech_init.py b/test/test_solidarity_tech/test_solidarity_tech_init.py index eb989cfa17..91ed57391a 100644 --- a/test/test_solidarity_tech/test_solidarity_tech_init.py +++ b/test/test_solidarity_tech/test_solidarity_tech_init.py @@ -5,14 +5,19 @@ from parsons.solidarity_tech.solidarity_tech import SolidarityTech +PLACEHOLDER_TOKEN = "SOME_API_KEY" def test_init_with_arg() -> None: - SolidarityTech(api_token="SOME_API_KEY") + st = SolidarityTech(api_token=PLACEHOLDER_TOKEN) + assert st.api_token == PLACEHOLDER_TOKEN + assert st.headers.get("authorization") == f"Bearer {PLACEHOLDER_TOKEN}" -@mock.patch.dict(os.environ, {"SOLIDARITY_TECH_TOKEN": "SOME_API_KEY"}) +@mock.patch.dict(os.environ, {"SOLIDARITY_TECH_TOKEN": PLACEHOLDER_TOKEN}) def test_init_with_env() -> None: - SolidarityTech() + st = SolidarityTech() + assert st.api_token == PLACEHOLDER_TOKEN + assert st.headers.get("authorization") == f"Bearer {PLACEHOLDER_TOKEN}" def test_init_with_no_api_token() -> None: From f80ce2055c688e5d3a69a2a1beedf97d59f890b1 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Mon, 27 Jul 2026 20:54:41 -0400 Subject: [PATCH 07/94] Add methods for event attendances --- parsons/solidarity_tech/solidarity_tech.py | 2 + .../solidarity_tech_event_attendances.py | 138 ++++++++++++++++++ .../test_solidarity_tech_init.py | 1 + 3 files changed, 141 insertions(+) create mode 100644 parsons/solidarity_tech/solidarity_tech_event_attendances.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 154f6b3546..0f2247d506 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -17,6 +17,7 @@ from parsons.solidarity_tech.solidarity_tech_email_blasts import SolidarityTechEmailBlasts from parsons.solidarity_tech.solidarity_tech_email_senders import SolidarityTechEmailSenders from parsons.solidarity_tech.solidarity_tech_emails import SolidarityTechEmails +from parsons.solidarity_tech.solidarity_tech_event_attendances import SolidarityTechEventAttendances logger = logging.getLogger(__name__) @@ -33,5 +34,6 @@ class SolidarityTech( SolidarityTechEmailBlasts, SolidarityTechEmailSenders, SolidarityTechEmails, + SolidarityTechEventAttendances, ): pass diff --git a/parsons/solidarity_tech/solidarity_tech_event_attendances.py b/parsons/solidarity_tech/solidarity_tech_event_attendances.py new file mode 100644 index 0000000000..5ccfe4142c --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_event_attendances.py @@ -0,0 +1,138 @@ +import logging +from datetime import datetime + +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechEventAttendances(SolidarityTechBase): + def get_event_attendances( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + event_id: int | None = None, + session_id: int | None = None, + ) -> str: + """ + Retrieve a list of event attendances. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + event_id: + Filters attendances by event_id within the accessible scope. + session_id: + Filters attendances by session_id (calendar item id) within the accessible scope. + + Returns: + All the event attendance entries. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "event_attendances", + limit=limit, + offset=offset, + since=since, + event_id=event_id, + session_id=session_id, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_event_attendance( + self, + event_id: int, + event_session_id: int, + user_id: int, + attended: bool, + ) -> bool: + """ + Creates an event attendance with the specified details. + + Args: + event_id: + Identifier for the Mobilize event. + event_session_id: + Identifier for the specific event session. + user_id: + Identifier for the user attending to the event. + attended: + Indicates if the user attended the event. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "attended": attended, + "event_id": event_id, + "event_session_id": event_session_id, + "user_id": user_id, + } + res = self._post_request( + "event_attendances", payload, additional_headers={"content-type": "application/json"} + ) + + if res.status_code not in (201, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 201 + + def delete_event_attendance( + self, + id: str, + ) -> bool: + """ + Deletes an event attendance with the specified ID. + + Args: + id: + Identifier of the event attendance to delete + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: Validation failed. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._del_request( + "event_attendances", + id, + ) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 404: + raise HTTPError("Event attendance not found") + + return res.status_code == 200 diff --git a/test/test_solidarity_tech/test_solidarity_tech_init.py b/test/test_solidarity_tech/test_solidarity_tech_init.py index 91ed57391a..49b21fbed7 100644 --- a/test/test_solidarity_tech/test_solidarity_tech_init.py +++ b/test/test_solidarity_tech/test_solidarity_tech_init.py @@ -7,6 +7,7 @@ PLACEHOLDER_TOKEN = "SOME_API_KEY" + def test_init_with_arg() -> None: st = SolidarityTech(api_token=PLACEHOLDER_TOKEN) assert st.api_token == PLACEHOLDER_TOKEN From 11ff17c715c73c90bdf41f29a66866adb27cd343 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Mon, 27 Jul 2026 21:15:34 -0400 Subject: [PATCH 08/94] Use numpy.int64 where appropriate --- .../solidarity_tech_agent_assignments.py | 10 ++++++---- .../solidarity_tech_event_attendances.py | 12 ++++-------- pyproject.toml | 5 ++++- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/parsons/solidarity_tech/solidarity_tech_agent_assignments.py b/parsons/solidarity_tech/solidarity_tech_agent_assignments.py index 29e58a29a7..24a0ca32b0 100644 --- a/parsons/solidarity_tech/solidarity_tech_agent_assignments.py +++ b/parsons/solidarity_tech/solidarity_tech_agent_assignments.py @@ -1,6 +1,8 @@ import logging from datetime import datetime +import numpy as np + from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError @@ -83,8 +85,8 @@ def get_agent_assignment( def create_agent_assignment( self, - user_id: int, - agent_user_id: int, + user_id: np.int64, + agent_user_id: np.int64, is_active: bool | None = None, ) -> bool: """ @@ -122,8 +124,8 @@ def create_agent_assignment( def update_agent_assignment( self, id: int, - user_id: int, - agent_user_id: int, + user_id: np.int64, + agent_user_id: np.int64, is_active: bool | None = None, ) -> bool: """ diff --git a/parsons/solidarity_tech/solidarity_tech_event_attendances.py b/parsons/solidarity_tech/solidarity_tech_event_attendances.py index 5ccfe4142c..a1d4306bb7 100644 --- a/parsons/solidarity_tech/solidarity_tech_event_attendances.py +++ b/parsons/solidarity_tech/solidarity_tech_event_attendances.py @@ -1,7 +1,7 @@ import logging from datetime import datetime -from requests.exceptions import HTTPError +import numpy as np from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError @@ -57,9 +57,9 @@ def get_event_attendances( def create_event_attendance( self, - event_id: int, - event_session_id: int, - user_id: int, + event_id: np.int64, + event_session_id: np.int64, + user_id: np.int64, attended: bool, ) -> bool: """ @@ -117,7 +117,6 @@ def delete_event_attendance( True if the operation was successful, False otherwise. Raises: - HTTPError: Validation failed. STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. Documentation Reference: @@ -132,7 +131,4 @@ def delete_event_attendance( if res.status_code not in (200, 404): raise STUnexpectedResponseCodeError(res) - if res.status_code == 404: - raise HTTPError("Event attendance not found") - return res.status_code == 200 diff --git a/pyproject.toml b/pyproject.toml index 25bde99cb4..cdbeb5f3fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,10 @@ scytl = ["defusedxml >= 0.7"] sftp = ["paramiko >= 3.0"] slack = ["slack-sdk >= 3.26"] smtp = ["email-validator >= 2.2"] -solidarity-tech = ["pyrate-limiter >= 4.4"] +solidarity-tech = [ + "numpy >= 2.0", + "pyrate-limiter >= 4.4" +] ssh = [ "sshtunnel >= 0.4", "psycopg2-binary >= 2.9.11", From 498740fb5566df68d5c2ee04fd5c08509716922b Mon Sep 17 00:00:00 2001 From: Ramona T Date: Mon, 27 Jul 2026 21:32:54 -0400 Subject: [PATCH 09/94] Update solidarity_tech_agent_assignments.py --- parsons/solidarity_tech/solidarity_tech_agent_assignments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsons/solidarity_tech/solidarity_tech_agent_assignments.py b/parsons/solidarity_tech/solidarity_tech_agent_assignments.py index 24a0ca32b0..c200ce4f9f 100644 --- a/parsons/solidarity_tech/solidarity_tech_agent_assignments.py +++ b/parsons/solidarity_tech/solidarity_tech_agent_assignments.py @@ -192,4 +192,4 @@ def delete_agent_assignment( if res and res.status_code != 404: raise STUnexpectedResponseCodeError(res) - return res.status_code == 404 + return not res.status_code From c364bd85ffd74c7fc92ec9fe887e620a582517be Mon Sep 17 00:00:00 2001 From: Ramona T Date: Mon, 27 Jul 2026 21:33:11 -0400 Subject: [PATCH 10/94] Add Event RSVP methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + .../solidarity_tech_event_rsvps.py | 252 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 parsons/solidarity_tech/solidarity_tech_event_rsvps.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 0f2247d506..d10e9687c8 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -18,6 +18,7 @@ from parsons.solidarity_tech.solidarity_tech_email_senders import SolidarityTechEmailSenders from parsons.solidarity_tech.solidarity_tech_emails import SolidarityTechEmails from parsons.solidarity_tech.solidarity_tech_event_attendances import SolidarityTechEventAttendances +from parsons.solidarity_tech.solidarity_tech_event_rsvps import SolidarityTechEventRSVPs logger = logging.getLogger(__name__) @@ -35,5 +36,6 @@ class SolidarityTech( SolidarityTechEmailSenders, SolidarityTechEmails, SolidarityTechEventAttendances, + SolidarityTechEventRSVPs, ): pass diff --git a/parsons/solidarity_tech/solidarity_tech_event_rsvps.py b/parsons/solidarity_tech/solidarity_tech_event_rsvps.py new file mode 100644 index 0000000000..042e2978a1 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_event_rsvps.py @@ -0,0 +1,252 @@ +import logging +from datetime import datetime +from typing import Literal + +import numpy as np + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechEventRSVPs(SolidarityTechBase): + def get_event_rsvps( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + event_id: int | None = None, + session_id: int | None = None, + user_id: int | None = None, + full_user_payload: bool = False, + ) -> str: + """ + Retrieve a list of event rsvps. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + event_id: + Filters rsvps by event_id within the accessible scope. + session_id: + Filters rsvps by session_id (calendar item id) within the accessible scope. + user_id: + Filters rsvps by user_id within the accessible scope. + full_user_payload: + If True, includes complete user data in the response instead of just basic details. + + Returns: + All the event rsvps. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "event_rsvps", + limit=limit, + offset=offset, + since=since, + event_id=event_id, + session_id=session_id, + user_id=user_id, + full_user_payload=full_user_payload, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_event_rsvp( + self, + id: int, + full_user_payload: bool = False, + ) -> str: + """ + Retrieve a single event rsvp. + + Args: + id: + ID of the event rsvp to retrieve. + full_user_payload: + If True, includes complete user data in the response instead of just basic details. + + Returns: + A single event rsvp. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + params = {"full_user_payload": full_user_payload} + res = self._get_single_resource("event_rsvps", id, params=params) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_event_rsvp( + self, + event_id: np.int64, + event_session_id: np.int64, + is_attending: Literal["yes", "no", "maybe", "waitlisted"], + agent_user_id: np.int64 | None, + user_id: np.int64 | None = None, + is_confirmed: bool | None = None, + source: str | None = None, + source_system: str | None = None, + skip_email_confirmation: bool = False, + ) -> bool: + """ + Creates an event rsvp with the specified details. + + Args: + event_id: + Identifier for the Mobilize event. + event_session_id: + Identifier for the specific event session. + is_attending: + Indicates if the user is attending the event. + agent_user_id: + Identifier for the agent user, if applicable. + user_id: + Identifier for the user RSVPing to the event. + is_confirmed: + Indicates if the RSVP is confirmed. + source: + Source of the RSVP. + source_system: + System from which the RSVP originated. + skip_email_confirmation: + If True, skips sending the initial email confirmation to the user. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "is_attending": is_attending, + "agent_user_id": agent_user_id, + "event_id": event_id, + "event_session_id": event_session_id, + "user_id": user_id, + "is_confirmed": is_confirmed, + "source": source, + "source_system": source_system, + "skip_email_confirmation": skip_email_confirmation, + } + res = self._post_request( + "event_rsvps", payload, additional_headers={"content-type": "application/json"} + ) + + if res.status_code not in (201, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 201 + + def update_event_rsvp( + self, + id: int, + is_attending: Literal["yes", "no", "maybe", "waitlisted"] | None = None, + is_confirmed: bool | None = None, + agent_user_id: np.int64 | None = None, + source: str | None = None, + source_system: str | None = None, + ) -> bool: + """ + Updates an event rsvp with the specified details. + + Args: + id: + Identifier of the event rsvp to update. + is_attending: + Indicates if the user is attending the event. + is_confirmed: + Indicates if the RSVP is confirmed. + agent_user_id: + Identifier for the agent user, if applicable. + source: + Source of the RSVP. + source_system: + System from which the RSVP originated. + + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + is_attending: is_attending, + is_confirmed: is_confirmed, + agent_user_id: agent_user_id, + source: source, + source_system: source_system, + } + res = self._put_request( + "event_rsvps", + id, + payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 + + def delete_event_attendance( + self, + id: str, + ) -> bool: + """ + Deletes an event rsvp with the specified ID. + + Args: + id: + Identifier of the event rsvp to delete + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._del_request( + "event_rsvps", + id, + ) + + if res and res.status_code != 404: + raise STUnexpectedResponseCodeError(res) + + return not res.status_code From 750645e6eb2f69fbca167c052d30406465d39a05 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Mon, 27 Jul 2026 22:04:36 -0400 Subject: [PATCH 11/94] Allow passing params directly to _get_resources, to combine with the special cases. --- parsons/solidarity_tech/solidarity_tech_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index 1b66200301..a4aa391a53 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -47,6 +47,12 @@ def _get_resources(self, endpoint: str, limit: int, **kwargs) -> requests.Respon params[query_key] = value del kwargs[key] + if kwargs.get("params"): + for key, value in kwargs.get("params", {}).items(): + if value is None: + continue + params[key] = value + logger.debug("Processing GET request at endpoint: %s", endpoint, extra=params) return self.api.request(url=endpoint, req_type="GET", params=params, **kwargs) From 83ba623911c8002ad7e576ac4084e06a3a20401b Mon Sep 17 00:00:00 2001 From: Ramona T Date: Mon, 27 Jul 2026 22:07:04 -0400 Subject: [PATCH 12/94] Fix test folder name --- test/{test_solidarity_tech => test_solidarity-tech}/conftest.py | 0 .../test_solidarity_tech_init.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename test/{test_solidarity_tech => test_solidarity-tech}/conftest.py (100%) rename test/{test_solidarity_tech => test_solidarity-tech}/test_solidarity_tech_init.py (100%) diff --git a/test/test_solidarity_tech/conftest.py b/test/test_solidarity-tech/conftest.py similarity index 100% rename from test/test_solidarity_tech/conftest.py rename to test/test_solidarity-tech/conftest.py diff --git a/test/test_solidarity_tech/test_solidarity_tech_init.py b/test/test_solidarity-tech/test_solidarity_tech_init.py similarity index 100% rename from test/test_solidarity_tech/test_solidarity_tech_init.py rename to test/test_solidarity-tech/test_solidarity_tech_init.py From ffe00f52c6992857e234240e13e67416f2795f2f Mon Sep 17 00:00:00 2001 From: Ramona T Date: Tue, 28 Jul 2026 09:27:32 -0400 Subject: [PATCH 13/94] Bump min numpy to 2.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cdbeb5f3fa..626d0c54ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,7 +128,7 @@ sftp = ["paramiko >= 3.0"] slack = ["slack-sdk >= 3.26"] smtp = ["email-validator >= 2.2"] solidarity-tech = [ - "numpy >= 2.0", + "numpy >= 2.2", "pyrate-limiter >= 4.4" ] ssh = [ From c31d4400c098218014d933c9b161ca7c97b48e1a Mon Sep 17 00:00:00 2001 From: Ramona T Date: Tue, 28 Jul 2026 09:55:14 -0400 Subject: [PATCH 14/94] Adjust dependency versions for more python version coverage --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 626d0c54ef..9321b2c0bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,8 +128,9 @@ sftp = ["paramiko >= 3.0"] slack = ["slack-sdk >= 3.26"] smtp = ["email-validator >= 2.2"] solidarity-tech = [ - "numpy >= 2.2", - "pyrate-limiter >= 4.4" + "numpy >= 2.1;python_version<'3.14'", + "numpy >= 2.3;python_version>='3.14'", + "pyrate-limiter >= 4.0", ] ssh = [ "sshtunnel >= 0.4", From fad1af3792b8d7bfdeba50862c39523899bd4a2d Mon Sep 17 00:00:00 2001 From: Ramona T Date: Tue, 28 Jul 2026 10:15:44 -0400 Subject: [PATCH 15/94] Bump min numpy for python 3.14 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9321b2c0bb..cb73c2b43d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,7 +129,7 @@ slack = ["slack-sdk >= 3.26"] smtp = ["email-validator >= 2.2"] solidarity-tech = [ "numpy >= 2.1;python_version<'3.14'", - "numpy >= 2.3;python_version>='3.14'", + "numpy >= 2.4;python_version>='3.14'", "pyrate-limiter >= 4.0", ] ssh = [ From ff58f330d102df942520c8c5b76e4cef34692ccb Mon Sep 17 00:00:00 2001 From: Ramona T Date: Tue, 28 Jul 2026 20:29:37 -0400 Subject: [PATCH 16/94] fix method name in event_rsvps --- parsons/solidarity_tech/solidarity_tech_event_rsvps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsons/solidarity_tech/solidarity_tech_event_rsvps.py b/parsons/solidarity_tech/solidarity_tech_event_rsvps.py index 042e2978a1..7fbe21dc1e 100644 --- a/parsons/solidarity_tech/solidarity_tech_event_rsvps.py +++ b/parsons/solidarity_tech/solidarity_tech_event_rsvps.py @@ -219,7 +219,7 @@ def update_event_rsvp( return res.status_code == 200 - def delete_event_attendance( + def delete_event_rsvp( self, id: str, ) -> bool: From 010b1a0282de0cedf607a671fb3676ce07d4b45e Mon Sep 17 00:00:00 2001 From: Ramona T Date: Tue, 28 Jul 2026 20:29:48 -0400 Subject: [PATCH 17/94] Add event sessions methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + .../solidarity_tech/solidarity_tech_base.py | 3 +- .../solidarity_tech_event_sessions.py | 430 ++++++++++++++++++ 3 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 parsons/solidarity_tech/solidarity_tech_event_sessions.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index d10e9687c8..1c6a585559 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -19,6 +19,7 @@ from parsons.solidarity_tech.solidarity_tech_emails import SolidarityTechEmails from parsons.solidarity_tech.solidarity_tech_event_attendances import SolidarityTechEventAttendances from parsons.solidarity_tech.solidarity_tech_event_rsvps import SolidarityTechEventRSVPs +from parsons.solidarity_tech.solidarity_tech_event_sessions import SolidarityTechEventSessions logger = logging.getLogger(__name__) @@ -37,5 +38,6 @@ class SolidarityTech( SolidarityTechEmails, SolidarityTechEventAttendances, SolidarityTechEventRSVPs, + SolidarityTechEventSessions, ): pass diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index a4aa391a53..5b6ea55e43 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -2,6 +2,7 @@ from collections.abc import Mapping from datetime import datetime +import numpy as np import requests from parsons.solidarity_tech.ratelimited_api_connector import RateLimitedAPIConnector @@ -63,7 +64,7 @@ def _get_single_resource(self, endpoint: str, id: int, **kwargs) -> requests.Res return self.api.request(url=complete_endpoint, req_type="GET", **kwargs) def _post_request( - self, endpoint: str, payload: Mapping[str, str | int] | None = None, **kwargs + self, endpoint: str, payload: Mapping[str, str | int | np.int64] | None = None, **kwargs ) -> requests.Response: """Handle POST requests.""" logger.debug("Processing POST request at endpoint: %s", endpoint, extra=payload) diff --git a/parsons/solidarity_tech/solidarity_tech_event_sessions.py b/parsons/solidarity_tech/solidarity_tech_event_sessions.py new file mode 100644 index 0000000000..0a38215180 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_event_sessions.py @@ -0,0 +1,430 @@ +import logging +from datetime import datetime +from typing import Literal + +import numpy as np +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechEventSessions(SolidarityTechBase): + def get_event_sessions( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime = 0, + event_id: int = 0, + upcoming: bool | None = None, + starts_after: int | datetime | None = None, + starts_before: int | datetime | None = None, + chapter_id: int | None = None, + event_tags: str | None = None, + include_rsvp_counts: bool | None = None, + include_confirmed_counts: bool | None = None, + include_hosts: bool | None = None, + count: bool | None = None, + ) -> str: + """ + Retrieve a list of event rsvps. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + event_id: + Filters sessions by event_id within the accessible scope. + upcoming: + If True, returns only sessions that have not ended yet, + sorted by start_time ascending (soonest first). + starts_after: + UTC timestamp in seconds since the Unix epoch; + only sessions with start_time at or after this moment. + starts_before: + UTC timestamp in seconds since the Unix epoch; + only sessions with start_time at or before this moment. + chapter_id: + Only sessions of events scoped to this chapter. + Chapters outside your accessible scope simply match nothing. + event_tags: + Comma-separated list of tags. + Matches sessions whose own tags OR whose parent event tags overlap with the list. + include_rsvp_counts: + If True, each session in the response includes an rsvp_counts object keyed by RSVP status. + (e.g. {"yes": 12, "no": 3}) + include_confirmed_counts: + If True, each session includes a confirmed_counts object + (the same per-status breakdown as rsvp_counts, restricted to RSVPs an organizer confirmed). + include_hosts: + If True, each session includes a hosts array of + {id, first_name, last_name} objects resolved from host_user_ids, in host order. + count: + If True, returns {"count": n} of matching sessions instead of the rows. + Combines with all other filters. + + Returns: + All the event sessions. + + Documentation Reference: + ``__ + + """ + params = { + "event_id": event_id, + "upcoming": upcoming, + "starts_after": starts_after, + "starts_before": starts_before, + "chapter_id": chapter_id, + "event_tags": event_tags, + "include_rsvp_counts": include_rsvp_counts, + "include_confirmed_counts": include_confirmed_counts, + "include_hosts": include_hosts, + "count": count, + } + res = self._get_resources( + "event_sessions", + limit=limit, + offset=offset, + since=since, + params=params, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_event_sessions( + self, + event_id: np.int64, + start_time: np.int64, + end_time: np.int64, + title: str, + event_type: Literal["virtual", "in_person"] | None = None, + location_name: str | None = None, + location_data: dict[str, str] | None = None, + location_address: str | None = None, + show_rsvp_bar: bool | None = None, + show_title_in_form: bool | None = None, + note: str | None = None, + max_capacity: int | None = None, + tags: list[str] | None = None, + ) -> bool: + """ + Creates an event rsvp with the specified details. + + Args: + event_id: + Identifier for the Mobilize event. + start_time: + UTC timestamp in seconds since the Unix epoch. + end_time: + UTC timestamp in seconds since the Unix epoch. + title: + Title of the event session. + event_type: + Type of session. + location_name: + Name of the location. + location_data: + Structured location details. + Components and coordinates may be sent either as native JSON (array/object) + or as JSON-encoded strings; both are stored and returned as JSON strings. + Neighborhood is the components entry whose types include "neighborhood"; + NYC borough is the entry whose types include "sublocality_level_1". + location_address: + Physical address of the event location. + show_rsvp_bar: + Flag to show RSVP buttons bar. + show_title_in_form: + Flag to show title in the form. + note: + Additional notes for the event session. + max_capacity: + Maximum capacity for the event session. + tags: + Array of tags for the event session. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: If the entity is not processable. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "event_id": event_id, + "start_time": start_time, + "end_time": end_time, + "event_type": event_type, + "title": title, + "location_name": location_name, + "location_data": location_data, + "location_address": location_address, + "show_rsvp_bar": show_rsvp_bar, + "show_title_in_form": show_title_in_form, + "note": note, + "max_capacity": max_capacity, + "tags": tags, + } + res = self._post_request( + "event_rsvps", payload, additional_headers={"content-type": "application/json"} + ) + + if res.status_code not in (201, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 422: + raise HTTPError("Unprocessable entity") + + return res.status_code == 201 + + def get_event_session( + self, + id: int, + include_hosts: bool = False, + ) -> str: + """ + Retrieve a single event session. + + Args: + id: + ID of the event session to retrieve. + include_hosts: + If True, the session includes a hosts array of + {id, first_name, last_name} objects resolved from host_user_ids, + in host order. + + Returns: + A single event session. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + params = {"include_hosts": include_hosts} + res = self._get_single_resource("event_sessions", id, params=params) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text + + def update_event_session( + self, + id: int, + start_time: np.int64 | None = None, + end_time: np.int64 | None = None, + title: str | None = None, + location_name: str | None = None, + location_address: str | None = None, + location_data: dict[str, str] | None = None, + show_rsvp_bar: bool | None = None, + show_title_in_form: bool | None = None, + note: str | None = None, + max_capacity: int | None = None, + tags: list[str] | None = None, + ) -> bool: + """ + Updates an event session with the specified details. + + Args: + id: + Identifier of the event session to update. + start_time: + UTC timestamp in seconds since the Unix epoch. + end_time: + UTC timestamp in seconds since the Unix epoch. + title: + Title of the event session. + location_name: + Name of the location. + location_address: + Physical address of the event location. + location_data: + See :meth:`create_event_session`. + ``components``/``coordinates`` accept native JSON or JSON strings + and are stored/returned as JSON strings. + Omit to leave the existing location_data unchanged. + show_rsvp_bar: + Flag to show RSVP buttons bar. + show_title_in_form: + Flag to show title in the form. + note: + Additional notes for the event session. + max_capacity: + Maximum capacity of the event session. + tags: + List of tags for the event session. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: Unprocessable Entity (422) error. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "start_time": start_time, + "end_time": end_time, + "title": title, + "location_name": location_name, + "location_address": location_address, + "location_data": location_data, + "show_rsvp_bar": show_rsvp_bar, + "show_title_in_form": show_title_in_form, + "note": note, + "max_capacity": max_capacity, + "tags": tags, + } + res = self._put_request( + "event_sessions", + id, + payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code not in (200, 404, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 422: + raise HTTPError("Unprocessable entity") + + return res.status_code == 200 + + def delete_event_session( + self, + id: str, + ) -> bool: + """ + Deletes an event session with the specified ID. + + Args: + id: + Identifier of the event session to delete + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._del_request( + "event_sessions", + id, + ) + + if res and res.status_code != 404: + raise STUnexpectedResponseCodeError(res) + + return not res.status_code + + def add_event_host( + self, + id: int, + user_id: np.int64, + ) -> bool: + """ + Adds a user as a host of the event session. + + .. admonition:: Atomic and Idempotent + + Adding a user who is already a host returns 200 without duplicating. + The user must belong to your organization. + Hosts are readable on the session as host_user_ids and usable in + message templates via the {{ event-session.hosts }}, + {{ event-session.host }}, and {{ event-session.host-names }} merge tags. + + Args: + id: + Identifier of the event session. + user_id: + ID of the user to add as a host. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "user_id": user_id, + } + res = self._post_request( + "event_sessions", payload, additional_headers={"content-type": "application/json"} + ) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 + + def remove_event_host( + self, + id: str, + user_id: int, + ) -> bool: + """ + Removes a user from the event session hosts. + + .. admonition:: Atomic and Idempotent + + Removing a user who is not a host returns 200. + + + Args: + id: + Identifier of the event session. + user_id: + ID of the user to remove from hosts. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._del_request( + "event_sessions", + f"{id}/hosts/{user_id}", + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 From b8bcca18e50b5c493dfce673f7add207c7d09894 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:05:32 -0400 Subject: [PATCH 18/94] add event methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + .../solidarity_tech/solidarity_tech_events.py | 215 ++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 parsons/solidarity_tech/solidarity_tech_events.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 1c6a585559..5379cba126 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -20,6 +20,7 @@ from parsons.solidarity_tech.solidarity_tech_event_attendances import SolidarityTechEventAttendances from parsons.solidarity_tech.solidarity_tech_event_rsvps import SolidarityTechEventRSVPs from parsons.solidarity_tech.solidarity_tech_event_sessions import SolidarityTechEventSessions +from parsons.solidarity_tech.solidarity_tech_events import SolidarityTechEvents logger = logging.getLogger(__name__) @@ -39,5 +40,6 @@ class SolidarityTech( SolidarityTechEventAttendances, SolidarityTechEventRSVPs, SolidarityTechEventSessions, + SolidarityTechEvents, ): pass diff --git a/parsons/solidarity_tech/solidarity_tech_events.py b/parsons/solidarity_tech/solidarity_tech_events.py new file mode 100644 index 0000000000..3164bbb02d --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_events.py @@ -0,0 +1,215 @@ +import logging +from datetime import datetime +from typing import Literal + +import numpy as np +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechEvents(SolidarityTechBase): + def get_events( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime = 0, + scope_id: int | None = None, + scope_type: Literal["Organization", "Chapter"] | None = None, + ) -> str: + """ + Lists events accessible within the given scope. + + Each event in the response includes ``primary_event_id`` and ``is_co_hosted_mirror``. + For co-hosted events that appear across multiple organizations, + ``primary_event_id`` always resolves to the original event ID, + allowing you to identify that two events from different scopes represent the same real world event. + Each event session also includes ``primary_session_id`` for the same purpose. + Events with an event page also include ``image_url`` and ``description`` fields, plus ``accessibility_info``; this is + an optional per-language hash of accessibility details from the event page settings + (e.g. {"en": "Wheelchair accessible entrance"}), null when not provided. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + scope_id: + ID of the scope to filter events by. + scope_type: + Type of the scope to filter events by. + + Returns: + All the events. + + Documentation Reference: + ``__ + + """ + params = { + "scope_id": scope_id, + "scope_type": scope_type, + } + res = self._get_resources( + "events", + limit=limit, + offset=offset, + since=since, + params=params, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_event( + self, + title: str, + event_type: Literal["virtual", "in_person", "hybrid"], + start_time: np.int64, + end_time: np.int64, + scope_id: str, + scope_type: Literal["Organization", "Chapter"], + location_address: str | None = None, + virtual_url: str | None = None, + location_name: str | None = None, + session_title: str | None = None, + tags: list[str] | None = None, + max_capacity: int | None = None, + latitude: float | None = None, + longitude: float | None = None, + skip_duplicate_check: bool = False, + ) -> bool: + """ + Creates an event with its first event session. + + The event session inherits the title from the event unless ``session_title`` is provided. + + Args: + title: + Event title (max 65 characters). + event_type: + Type of event. + start_time: + Start time as UNIX timestamp. + end_time: + End time as UNIX timestamp. + location_address: + For virtual: meeting URL. + For in_person and hybrid: street address for the in-person session. + virtual_url: + Meeting URL for the virtual session when event_type is hybrid. + location_name: + Display name for location (e.g., "City Hall"). + scope_id: + ID of the scope (Organization or Chapter). + scope_type: + Type of scope. + session_title: + Title for the first event session (defaults to event title). + tags: + Event tags. + max_capacity: + Maximum capacity for the event session (0 = unlimited). + latitude: + Latitude for ``in_person`` events (optional, will geocode if not provided). + longitude: + Longitude for ``in_person`` events (optional, will geocode if not provided). + skip_duplicate_check: + If True, bypasses duplicate event detection. Default is False. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: If the required scope is not found. + HTTPError: If a duplicate event is detected. + HTTPError: If event data validation fails. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "title": title, + "event_type": event_type, + "start_time": start_time, + "end_time": end_time, + "location_address": location_address, + "virtual_url": virtual_url, + "location_name": location_name, + "scope_id": scope_id, + "scope_type": scope_type, + "session_title": session_title, + "tags": tags, + "max_capacity": max_capacity, + "latitude": latitude, + "longitude": longitude, + "skip_duplicate_check": skip_duplicate_check, + } + res = self._post_request( + "events", payload, additional_headers={"content-type": "application/json"} + ) + + if res.status_code not in (201, 404, 409, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 404: + raise HTTPError("Event scope not found") + + if res.status_code == 409: + raise HTTPError("Duplicate event detected") + + if res.status_code == 422: + raise HTTPError("Data validation error") + + return res.status_code == 201 + + def get_event( + self, + id: int, + include_hosts: bool = False, + ) -> str: + """ + Returns a single event. + + The response includes ``primary_event_id`` + (always resolves to the original event ID, even for co-hosted mirrors) and + ``is_co_hosted_mirror`` (true if this event is a mirror copy from a co-host relationship). + Event sessions include ``primary_session_id`` for the same purpose. + If the event has an event page, the response also includes ``image_url`` (the event page image), + ``description`` (plain text content from the event page), and ``accessibility_info`` + (an optional per-language hash of accessibility details from the event page settings, + e.g. {"en": "Wheelchair accessible entrance"}). + These fields are null when no event page exists or the value is not set. + + Args: + id: + ID of the event to retrieve. + + Returns: + A single event. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + params = {"include_hosts": include_hosts} + res = self._get_single_resource("event_sessions", id, params=params) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text From 0882b047da5bd1834d4d6dd48b6d68d08013b974 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:12:44 -0400 Subject: [PATCH 19/94] Add field survey urls method --- parsons/solidarity_tech/solidarity_tech.py | 2 + .../solidarity_tech_field_survey_urls.py | 65 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 parsons/solidarity_tech/solidarity_tech_field_survey_urls.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 5379cba126..e88bcd1ae8 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -21,6 +21,7 @@ from parsons.solidarity_tech.solidarity_tech_event_rsvps import SolidarityTechEventRSVPs from parsons.solidarity_tech.solidarity_tech_event_sessions import SolidarityTechEventSessions from parsons.solidarity_tech.solidarity_tech_events import SolidarityTechEvents +from parsons.solidarity_tech.solidarity_tech_field_survey_urls import SolidarityTechFieldSurveyURLs logger = logging.getLogger(__name__) @@ -41,5 +42,6 @@ class SolidarityTech( SolidarityTechEventRSVPs, SolidarityTechEventSessions, SolidarityTechEvents, + SolidarityTechFieldSurveyURLs, ): pass diff --git a/parsons/solidarity_tech/solidarity_tech_field_survey_urls.py b/parsons/solidarity_tech/solidarity_tech_field_survey_urls.py new file mode 100644 index 0000000000..c25075ab8f --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_field_survey_urls.py @@ -0,0 +1,65 @@ +import logging + +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError + +logger = logging.getLogger(__name__) + + +class SolidarityTechFieldSurveyURLs(SolidarityTechBase): + def generate_field_survey_url( + self, + user_id: int, + agent_user_id: int, + page_id: int, + ) -> bool: + """ + Generates a field survey URL for the given user, agent, and page. + + Response contains complete URL with access token (expires in 24 hours), + and an ISO 8601 timestamp of when the access token expires. + + Args: + user_id: + The ID of the user to generate URL for. + agent_user_id: + The ID of the agent user. + page_id: + The ID of the action page (field survey). + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: If user, agent, or page are not found. + HTTPError: If required parameters are missing. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "user_id": user_id, + "agent_user_id": agent_user_id, + "page_id": page_id, + } + res = self._post_request( + "field_survey_urls", + payload, + additional_headers={"accept": "application/json", "content-type": "application/json"}, + ) + + if res.status_code not in (200, 404, 409, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 404: + raise HTTPError("User, agent, or page not found") + + if res.status_code == 422: + raise HTTPError("Required parameters are missing") + + return res.status_code == 200 From 49d083476291bf6b709edd830ea00342d16ff5a8 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:18:43 -0400 Subject: [PATCH 20/94] Shorter file names --- ...arity_tech_exceptions.py => exceptions.py} | 0 parsons/solidarity_tech/solidarity_tech.py | 38 ++++++++----------- .../solidarity_tech/solidarity_tech_base.py | 5 ++- ...ty_tech_activities.py => st_activities.py} | 2 +- ...assignments.py => st_agent_assignments.py} | 2 +- ...lments.py => st_automation_enrollments.py} | 2 +- .../{solidarity_tech_calls.py => st_calls.py} | 2 +- ...numbers.py => st_chapter_phone_numbers.py} | 2 +- ...darity_tech_chapters.py => st_chapters.py} | 2 +- ...erties.py => st_custom_user_properties.py} | 2 +- ...tion_charges.py => st_donation_charges.py} | 2 +- ...ech_email_blasts.py => st_email_blasts.py} | 2 +- ...h_email_senders.py => st_email_senders.py} | 2 +- ...solidarity_tech_emails.py => st_emails.py} | 2 +- ...attendances.py => st_event_attendances.py} | 2 +- ..._tech_event_rsvps.py => st_event_rsvps.py} | 2 +- ...event_sessions.py => st_event_sessions.py} | 2 +- ...solidarity_tech_events.py => st_events.py} | 2 +- ...survey_urls.py => st_field_survey_urls.py} | 2 +- 19 files changed, 36 insertions(+), 39 deletions(-) rename parsons/solidarity_tech/{solidarity_tech_exceptions.py => exceptions.py} (100%) rename parsons/solidarity_tech/{solidarity_tech_activities.py => st_activities.py} (95%) rename parsons/solidarity_tech/{solidarity_tech_agent_assignments.py => st_agent_assignments.py} (98%) rename parsons/solidarity_tech/{solidarity_tech_automation_enrollments.py => st_automation_enrollments.py} (94%) rename parsons/solidarity_tech/{solidarity_tech_calls.py => st_calls.py} (94%) rename parsons/solidarity_tech/{solidarity_tech_chapter_phone_numbers.py => st_chapter_phone_numbers.py} (94%) rename parsons/solidarity_tech/{solidarity_tech_chapters.py => st_chapters.py} (93%) rename parsons/solidarity_tech/{solidarity_tech_custom_user_properties.py => st_custom_user_properties.py} (98%) rename parsons/solidarity_tech/{solidarity_tech_donation_charges.py => st_donation_charges.py} (95%) rename parsons/solidarity_tech/{solidarity_tech_email_blasts.py => st_email_blasts.py} (95%) rename parsons/solidarity_tech/{solidarity_tech_email_senders.py => st_email_senders.py} (92%) rename parsons/solidarity_tech/{solidarity_tech_emails.py => st_emails.py} (96%) rename parsons/solidarity_tech/{solidarity_tech_event_attendances.py => st_event_attendances.py} (97%) rename parsons/solidarity_tech/{solidarity_tech_event_rsvps.py => st_event_rsvps.py} (98%) rename parsons/solidarity_tech/{solidarity_tech_event_sessions.py => st_event_sessions.py} (99%) rename parsons/solidarity_tech/{solidarity_tech_events.py => st_events.py} (98%) rename parsons/solidarity_tech/{solidarity_tech_field_survey_urls.py => st_field_survey_urls.py} (95%) diff --git a/parsons/solidarity_tech/solidarity_tech_exceptions.py b/parsons/solidarity_tech/exceptions.py similarity index 100% rename from parsons/solidarity_tech/solidarity_tech_exceptions.py rename to parsons/solidarity_tech/exceptions.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index e88bcd1ae8..7e278e3ba5 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -1,27 +1,21 @@ import logging -from parsons.solidarity_tech.solidarity_tech_activities import SolidarityTechActivities -from parsons.solidarity_tech.solidarity_tech_agent_assignments import SolidarityTechAgentAssignments -from parsons.solidarity_tech.solidarity_tech_automation_enrollments import ( - SolidarityTechAutomationEnrollments, -) -from parsons.solidarity_tech.solidarity_tech_calls import SolidarityTechCalls -from parsons.solidarity_tech.solidarity_tech_chapter_phone_numbers import ( - SolidarityTechChapterPhoneNumbers, -) -from parsons.solidarity_tech.solidarity_tech_chapters import SolidarityTechChapters -from parsons.solidarity_tech.solidarity_tech_custom_user_properties import ( - SolidarityTechCustomUserProperties, -) -from parsons.solidarity_tech.solidarity_tech_donation_charges import SolidarityTechDonationCharges -from parsons.solidarity_tech.solidarity_tech_email_blasts import SolidarityTechEmailBlasts -from parsons.solidarity_tech.solidarity_tech_email_senders import SolidarityTechEmailSenders -from parsons.solidarity_tech.solidarity_tech_emails import SolidarityTechEmails -from parsons.solidarity_tech.solidarity_tech_event_attendances import SolidarityTechEventAttendances -from parsons.solidarity_tech.solidarity_tech_event_rsvps import SolidarityTechEventRSVPs -from parsons.solidarity_tech.solidarity_tech_event_sessions import SolidarityTechEventSessions -from parsons.solidarity_tech.solidarity_tech_events import SolidarityTechEvents -from parsons.solidarity_tech.solidarity_tech_field_survey_urls import SolidarityTechFieldSurveyURLs +from parsons.solidarity_tech.st_activities import SolidarityTechActivities +from parsons.solidarity_tech.st_agent_assignments import SolidarityTechAgentAssignments +from parsons.solidarity_tech.st_automation_enrollments import SolidarityTechAutomationEnrollments +from parsons.solidarity_tech.st_calls import SolidarityTechCalls +from parsons.solidarity_tech.st_chapter_phone_numbers import SolidarityTechChapterPhoneNumbers +from parsons.solidarity_tech.st_chapters import SolidarityTechChapters +from parsons.solidarity_tech.st_custom_user_properties import SolidarityTechCustomUserProperties +from parsons.solidarity_tech.st_donation_charges import SolidarityTechDonationCharges +from parsons.solidarity_tech.st_email_blasts import SolidarityTechEmailBlasts +from parsons.solidarity_tech.st_email_senders import SolidarityTechEmailSenders +from parsons.solidarity_tech.st_emails import SolidarityTechEmails +from parsons.solidarity_tech.st_event_attendances import SolidarityTechEventAttendances +from parsons.solidarity_tech.st_event_rsvps import SolidarityTechEventRSVPs +from parsons.solidarity_tech.st_event_sessions import SolidarityTechEventSessions +from parsons.solidarity_tech.st_events import SolidarityTechEvents +from parsons.solidarity_tech.st_field_survey_urls import SolidarityTechFieldSurveyURLs logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index 5b6ea55e43..9367030b16 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -64,7 +64,10 @@ def _get_single_resource(self, endpoint: str, id: int, **kwargs) -> requests.Res return self.api.request(url=complete_endpoint, req_type="GET", **kwargs) def _post_request( - self, endpoint: str, payload: Mapping[str, str | int | np.int64] | None = None, **kwargs + self, + endpoint: str, + payload: Mapping[str, str | int | np.int64 | float] | None = None, + **kwargs, ) -> requests.Response: """Handle POST requests.""" logger.debug("Processing POST request at endpoint: %s", endpoint, extra=payload) diff --git a/parsons/solidarity_tech/solidarity_tech_activities.py b/parsons/solidarity_tech/st_activities.py similarity index 95% rename from parsons/solidarity_tech/solidarity_tech_activities.py rename to parsons/solidarity_tech/st_activities.py index d7aa35e39d..c25647bb12 100644 --- a/parsons/solidarity_tech/solidarity_tech_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -1,8 +1,8 @@ import logging from datetime import datetime +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py similarity index 98% rename from parsons/solidarity_tech/solidarity_tech_agent_assignments.py rename to parsons/solidarity_tech/st_agent_assignments.py index c200ce4f9f..57e483abd6 100644 --- a/parsons/solidarity_tech/solidarity_tech_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -3,8 +3,8 @@ import numpy as np +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_automation_enrollments.py b/parsons/solidarity_tech/st_automation_enrollments.py similarity index 94% rename from parsons/solidarity_tech/solidarity_tech_automation_enrollments.py rename to parsons/solidarity_tech/st_automation_enrollments.py index 8c283d5302..305c88aba2 100644 --- a/parsons/solidarity_tech/solidarity_tech_automation_enrollments.py +++ b/parsons/solidarity_tech/st_automation_enrollments.py @@ -2,8 +2,8 @@ from requests.exceptions import HTTPError +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_calls.py b/parsons/solidarity_tech/st_calls.py similarity index 94% rename from parsons/solidarity_tech/solidarity_tech_calls.py rename to parsons/solidarity_tech/st_calls.py index 4da98b9acd..43aee9941d 100644 --- a/parsons/solidarity_tech/solidarity_tech_calls.py +++ b/parsons/solidarity_tech/st_calls.py @@ -1,8 +1,8 @@ import logging from datetime import datetime +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_chapter_phone_numbers.py b/parsons/solidarity_tech/st_chapter_phone_numbers.py similarity index 94% rename from parsons/solidarity_tech/solidarity_tech_chapter_phone_numbers.py rename to parsons/solidarity_tech/st_chapter_phone_numbers.py index dcdfccfa0f..14b9e1f8fd 100644 --- a/parsons/solidarity_tech/solidarity_tech_chapter_phone_numbers.py +++ b/parsons/solidarity_tech/st_chapter_phone_numbers.py @@ -1,8 +1,8 @@ import logging from datetime import datetime +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_chapters.py b/parsons/solidarity_tech/st_chapters.py similarity index 93% rename from parsons/solidarity_tech/solidarity_tech_chapters.py rename to parsons/solidarity_tech/st_chapters.py index e71f8310e0..802ac38c27 100644 --- a/parsons/solidarity_tech/solidarity_tech_chapters.py +++ b/parsons/solidarity_tech/st_chapters.py @@ -1,8 +1,8 @@ import logging from datetime import datetime +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py similarity index 98% rename from parsons/solidarity_tech/solidarity_tech_custom_user_properties.py rename to parsons/solidarity_tech/st_custom_user_properties.py index 67cdab34a0..61d643f8de 100644 --- a/parsons/solidarity_tech/solidarity_tech_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -4,8 +4,8 @@ from requests.exceptions import HTTPError +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_donation_charges.py b/parsons/solidarity_tech/st_donation_charges.py similarity index 95% rename from parsons/solidarity_tech/solidarity_tech_donation_charges.py rename to parsons/solidarity_tech/st_donation_charges.py index 90a3af59db..a3dd280935 100644 --- a/parsons/solidarity_tech/solidarity_tech_donation_charges.py +++ b/parsons/solidarity_tech/st_donation_charges.py @@ -3,8 +3,8 @@ from requests.exceptions import HTTPError +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_email_blasts.py b/parsons/solidarity_tech/st_email_blasts.py similarity index 95% rename from parsons/solidarity_tech/solidarity_tech_email_blasts.py rename to parsons/solidarity_tech/st_email_blasts.py index e72989cc0a..219aace58c 100644 --- a/parsons/solidarity_tech/solidarity_tech_email_blasts.py +++ b/parsons/solidarity_tech/st_email_blasts.py @@ -3,8 +3,8 @@ from requests.exceptions import HTTPError +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_email_senders.py b/parsons/solidarity_tech/st_email_senders.py similarity index 92% rename from parsons/solidarity_tech/solidarity_tech_email_senders.py rename to parsons/solidarity_tech/st_email_senders.py index fefcae4f81..2c1e529f14 100644 --- a/parsons/solidarity_tech/solidarity_tech_email_senders.py +++ b/parsons/solidarity_tech/st_email_senders.py @@ -1,7 +1,7 @@ import logging +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_emails.py b/parsons/solidarity_tech/st_emails.py similarity index 96% rename from parsons/solidarity_tech/solidarity_tech_emails.py rename to parsons/solidarity_tech/st_emails.py index 243ffd01b6..d822052a52 100644 --- a/parsons/solidarity_tech/solidarity_tech_emails.py +++ b/parsons/solidarity_tech/st_emails.py @@ -2,8 +2,8 @@ from requests.exceptions import HTTPError +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py similarity index 97% rename from parsons/solidarity_tech/solidarity_tech_event_attendances.py rename to parsons/solidarity_tech/st_event_attendances.py index a1d4306bb7..679ee7d833 100644 --- a/parsons/solidarity_tech/solidarity_tech_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -3,8 +3,8 @@ import numpy as np +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py similarity index 98% rename from parsons/solidarity_tech/solidarity_tech_event_rsvps.py rename to parsons/solidarity_tech/st_event_rsvps.py index 7fbe21dc1e..2fe78f75ed 100644 --- a/parsons/solidarity_tech/solidarity_tech_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -4,8 +4,8 @@ import numpy as np +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py similarity index 99% rename from parsons/solidarity_tech/solidarity_tech_event_sessions.py rename to parsons/solidarity_tech/st_event_sessions.py index 0a38215180..d822b7e1a6 100644 --- a/parsons/solidarity_tech/solidarity_tech_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -5,8 +5,8 @@ import numpy as np from requests.exceptions import HTTPError +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_events.py b/parsons/solidarity_tech/st_events.py similarity index 98% rename from parsons/solidarity_tech/solidarity_tech_events.py rename to parsons/solidarity_tech/st_events.py index 3164bbb02d..e7dae654b4 100644 --- a/parsons/solidarity_tech/solidarity_tech_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -5,8 +5,8 @@ import numpy as np from requests.exceptions import HTTPError +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/solidarity_tech_field_survey_urls.py b/parsons/solidarity_tech/st_field_survey_urls.py similarity index 95% rename from parsons/solidarity_tech/solidarity_tech_field_survey_urls.py rename to parsons/solidarity_tech/st_field_survey_urls.py index c25075ab8f..2cf32ece57 100644 --- a/parsons/solidarity_tech/solidarity_tech_field_survey_urls.py +++ b/parsons/solidarity_tech/st_field_survey_urls.py @@ -2,8 +2,8 @@ from requests.exceptions import HTTPError +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_exceptions import STUnexpectedResponseCodeError logger = logging.getLogger(__name__) From bd3bd9c957d8e6bdd2ff3268a1b3d6e33d87cc21 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:22:16 -0400 Subject: [PATCH 21/94] fix survey url documentation link --- parsons/solidarity_tech/st_field_survey_urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsons/solidarity_tech/st_field_survey_urls.py b/parsons/solidarity_tech/st_field_survey_urls.py index 2cf32ece57..36c8865edb 100644 --- a/parsons/solidarity_tech/st_field_survey_urls.py +++ b/parsons/solidarity_tech/st_field_survey_urls.py @@ -39,7 +39,7 @@ def generate_field_survey_url( STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. Documentation Reference: - ``__ + ``__ """ payload = { From 206f5bbecc4b67abaf2f8cd9f684437b38ff4ef6 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:28:50 -0400 Subject: [PATCH 22/94] add organizations methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + parsons/solidarity_tech/st_organizations.py | 80 +++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 parsons/solidarity_tech/st_organizations.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 7e278e3ba5..8421fad100 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -16,6 +16,7 @@ from parsons.solidarity_tech.st_event_sessions import SolidarityTechEventSessions from parsons.solidarity_tech.st_events import SolidarityTechEvents from parsons.solidarity_tech.st_field_survey_urls import SolidarityTechFieldSurveyURLs +from parsons.solidarity_tech.st_organizations import SolidarityTechOrganizations logger = logging.getLogger(__name__) @@ -37,5 +38,6 @@ class SolidarityTech( SolidarityTechEventSessions, SolidarityTechEvents, SolidarityTechFieldSurveyURLs, + SolidarityTechOrganizations, ): pass diff --git a/parsons/solidarity_tech/st_organizations.py b/parsons/solidarity_tech/st_organizations.py new file mode 100644 index 0000000000..005237ecf6 --- /dev/null +++ b/parsons/solidarity_tech/st_organizations.py @@ -0,0 +1,80 @@ +import logging +from datetime import datetime + +from requests import HTTPError + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechOrganizations(SolidarityTechBase): + def get_organizations( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + ) -> str: + """ + Retrieve a list of organizations. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + + Returns: + All the organizations. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "organizations", + limit=limit, + offset=offset, + since=since, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_organization( + self, + id: int, + ) -> str: + """ + Retrieve a single organization. + + Args: + id: + ID of the organization to retrieve. + + Returns: + A single organization entry. + + Raises: + HTTPError: If the organization is not found. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("organizations", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 404: + raise HTTPError("Organization not found.", response=res) + + return res.text From e999c89ae27ea5fb43cc1d4a2da684f3aa310904 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:29:06 -0400 Subject: [PATCH 23/94] improve parameter handling with error message --- parsons/solidarity_tech/solidarity_tech_base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index 9367030b16..726110ffdc 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -40,7 +40,7 @@ def _get_resources(self, endpoint: str, limit: int, **kwargs) -> requests.Respon "since": "_since", "include_count": "_include_count", } - params = {"_limit": limit} + params = {} for key, value in kwargs.items(): if value is None: continue @@ -52,6 +52,9 @@ def _get_resources(self, endpoint: str, limit: int, **kwargs) -> requests.Respon for key, value in kwargs.get("params", {}).items(): if value is None: continue + if key in params: + err_msg = f"Request param '{key}' already exists." + raise KeyError(err_msg) params[key] = value logger.debug("Processing GET request at endpoint: %s", endpoint, extra=params) From f74616f203896d2fc1023499b9a47f625e7b1aaa Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:29:26 -0400 Subject: [PATCH 24/94] include response with HTTPErrors --- parsons/solidarity_tech/st_automation_enrollments.py | 2 +- parsons/solidarity_tech/st_custom_user_properties.py | 10 +++++----- parsons/solidarity_tech/st_donation_charges.py | 2 +- parsons/solidarity_tech/st_email_blasts.py | 2 +- parsons/solidarity_tech/st_emails.py | 4 ++-- parsons/solidarity_tech/st_event_sessions.py | 4 ++-- parsons/solidarity_tech/st_events.py | 6 +++--- parsons/solidarity_tech/st_field_survey_urls.py | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/parsons/solidarity_tech/st_automation_enrollments.py b/parsons/solidarity_tech/st_automation_enrollments.py index 305c88aba2..20a898cf38 100644 --- a/parsons/solidarity_tech/st_automation_enrollments.py +++ b/parsons/solidarity_tech/st_automation_enrollments.py @@ -46,6 +46,6 @@ def enroll_user_in_automation( raise STUnexpectedResponseCodeError(res) if res.status_code == 422: - raise HTTPError("Automation is inactive") + raise HTTPError("Automation is inactive", response=res) return res.status_code == 201 diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index 61d643f8de..1c270b1d20 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -115,7 +115,7 @@ def create_custom_user_property( raise STUnexpectedResponseCodeError(res) if res.status_code == 422: - raise HTTPError("Validation failed") + raise HTTPError("Validation failed", response=res) return res.status_code == 201 @@ -155,10 +155,10 @@ def delete_custom_user_property_option( raise STUnexpectedResponseCodeError(res) if res.status_code == 404: - raise HTTPError("Option or custom user property not found") + raise HTTPError("Option or custom user property not found", response=res) if res.status_code == 422: - raise HTTPError("Validation failed") + raise HTTPError("Validation failed", response=res) return res.status_code == 200 @@ -206,9 +206,9 @@ def create_custom_user_property_option( raise STUnexpectedResponseCodeError(res) if res.status_code == 404: - raise HTTPError("Custom user property not found") + raise HTTPError("Custom user property not found", response=res) if res.status_code == 422: - raise HTTPError("Validation failed") + raise HTTPError("Validation failed", response=res) return res.status_code == 201 diff --git a/parsons/solidarity_tech/st_donation_charges.py b/parsons/solidarity_tech/st_donation_charges.py index a3dd280935..0767c65319 100644 --- a/parsons/solidarity_tech/st_donation_charges.py +++ b/parsons/solidarity_tech/st_donation_charges.py @@ -72,7 +72,7 @@ def get_donation_charge( res = self._get_single_resource("donation_charges", id) if res.status_code == 404: - raise HTTPError("Donation charge not found") + raise HTTPError("Donation charge not found", response=res) if res.status_code: raise STUnexpectedResponseCodeError(res) diff --git a/parsons/solidarity_tech/st_email_blasts.py b/parsons/solidarity_tech/st_email_blasts.py index 219aace58c..ee53afae7c 100644 --- a/parsons/solidarity_tech/st_email_blasts.py +++ b/parsons/solidarity_tech/st_email_blasts.py @@ -74,6 +74,6 @@ def get_email_blast( raise STUnexpectedResponseCodeError(res) if res.status_code == 404: - raise HTTPError("Email blast not found") + raise HTTPError("Email blast not found", response=res) return res.text diff --git a/parsons/solidarity_tech/st_emails.py b/parsons/solidarity_tech/st_emails.py index d822052a52..7cdd110e27 100644 --- a/parsons/solidarity_tech/st_emails.py +++ b/parsons/solidarity_tech/st_emails.py @@ -75,9 +75,9 @@ def send_one_off_email( raise STUnexpectedResponseCodeError(res) if res.status_code == 404: - raise HTTPError("User not found") + raise HTTPError("User not found", response=res) if res.status_code == 422: - raise HTTPError("Missing required parameters") + raise HTTPError("Missing required parameters", response=res) return res.text diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index d822b7e1a6..39eba5ad77 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -187,7 +187,7 @@ def create_event_sessions( raise STUnexpectedResponseCodeError(res) if res.status_code == 422: - raise HTTPError("Unprocessable entity") + raise HTTPError("Unprocessable entity", response=res) return res.status_code == 201 @@ -308,7 +308,7 @@ def update_event_session( raise STUnexpectedResponseCodeError(res) if res.status_code == 422: - raise HTTPError("Unprocessable entity") + raise HTTPError("Unprocessable entity", response=res) return res.status_code == 200 diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index e7dae654b4..a31cc09cb7 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -164,13 +164,13 @@ def create_event( raise STUnexpectedResponseCodeError(res) if res.status_code == 404: - raise HTTPError("Event scope not found") + raise HTTPError("Event scope not found", response=res) if res.status_code == 409: - raise HTTPError("Duplicate event detected") + raise HTTPError("Duplicate event detected", response=res) if res.status_code == 422: - raise HTTPError("Data validation error") + raise HTTPError("Data validation error", response=res) return res.status_code == 201 diff --git a/parsons/solidarity_tech/st_field_survey_urls.py b/parsons/solidarity_tech/st_field_survey_urls.py index 36c8865edb..281089f1fd 100644 --- a/parsons/solidarity_tech/st_field_survey_urls.py +++ b/parsons/solidarity_tech/st_field_survey_urls.py @@ -57,9 +57,9 @@ def generate_field_survey_url( raise STUnexpectedResponseCodeError(res) if res.status_code == 404: - raise HTTPError("User, agent, or page not found") + raise HTTPError("User, agent, or page not found", response=res) if res.status_code == 422: - raise HTTPError("Required parameters are missing") + raise HTTPError("Required parameters are missing", response=res) return res.status_code == 200 From 4c9bdcad26a0e5d5eb39facb32b9123b5d491c14 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:34:52 -0400 Subject: [PATCH 25/94] add pages methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + parsons/solidarity_tech/st_pages.py | 92 ++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 parsons/solidarity_tech/st_pages.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 8421fad100..01cf80d4e4 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -17,6 +17,7 @@ from parsons.solidarity_tech.st_events import SolidarityTechEvents from parsons.solidarity_tech.st_field_survey_urls import SolidarityTechFieldSurveyURLs from parsons.solidarity_tech.st_organizations import SolidarityTechOrganizations +from parsons.solidarity_tech.st_pages import SolidarityTechPages logger = logging.getLogger(__name__) @@ -39,5 +40,6 @@ class SolidarityTech( SolidarityTechEvents, SolidarityTechFieldSurveyURLs, SolidarityTechOrganizations, + SolidarityTechPages, ): pass diff --git a/parsons/solidarity_tech/st_pages.py b/parsons/solidarity_tech/st_pages.py new file mode 100644 index 0000000000..d89a756de9 --- /dev/null +++ b/parsons/solidarity_tech/st_pages.py @@ -0,0 +1,92 @@ +import logging +from datetime import datetime + +from requests import HTTPError + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechPages(SolidarityTechBase): + def get_pages( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + include_action_counts: bool = False, + ) -> str: + """ + Retrieve a list of pages. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + include_action_counts: + If True, each page includes ``action_count`` (total submissions) and ``action_goal`` + (the next milestone the public progress bar would display for that count). + Default is False. + + Returns: + All the pages. + + Documentation Reference: + ``__ + + """ + params = {"include_action_counts": include_action_counts} + res = self._get_resources( + "pages", + limit=limit, + offset=offset, + since=since, + params=params, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_page( + self, + id: int, + include_action_counts: bool = False, + ) -> str: + """ + Retrieve a single page. + + Args: + id: + ID of the page to retrieve. + include_action_counts: + If True, the page includes ``action_count`` (total submissions) and ``action_goal`` + (the next milestone the public progress bar would display for that count). + Default is False. + + Returns: + A single page entry. + + Raises: + HTTPError: If the page is not found. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("pages", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 404: + raise HTTPError("page not found.", response=res) + + return res.text From 04eb6506b4a9a06dae77c429dd828d76b8014363 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:39:31 -0400 Subject: [PATCH 26/94] add phonebank methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + parsons/solidarity_tech/st_phonebanks.py | 94 ++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 parsons/solidarity_tech/st_phonebanks.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 01cf80d4e4..341af3aec5 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -18,6 +18,7 @@ from parsons.solidarity_tech.st_field_survey_urls import SolidarityTechFieldSurveyURLs from parsons.solidarity_tech.st_organizations import SolidarityTechOrganizations from parsons.solidarity_tech.st_pages import SolidarityTechPages +from parsons.solidarity_tech.st_phonebanks import SolidarityTechPhonebanks logger = logging.getLogger(__name__) @@ -41,5 +42,6 @@ class SolidarityTech( SolidarityTechFieldSurveyURLs, SolidarityTechOrganizations, SolidarityTechPages, + SolidarityTechPhonebanks, ): pass diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py new file mode 100644 index 0000000000..e6b4b60444 --- /dev/null +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -0,0 +1,94 @@ +import logging +from datetime import datetime + +from requests import HTTPError + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechPhonebanks(SolidarityTechBase): + def get_phonebanks( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + event_id: int = 0, + ids: list[int] | None = None, + include_stats: bool = False, + ) -> str: + """ + Retrieve a list of phonebanks. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + event_id: + Filters phonebanks by event_id within the accessible scope. + ids: + Filters to specific phonebank ids. Accepts a comma-separated string (e.g. "12,34"). + include_stats: + If True, each phonebank row also includes aggregate funnel numbers + ``attempts`` (contact attempts), ``contacted`` (distinct people attempted), + and ``reached`` (distinct people on answered calls). + Default is False. + + Returns: + All the phonebanks. + + Documentation Reference: + ``__ + + """ + params = {"event_id": event_id, "ids": ids, "include_stats": include_stats} + res = self._get_resources( + "phonebanks", + limit=limit, + offset=offset, + since=since, + params=params, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_phonebank( + self, + id: int, + ) -> str: + """ + Retrieve a single phonebank. + + Args: + id: + ID of the phonebank to retrieve. + + Returns: + A single phonebank entry. + + Raises: + HTTPError: If the phonebank is not found. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("phonebanks", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 404: + raise HTTPError("phonebank not found.", response=res) + + return res.text From 039c23fb0d38f51e983a08b8034870aee7597e56 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:49:26 -0400 Subject: [PATCH 27/94] move extra params to params arg of _get_resources --- parsons/solidarity_tech/st_activities.py | 3 ++- parsons/solidarity_tech/st_agent_assignments.py | 4 ++-- parsons/solidarity_tech/st_calls.py | 3 ++- parsons/solidarity_tech/st_chapter_phone_numbers.py | 3 ++- parsons/solidarity_tech/st_custom_user_properties.py | 4 ++-- parsons/solidarity_tech/st_event_attendances.py | 4 ++-- parsons/solidarity_tech/st_event_rsvps.py | 11 +++++++---- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/parsons/solidarity_tech/st_activities.py b/parsons/solidarity_tech/st_activities.py index c25647bb12..e2cc50ae4e 100644 --- a/parsons/solidarity_tech/st_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -47,13 +47,14 @@ def get_activities( ``__ """ + params = {"user_id": user_id} res = self._get_resources( "activities", limit=limit, cursor=cursor, since=since, include_count=include_count or None, - user_id=user_id, + params=params, additional_headers={"accept": "application/json"}, ) diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index 57e483abd6..5827342064 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -41,13 +41,13 @@ def get_agent_assignments( ``__ """ + params = {"user_id": user_id, "agent_user_id": agent_user_id} res = self._get_resources( "agent_assignments", limit=limit, offset=offset, since=since, - user_id=user_id, - agent_user_id=agent_user_id, + params=params, ) if res.status_code not in (200, 404): diff --git a/parsons/solidarity_tech/st_calls.py b/parsons/solidarity_tech/st_calls.py index 43aee9941d..53efff096e 100644 --- a/parsons/solidarity_tech/st_calls.py +++ b/parsons/solidarity_tech/st_calls.py @@ -39,12 +39,13 @@ def get_calls( ``__ """ + params = {"user_id": user_id} res = self._get_resources( "calls", - user_id=user_id, limit=limit, offset=offset, since=since, + params=params, additional_headers={"accept": "application/json"}, ) diff --git a/parsons/solidarity_tech/st_chapter_phone_numbers.py b/parsons/solidarity_tech/st_chapter_phone_numbers.py index 14b9e1f8fd..5db8e49157 100644 --- a/parsons/solidarity_tech/st_chapter_phone_numbers.py +++ b/parsons/solidarity_tech/st_chapter_phone_numbers.py @@ -39,12 +39,13 @@ def get_chapter_phone_numbers( ``__ """ + params = {"chapter_id": chapter_id} res = self._get_resources( "chapter_phone_numbers", limit=limit, offset=offset, since=since, - chapter_id=chapter_id, + params=params, ) if res.status_code != 200: diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index 1c270b1d20..07cac3c4dd 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -41,13 +41,13 @@ def get_custom_user_properties( ``__ """ + params = {"scope_id": scope_id, "scope_type": scope_type} res = self._get_resources( "custom_user_properties", limit=limit, offset=offset, since=since, - scope_id=scope_id, - scope_type=scope_type, + params=params, additional_headers={"accept": "application/json"}, ) diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index 679ee7d833..c0b7979c14 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -41,13 +41,13 @@ def get_event_attendances( ``__ """ + params = {"event_id": event_id, "session_id": session_id} res = self._get_resources( "event_attendances", limit=limit, offset=offset, since=since, - event_id=event_id, - session_id=session_id, + params=params, ) if res.status_code != 200: diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index 2fe78f75ed..8f1f74f30d 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -48,15 +48,18 @@ def get_event_rsvps( ``__ """ + params = { + "event_id": event_id, + "session_id": session_id, + "user_id": user_id, + "full_user_payload": full_user_payload, + } res = self._get_resources( "event_rsvps", limit=limit, offset=offset, since=since, - event_id=event_id, - session_id=session_id, - user_id=user_id, - full_user_payload=full_user_payload, + params=params, ) if res.status_code != 200: From 964f266595414713842c9cc6a60dfeaade195328 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:58:56 -0400 Subject: [PATCH 28/94] improved params combining in base class --- parsons/solidarity_tech/solidarity_tech_base.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index 726110ffdc..fb567136d2 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -41,20 +41,18 @@ def _get_resources(self, endpoint: str, limit: int, **kwargs) -> requests.Respon "include_count": "_include_count", } params = {} - for key, value in kwargs.items(): - if value is None: - continue - query_key = param_mapping.get(key, key) - params[query_key] = value - del kwargs[key] + for key, value in param_mapping.items(): + if key in kwargs: + params[key] = value + del kwargs[key] - if kwargs.get("params"): + if "params" in kwargs: for key, value in kwargs.get("params", {}).items(): - if value is None: - continue if key in params: err_msg = f"Request param '{key}' already exists." raise KeyError(err_msg) + if value is None: + continue params[key] = value logger.debug("Processing GET request at endpoint: %s", endpoint, extra=params) From 8610a04cf81a9c1c3028d567f63e54c713ef1f96 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:59:06 -0400 Subject: [PATCH 29/94] fix capitalization in phonebanks --- parsons/solidarity_tech/st_phonebanks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py index e6b4b60444..7e23c7cdc1 100644 --- a/parsons/solidarity_tech/st_phonebanks.py +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -89,6 +89,6 @@ def get_phonebank( raise STUnexpectedResponseCodeError(res) if res.status_code == 404: - raise HTTPError("phonebank not found.", response=res) + raise HTTPError("Phonebank not found.", response=res) return res.text From 2fb6dc8e02b062106c296b4a5d714edf41a8a652 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 12:59:37 -0400 Subject: [PATCH 30/94] add scheduled calls methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + parsons/solidarity_tech/st_scheduled_calls.py | 88 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 parsons/solidarity_tech/st_scheduled_calls.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 341af3aec5..63d946420e 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -19,6 +19,7 @@ from parsons.solidarity_tech.st_organizations import SolidarityTechOrganizations from parsons.solidarity_tech.st_pages import SolidarityTechPages from parsons.solidarity_tech.st_phonebanks import SolidarityTechPhonebanks +from parsons.solidarity_tech.st_scheduled_calls import SolidarityTechScheduledCalls logger = logging.getLogger(__name__) @@ -43,5 +44,6 @@ class SolidarityTech( SolidarityTechOrganizations, SolidarityTechPages, SolidarityTechPhonebanks, + SolidarityTechScheduledCalls, ): pass diff --git a/parsons/solidarity_tech/st_scheduled_calls.py b/parsons/solidarity_tech/st_scheduled_calls.py new file mode 100644 index 0000000000..b1ebf4c6fa --- /dev/null +++ b/parsons/solidarity_tech/st_scheduled_calls.py @@ -0,0 +1,88 @@ +import logging +from datetime import datetime + +from requests import HTTPError + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechScheduledCalls(SolidarityTechBase): + def get_scheduled_calls( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + user_id: int | None = None, + agent_user_id: int | None = None, + ) -> str: + """ + Retrieve a list of scheduled calls. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + user_id: + User ID to filter scheduled calls related to a specific user. + agent_user_id: + Agent User ID to filter agent user assignments related to a specific agent user. + + Returns: + All the scheduled calls. + + Documentation Reference: + ``__ + + """ + params = {"user_id": user_id, "agent_user_id": agent_user_id} + res = self._get_resources( + "scheduled_calls", + limit=limit, + offset=offset, + since=since, + params=params, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_scheduled_call( + self, + id: int, + ) -> str: + """ + Retrieve a single scheduled call. + + Args: + id: + ID of the scheduled call to retrieve. + + Returns: + A single scheduled call entry. + + Raises: + HTTPError: If the scheduled call is not found. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("scheduled_calls", id) + + if res and res.status_code != 404: + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 404: + raise HTTPError("Scheduled call not found.", response=res) + + return res.text From e7e6178c58f10e62f1f3dd37c7c4d8868f348488 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 13:19:27 -0400 Subject: [PATCH 31/94] add scheduled tasks methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + .../solidarity_tech/st_agent_assignments.py | 2 +- .../solidarity_tech/st_event_attendances.py | 2 +- parsons/solidarity_tech/st_event_rsvps.py | 2 +- parsons/solidarity_tech/st_event_sessions.py | 2 +- parsons/solidarity_tech/st_scheduled_tasks.py | 234 ++++++++++++++++++ 6 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 parsons/solidarity_tech/st_scheduled_tasks.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 63d946420e..7bab0f40b6 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -20,6 +20,7 @@ from parsons.solidarity_tech.st_pages import SolidarityTechPages from parsons.solidarity_tech.st_phonebanks import SolidarityTechPhonebanks from parsons.solidarity_tech.st_scheduled_calls import SolidarityTechScheduledCalls +from parsons.solidarity_tech.st_scheduled_tasks import SolidarityTechScheduledTasks logger = logging.getLogger(__name__) @@ -45,5 +46,6 @@ class SolidarityTech( SolidarityTechPages, SolidarityTechPhonebanks, SolidarityTechScheduledCalls, + SolidarityTechScheduledTasks, ): pass diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index 5827342064..d06021d707 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -170,7 +170,7 @@ def delete_agent_assignment( id: int, ) -> bool: """ - Update an agent assignment with specified details. + Delete an agent assignment with specified ID. Args: id: diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index c0b7979c14..a059908279 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -106,7 +106,7 @@ def delete_event_attendance( id: str, ) -> bool: """ - Deletes an event attendance with the specified ID. + Delete an event attendance with the specified ID. Args: id: diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index 8f1f74f30d..52dc01e3c7 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -227,7 +227,7 @@ def delete_event_rsvp( id: str, ) -> bool: """ - Deletes an event rsvp with the specified ID. + Delete an event rsvp with the specified ID. Args: id: diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index 39eba5ad77..a47b83b69e 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -317,7 +317,7 @@ def delete_event_session( id: str, ) -> bool: """ - Deletes an event session with the specified ID. + Delete an event session with the specified ID. Args: id: diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py new file mode 100644 index 0000000000..79e3f1f64f --- /dev/null +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -0,0 +1,234 @@ +import logging +from datetime import datetime + +import numpy as np + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechScheduledTasks(SolidarityTechBase): + def get_scheduled_tasks( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + user_id: int | None = None, + agent_user_id: int | None = None, + ) -> str: + """ + Retrieve a list of scheduled tasks. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + user_id: + User ID to filter scheduled tasks related to a specific user. + agent_user_id: + Agent User ID to filter agent user assignments related to a specific agent user. + + Returns: + All the scheduled task entries. + + Documentation Reference: + ``__ + + """ + params = {"user_id": user_id, "agent_user_id": agent_user_id} + res = self._get_resources( + "scheduled_tasks", + limit=limit, + offset=offset, + since=since, + params=params, + ) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_scheduled_task( + self, + id: int, + ) -> str: + """ + Retrieve a single scheduled task. + + Args: + id: + ID of the scheduled task to retrieve. + + Returns: + A single scheduled task entry. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("scheduled_tasks", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_scheduled_task( + self, + due_at: str | int | datetime, + remind_at: str | int | datetime | None = None, + agent_user_id: np.int64 | None = None, + user_id: np.int64 | None = None, + notes: str | None = None, + marked_as_completed: bool | None = None, + ) -> bool: + """ + Create an scheduled task with specified details. + + Args: + due_at: + The date and time when the task is due. + Accepts either an ISO 8601 formatted date-time string + or a UNIX timestamp as a string or integer. + remind_at: + The date and time when a reminder for the task should be sent. + Accepts either an ISO 8601 formatted date-time string + or a UNIX timestamp as a string or integer. + agent_user_id: + Identifier for the agent user assigned to the task. + user_id: + Identifier for the user who created the task. + ``notes``: + Additional notes or details about the task. + marked_as_completed: + Indicates if the task has been marked as completed. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "due_at": due_at.timestamp() if isinstance(due_at, datetime) else due_at, + "remind_at": remind_at.timestamp() if isinstance(remind_at, datetime) else remind_at, + "agent_user_id": agent_user_id, + "user_id": user_id, + "notes": notes, + "marked_as_completed": marked_as_completed, + } + res = self._post_request( + "scheduled_tasks", payload, additional_headers={"content-type": "application/json"} + ) + + if res.status_code not in (201, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 201 + + def update_scheduled_task( + self, + id: int, + due_at: str | int | datetime | None = None, + remind_at: str | int | datetime | None = None, + agent_user_id: np.int64 | None = None, + user_id: np.int64 | None = None, + notes: str | None = None, + marked_as_completed: bool | None = None, + ) -> bool: + """ + Update a scheduled task with specified details. + + Args: + id: + Identifier for the scheduled task to update. + due_at: + The date and time when the task is due. + Accepts either an ISO 8601 formatted date-time string + or a UNIX timestamp as a string or integer. + remind_at: + Reminder time for the task. + agent_user_id: + Identifier for the agent user. + user_id: + Identifier for the user. + ``notes``: + Additional notes or details about the task. + marked_as_completed: + Indicates if the task has been marked as completed. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: If the update could not be processed. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "due_at": due_at.timestamp() if isinstance(due_at, datetime) else due_at, + "remind_at": remind_at.timestamp() if isinstance(remind_at, datetime) else remind_at, + "agent_user_id": agent_user_id, + "user_id": user_id, + "notes": notes, + "marked_as_completed": marked_as_completed, + } + res = self._put_request( + "scheduled_tasks", + id, + payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code not in (200, 404, 422): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 + + def delete_scheduled_task( + self, + id: int, + ) -> bool: + """ + Delete a scheduled task with the specified ID. + + Args: + id: + Identifier for the scheduled task to delete. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._del_request("scheduled_tasks", id) + + if res and res.status_code != 404: + raise STUnexpectedResponseCodeError(res) + + return not res.status_code From efe54528648f01f84379787bf9f915844ae9ac03 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 13:26:36 -0400 Subject: [PATCH 32/94] add task agents methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + parsons/solidarity_tech/st_task_agents.py | 148 +++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 parsons/solidarity_tech/st_task_agents.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 7bab0f40b6..c8e8be7ccb 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -21,6 +21,7 @@ from parsons.solidarity_tech.st_phonebanks import SolidarityTechPhonebanks from parsons.solidarity_tech.st_scheduled_calls import SolidarityTechScheduledCalls from parsons.solidarity_tech.st_scheduled_tasks import SolidarityTechScheduledTasks +from parsons.solidarity_tech.st_task_agents import SolidarityTechTaskAgents logger = logging.getLogger(__name__) @@ -47,5 +48,6 @@ class SolidarityTech( SolidarityTechPhonebanks, SolidarityTechScheduledCalls, SolidarityTechScheduledTasks, + SolidarityTechTaskAgents, ): pass diff --git a/parsons/solidarity_tech/st_task_agents.py b/parsons/solidarity_tech/st_task_agents.py new file mode 100644 index 0000000000..a10a7e35da --- /dev/null +++ b/parsons/solidarity_tech/st_task_agents.py @@ -0,0 +1,148 @@ +import logging +from datetime import datetime + +import numpy as np + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechTaskAgents(SolidarityTechBase): + def get_task_agents( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime | None = 0, + task_id: int | None = None, + ) -> str: + """ + Retrieve a list of task agents. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + task_id: + Filters task agents by task within the accessible scope. + + Returns: + All the task agent entries. + + Documentation Reference: + ``__ + + """ + params = {"task_id": task_id} + res = self._get_resources( + "task_agents", + limit=limit, + offset=offset, + since=since, + params=params, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_task_agent( + self, + id: int, + ) -> str: + """ + Retrieve a single task agent. + + Args: + id: + ID of the task agent to retrieve. + + Returns: + A single task agent entry. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("task_agents", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_task_agent( + self, + user_id: np.int64, + task_id: np.int64, + ) -> bool: + """ + Create an task agent with specified details. + + Args: + user_id: + Identifier for the task agent. + task_id: + Identifier for the task. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "user_id": user_id, + "task_id": task_id, + } + res = self._post_request( + "task_agents", payload, additional_headers={"content-type": "application/json"} + ) + + if res.status_code not in (201, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 201 + + def delete_task_agent( + self, + id: int, + ) -> bool: + """ + Delete a task agent with the specified ID. + + Args: + id: + Identifier for the task agent to delete. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._del_request("task_agents", id) + + if res and res.status_code != 404: + raise STUnexpectedResponseCodeError(res) + + return not res.status_code From 4d1cea514419881c45efdecc6af3c98360b4f968 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 13:36:47 -0400 Subject: [PATCH 33/94] fix scheduled calls urls --- parsons/solidarity_tech/st_scheduled_calls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parsons/solidarity_tech/st_scheduled_calls.py b/parsons/solidarity_tech/st_scheduled_calls.py index b1ebf4c6fa..dc9a64469a 100644 --- a/parsons/solidarity_tech/st_scheduled_calls.py +++ b/parsons/solidarity_tech/st_scheduled_calls.py @@ -38,7 +38,7 @@ def get_scheduled_calls( All the scheduled calls. Documentation Reference: - ``__ + ``__ """ params = {"user_id": user_id, "agent_user_id": agent_user_id} @@ -74,7 +74,7 @@ def get_scheduled_call( STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. Documentation Reference: - ``__ + ``__ """ res = self._get_single_resource("scheduled_calls", id) From 4d17ba7a388ae90ff1ac089f5890589fe76bb6c6 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 13:37:13 -0400 Subject: [PATCH 34/94] fix scheduled task url --- parsons/solidarity_tech/st_scheduled_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py index 79e3f1f64f..9806222840 100644 --- a/parsons/solidarity_tech/st_scheduled_tasks.py +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -121,7 +121,7 @@ def create_scheduled_task( STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. Documentation Reference: - ``__ + ``__ """ payload = { From 3b493edf4db03b5464b05c00591c2278e8bfd0cb Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 18:00:39 -0400 Subject: [PATCH 35/94] add task assignments methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + .../solidarity_tech/st_task_assignments.py | 199 ++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 parsons/solidarity_tech/st_task_assignments.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index c8e8be7ccb..53eb17fee0 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -22,6 +22,7 @@ from parsons.solidarity_tech.st_scheduled_calls import SolidarityTechScheduledCalls from parsons.solidarity_tech.st_scheduled_tasks import SolidarityTechScheduledTasks from parsons.solidarity_tech.st_task_agents import SolidarityTechTaskAgents +from parsons.solidarity_tech.st_task_assignments import SolidarityTechTaskAssignments logger = logging.getLogger(__name__) @@ -49,5 +50,6 @@ class SolidarityTech( SolidarityTechScheduledCalls, SolidarityTechScheduledTasks, SolidarityTechTaskAgents, + SolidarityTechTaskAssignments, ): pass diff --git a/parsons/solidarity_tech/st_task_assignments.py b/parsons/solidarity_tech/st_task_assignments.py new file mode 100644 index 0000000000..688e7f83d3 --- /dev/null +++ b/parsons/solidarity_tech/st_task_assignments.py @@ -0,0 +1,199 @@ +import logging +from datetime import datetime + +import numpy as np + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechTaskAssignments(SolidarityTechBase): + def get_task_assignments( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime = 0, + task_id: int = 0, + agent_user_id: int = 0, + ) -> str: + """ + Retrieve a list of task assignments. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + task_id: + Filters task assignments by phonebank or textbank task within the accessible scope. + agent_user_id: + Filters task assignments by agent user within the accessible scope. + + Returns: + All the task assignment entries. + + Documentation Reference: + ``__ + + """ + params = {"task_id": task_id, "agent_user_id": agent_user_id} + res = self._get_resources( + "task_assignments", + limit=limit, + offset=offset, + since=since, + params=params, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_task_assignment( + self, + id: int, + ) -> str: + """ + Retrieve a single task assignment. + + Args: + id: + ID of the task assignment to retrieve. + + Returns: + A single task assignment entry. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("task_assignments", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_task_assignment( + self, + user_id: np.int64, + task_id: np.int64, + agent_user_id: np.int64 | None = None, + ) -> bool: + """ + Creates a task assignment. + + Assigns a user to participate in a phonebank or textbank campaign. + + Args: + user_id: + Identifier for the task assignment user. + task_id: + Identifier for the phonebank or textbank task. + agent_user_id: + Identifier for the agent user who will conduct outreach (volunteer or staff member). + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "user_id": user_id, + "task_id": task_id, + "agent_user_id": agent_user_id, + } + res = self._post_request( + "task_assignments", + payload=payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code != 201: + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 201 + + def update_task_assignment( + self, + id: int, + agent_user_id: np.int64 | None = None, + ) -> bool: + """ + Updates an task assignment with the specified details. + + Args: + id: + Identifier of the task assignment to update. + agent_user_id: + Identifier for the agent user, if applicable. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "agent_user_id": agent_user_id, + } + res = self._put_request( + "scheduled_tasks", + id, + payload=payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 + + def delete_task_assignment( + self, + id: int, + ) -> bool: + """ + Delete a task assignment with the specified ID. + + Args: + id: + Identifier of the task assignment to delete. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._del_request("task_assignments", id) + + if res and res.status_code != 404: + raise STUnexpectedResponseCodeError(res) + + return not res.status_code From 9a7e22f381dcdb4b9e0047d2dc8792b2b18ef663 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 18:00:55 -0400 Subject: [PATCH 36/94] add team members methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + parsons/solidarity_tech/st_team_members.py | 179 +++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 parsons/solidarity_tech/st_team_members.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 53eb17fee0..2d20b7959b 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -23,6 +23,7 @@ from parsons.solidarity_tech.st_scheduled_tasks import SolidarityTechScheduledTasks from parsons.solidarity_tech.st_task_agents import SolidarityTechTaskAgents from parsons.solidarity_tech.st_task_assignments import SolidarityTechTaskAssignments +from parsons.solidarity_tech.st_team_members import SolidarityTechTeamMembers logger = logging.getLogger(__name__) @@ -51,5 +52,6 @@ class SolidarityTech( SolidarityTechScheduledTasks, SolidarityTechTaskAgents, SolidarityTechTaskAssignments, + SolidarityTechTeamMembers, ): pass diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py new file mode 100644 index 0000000000..a39308c5b0 --- /dev/null +++ b/parsons/solidarity_tech/st_team_members.py @@ -0,0 +1,179 @@ +import logging +from datetime import datetime +from typing import Literal + +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechTeamMembers(SolidarityTechBase): + def get_team_members( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime = 0, + ) -> str: + """ + Retrieve a list of team members. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + + Returns: + All the team member entries. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "team_members", + limit=limit, + offset=offset, + since=since, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_team_member( + self, + role_id: int, + scope_type: Literal["Organization", "Chapter"], + scope_id: int, + invite_via: Literal["sms", "email"], + member_id: str | None = None, + phone_number: str | None = None, + email: str | None = None, + full_name: str | None = None, + first_name: str | None = None, + last_name: str | None = None, + task_id: int | None = None, + ) -> bool: + """ + Creates a new team member. + + Args: + member_id: + Hash ID of existing user (optional if phone_number or email provided). + phone_number: + Phone number of the person (primary key for user lookup/creation). + email: + Email of the person (secondary key for user lookup/creation). + full_name: + Full name for new user creation. + first_name: + First name for new user creation. + last_name: + Last name for new user creation. + role_id: + ID of the role to assign. + scope_type: + Type of scope. + scope_id: + ID of the scope (Chapter or Organization). + invite_via: + How to send the invitation. + task_id: + Optional task ID to assign the member to. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + ValueError: If ``member_id``, ``phone_number``, and ``email`` are all None. + HTTPError: If the parameters are invalid. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + if not member_id and not phone_number and not email: + raise ValueError("One of member_id, phone_number, or email is required.") + + payload = { + "member_id": member_id, + "phone_number": phone_number, + "email": email, + "full_name": full_name, + "first_name": first_name, + "last_name": last_name, + "role_id": role_id, + "scope_type": scope_type, + "scope_id": scope_id, + "invite_via": invite_via, + "task_id": task_id, + } + res = self._post_request( + "team_members", payload=payload, additional_headers={"content-type": "application/json"} + ) + + if res.status_code not in (201, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 422: + raise HTTPError("Invalid parameters", response=res) + + return res.status_code == 201 + + def update_team_member( + self, + id: int, + role_id: int, + scope_type: Literal["Organization", "Chapter"], + scope_id: int, + ) -> bool: + """ + Updates a team member with the specified details. + + Args: + id: + Team member ID (UserRoleScope ID). + role_id: + ID of the role to assign. + scope_type: + Type of scope. + scope_id: + ID of the scope (Chapter or Organization). + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "role_id": role_id, + "scope_type": scope_type, + "scope_id": scope_id, + } + res = self._put_request( + "team_members", + id, + payload=payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 From 9975062a907260e569ae379e4089bf2e6cde6177 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 18:01:27 -0400 Subject: [PATCH 37/94] type improvements --- parsons/solidarity_tech/solidarity_tech_base.py | 10 +++++++--- parsons/solidarity_tech/st_activities.py | 2 +- parsons/solidarity_tech/st_agent_assignments.py | 8 +++++--- parsons/solidarity_tech/st_automation_enrollments.py | 2 +- parsons/solidarity_tech/st_calls.py | 2 +- parsons/solidarity_tech/st_chapter_phone_numbers.py | 2 +- parsons/solidarity_tech/st_chapters.py | 2 +- parsons/solidarity_tech/st_custom_user_properties.py | 2 +- parsons/solidarity_tech/st_donation_charges.py | 2 +- parsons/solidarity_tech/st_email_blasts.py | 2 +- parsons/solidarity_tech/st_event_attendances.py | 6 ++++-- parsons/solidarity_tech/st_event_rsvps.py | 6 +++--- parsons/solidarity_tech/st_event_sessions.py | 8 +++++--- parsons/solidarity_tech/st_events.py | 2 +- parsons/solidarity_tech/st_field_survey_urls.py | 2 +- parsons/solidarity_tech/st_organizations.py | 4 ++-- parsons/solidarity_tech/st_pages.py | 4 ++-- parsons/solidarity_tech/st_phonebanks.py | 4 ++-- parsons/solidarity_tech/st_scheduled_calls.py | 4 ++-- parsons/solidarity_tech/st_scheduled_tasks.py | 8 +++++--- parsons/solidarity_tech/st_task_agents.py | 4 ++-- 21 files changed, 49 insertions(+), 37 deletions(-) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index fb567136d2..c55a294bb9 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -27,7 +27,7 @@ def __init__(self, api_token: str | None = None) -> None: self.api_url = "https://api.solidarity.tech/v1/" self.api = RateLimitedAPIConnector(self.api_url, headers=self.headers) - def _get_resources(self, endpoint: str, limit: int, **kwargs) -> requests.Response: + def _get_resources(self, endpoint: str, **kwargs) -> requests.Response: """Process parameters and handle GET requests for lists of resources.""" since = kwargs.get("since") if isinstance(since, datetime): @@ -67,7 +67,7 @@ def _get_single_resource(self, endpoint: str, id: int, **kwargs) -> requests.Res def _post_request( self, endpoint: str, - payload: Mapping[str, str | int | np.int64 | float] | None = None, + payload: Mapping[str, str | int | np.int64 | float | None] | None = None, **kwargs, ) -> requests.Response: """Handle POST requests.""" @@ -75,7 +75,11 @@ def _post_request( return self.api.request(url=endpoint, req_type="POST", json=payload, **kwargs) def _put_request( - self, endpoint: str, id: int, payload: Mapping[str, str | int] | None = None, **kwargs + self, + endpoint: str, + id: int, + payload: Mapping[str, str | int | np.int64 | float | None] | None = None, + **kwargs, ) -> requests.Response: """Handle PUT requests.""" complete_endpoint = f"{endpoint}/{id}" diff --git a/parsons/solidarity_tech/st_activities.py b/parsons/solidarity_tech/st_activities.py index e2cc50ae4e..b7869346c1 100644 --- a/parsons/solidarity_tech/st_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -12,7 +12,7 @@ def get_activities( self, limit: int = 20, cursor: int | None = None, - since: int | datetime | None = 0, + since: int | datetime = 0, include_count: bool = False, user_id: int | None = None, ) -> str: diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index d06021d707..7b852f6f21 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -14,7 +14,7 @@ def get_agent_assignments( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, user_id: int | None = None, agent_user_id: int | None = None, ) -> str: @@ -113,7 +113,9 @@ def create_agent_assignment( """ payload = {"user_id": user_id, "agent_user_id": agent_user_id, "is_active": is_active} res = self._post_request( - "agent_assignments", payload, additional_headers={"content-type": "application/json"} + "agent_assignments", + payload=payload, + additional_headers={"content-type": "application/json"}, ) if res.status_code not in (201, 404): @@ -156,7 +158,7 @@ def update_agent_assignment( res = self._put_request( "agent_assignments", id, - payload, + payload=payload, additional_headers={"content-type": "application/json"}, ) diff --git a/parsons/solidarity_tech/st_automation_enrollments.py b/parsons/solidarity_tech/st_automation_enrollments.py index 20a898cf38..f93d90bdb9 100644 --- a/parsons/solidarity_tech/st_automation_enrollments.py +++ b/parsons/solidarity_tech/st_automation_enrollments.py @@ -38,7 +38,7 @@ def enroll_user_in_automation( payload = {"automation_id": automation_id, "user_id": user_id} res = self._post_request( "automation_enrollments", - payload, + payload=payload, additional_headers={"content-type": "application/json"}, ) diff --git a/parsons/solidarity_tech/st_calls.py b/parsons/solidarity_tech/st_calls.py index 53efff096e..3ce6f6b8b2 100644 --- a/parsons/solidarity_tech/st_calls.py +++ b/parsons/solidarity_tech/st_calls.py @@ -13,7 +13,7 @@ def get_calls( user_id: int | None = None, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, ) -> str: """ Retrieve a list of calls. diff --git a/parsons/solidarity_tech/st_chapter_phone_numbers.py b/parsons/solidarity_tech/st_chapter_phone_numbers.py index 5db8e49157..689063e203 100644 --- a/parsons/solidarity_tech/st_chapter_phone_numbers.py +++ b/parsons/solidarity_tech/st_chapter_phone_numbers.py @@ -12,7 +12,7 @@ def get_chapter_phone_numbers( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, chapter_id: int = 0, ) -> str: """ diff --git a/parsons/solidarity_tech/st_chapters.py b/parsons/solidarity_tech/st_chapters.py index 802ac38c27..ffb666996e 100644 --- a/parsons/solidarity_tech/st_chapters.py +++ b/parsons/solidarity_tech/st_chapters.py @@ -12,7 +12,7 @@ def get_chapters( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, ) -> str: """ Retrieve a list of chapters. diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index 07cac3c4dd..a384b17149 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -15,7 +15,7 @@ def get_custom_user_properties( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, scope_id: int | None = None, scope_type: Literal["Organization", "Chapter"] | None = None, ) -> str: diff --git a/parsons/solidarity_tech/st_donation_charges.py b/parsons/solidarity_tech/st_donation_charges.py index 0767c65319..b689113f37 100644 --- a/parsons/solidarity_tech/st_donation_charges.py +++ b/parsons/solidarity_tech/st_donation_charges.py @@ -14,7 +14,7 @@ def get_donation_charges( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, ) -> str: """ Retrieve a list of donation charges. diff --git a/parsons/solidarity_tech/st_email_blasts.py b/parsons/solidarity_tech/st_email_blasts.py index ee53afae7c..1b569c906f 100644 --- a/parsons/solidarity_tech/st_email_blasts.py +++ b/parsons/solidarity_tech/st_email_blasts.py @@ -14,7 +14,7 @@ def get_email_blasts( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, ) -> str: """ Retrieve a list of email blasts. diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index a059908279..d6f2e09a69 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -14,7 +14,7 @@ def get_event_attendances( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, event_id: int | None = None, session_id: int | None = None, ) -> str: @@ -93,7 +93,9 @@ def create_event_attendance( "user_id": user_id, } res = self._post_request( - "event_attendances", payload, additional_headers={"content-type": "application/json"} + "event_attendances", + payload=payload, + additional_headers={"content-type": "application/json"}, ) if res.status_code not in (201, 404): diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index 52dc01e3c7..c40d1b2aef 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -15,7 +15,7 @@ def get_event_rsvps( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, event_id: int | None = None, session_id: int | None = None, user_id: int | None = None, @@ -157,7 +157,7 @@ def create_event_rsvp( "skip_email_confirmation": skip_email_confirmation, } res = self._post_request( - "event_rsvps", payload, additional_headers={"content-type": "application/json"} + "event_rsvps", payload=payload, additional_headers={"content-type": "application/json"} ) if res.status_code not in (201, 404): @@ -213,7 +213,7 @@ def update_event_rsvp( res = self._put_request( "event_rsvps", id, - payload, + payload=payload, additional_headers={"content-type": "application/json"}, ) diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index a47b83b69e..1423449253 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -180,7 +180,7 @@ def create_event_sessions( "tags": tags, } res = self._post_request( - "event_rsvps", payload, additional_headers={"content-type": "application/json"} + "event_rsvps", payload=payload, additional_headers={"content-type": "application/json"} ) if res.status_code not in (201, 422): @@ -300,7 +300,7 @@ def update_event_session( res = self._put_request( "event_sessions", id, - payload, + payload=payload, additional_headers={"content-type": "application/json"}, ) @@ -381,7 +381,9 @@ def add_event_host( "user_id": user_id, } res = self._post_request( - "event_sessions", payload, additional_headers={"content-type": "application/json"} + "event_sessions", + payload=payload, + additional_headers={"content-type": "application/json"}, ) if res.status_code not in (200, 404): diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index a31cc09cb7..a71b67c39b 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -157,7 +157,7 @@ def create_event( "skip_duplicate_check": skip_duplicate_check, } res = self._post_request( - "events", payload, additional_headers={"content-type": "application/json"} + "events", payload=payload, additional_headers={"content-type": "application/json"} ) if res.status_code not in (201, 404, 409, 422): diff --git a/parsons/solidarity_tech/st_field_survey_urls.py b/parsons/solidarity_tech/st_field_survey_urls.py index 281089f1fd..b76c380e73 100644 --- a/parsons/solidarity_tech/st_field_survey_urls.py +++ b/parsons/solidarity_tech/st_field_survey_urls.py @@ -49,7 +49,7 @@ def generate_field_survey_url( } res = self._post_request( "field_survey_urls", - payload, + payload=payload, additional_headers={"accept": "application/json", "content-type": "application/json"}, ) diff --git a/parsons/solidarity_tech/st_organizations.py b/parsons/solidarity_tech/st_organizations.py index 005237ecf6..c6d9db7b4c 100644 --- a/parsons/solidarity_tech/st_organizations.py +++ b/parsons/solidarity_tech/st_organizations.py @@ -1,7 +1,7 @@ import logging from datetime import datetime -from requests import HTTPError +from requests.exceptions import HTTPError from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -14,7 +14,7 @@ def get_organizations( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, ) -> str: """ Retrieve a list of organizations. diff --git a/parsons/solidarity_tech/st_pages.py b/parsons/solidarity_tech/st_pages.py index d89a756de9..79426454e5 100644 --- a/parsons/solidarity_tech/st_pages.py +++ b/parsons/solidarity_tech/st_pages.py @@ -1,7 +1,7 @@ import logging from datetime import datetime -from requests import HTTPError +from requests.exceptions import HTTPError from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -14,7 +14,7 @@ def get_pages( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, include_action_counts: bool = False, ) -> str: """ diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py index 7e23c7cdc1..0dd3e04c3a 100644 --- a/parsons/solidarity_tech/st_phonebanks.py +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -1,7 +1,7 @@ import logging from datetime import datetime -from requests import HTTPError +from requests.exceptions import HTTPError from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -14,7 +14,7 @@ def get_phonebanks( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, event_id: int = 0, ids: list[int] | None = None, include_stats: bool = False, diff --git a/parsons/solidarity_tech/st_scheduled_calls.py b/parsons/solidarity_tech/st_scheduled_calls.py index dc9a64469a..b5e7f686ef 100644 --- a/parsons/solidarity_tech/st_scheduled_calls.py +++ b/parsons/solidarity_tech/st_scheduled_calls.py @@ -1,7 +1,7 @@ import logging from datetime import datetime -from requests import HTTPError +from requests.exceptions import HTTPError from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -14,7 +14,7 @@ def get_scheduled_calls( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, user_id: int | None = None, agent_user_id: int | None = None, ) -> str: diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py index 9806222840..4e51ef3942 100644 --- a/parsons/solidarity_tech/st_scheduled_tasks.py +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -14,7 +14,7 @@ def get_scheduled_tasks( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, user_id: int | None = None, agent_user_id: int | None = None, ) -> str: @@ -133,7 +133,9 @@ def create_scheduled_task( "marked_as_completed": marked_as_completed, } res = self._post_request( - "scheduled_tasks", payload, additional_headers={"content-type": "application/json"} + "scheduled_tasks", + payload=payload, + additional_headers={"content-type": "application/json"}, ) if res.status_code not in (201, 404): @@ -195,7 +197,7 @@ def update_scheduled_task( res = self._put_request( "scheduled_tasks", id, - payload, + payload=payload, additional_headers={"content-type": "application/json"}, ) diff --git a/parsons/solidarity_tech/st_task_agents.py b/parsons/solidarity_tech/st_task_agents.py index a10a7e35da..f4baf75212 100644 --- a/parsons/solidarity_tech/st_task_agents.py +++ b/parsons/solidarity_tech/st_task_agents.py @@ -14,7 +14,7 @@ def get_task_agents( self, limit: int = 20, offset: int = 0, - since: int | datetime | None = 0, + since: int | datetime = 0, task_id: int | None = None, ) -> str: """ @@ -110,7 +110,7 @@ def create_task_agent( "task_id": task_id, } res = self._post_request( - "task_agents", payload, additional_headers={"content-type": "application/json"} + "task_agents", payload=payload, additional_headers={"content-type": "application/json"} ) if res.status_code not in (201, 404): From 17a58d4f32b834093e39bee8028ce283e96e9090 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 18:11:42 -0400 Subject: [PATCH 38/94] Use shared literals --- parsons/solidarity_tech/solidarity_tech_literals.py | 9 +++++++++ parsons/solidarity_tech/st_custom_user_properties.py | 10 ++++------ parsons/solidarity_tech/st_event_rsvps.py | 6 +++--- parsons/solidarity_tech/st_event_sessions.py | 4 ++-- parsons/solidarity_tech/st_events.py | 7 ++++--- parsons/solidarity_tech/st_team_members.py | 8 ++++---- 6 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 parsons/solidarity_tech/solidarity_tech_literals.py diff --git a/parsons/solidarity_tech/solidarity_tech_literals.py b/parsons/solidarity_tech/solidarity_tech_literals.py new file mode 100644 index 0000000000..52b6f7f92e --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_literals.py @@ -0,0 +1,9 @@ +from typing import Literal + +AttendanceType = Literal["yes", "no", "maybe", "waitlisted"] +EventType = Literal["virtual", "in_person"] +FieldType = Literal[ + "input", "textarea", "number", "date", "checkbox", "select", "radios", "checkboxes" +] +InviteType = Literal["sms", "email"] +ScopeType = Literal["Organization", "Chapter"] diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index a384b17149..1d50b102a7 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -1,11 +1,11 @@ import logging from datetime import datetime -from typing import Literal from requests.exceptions import HTTPError from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_literals import FieldType, ScopeType logger = logging.getLogger(__name__) @@ -17,7 +17,7 @@ def get_custom_user_properties( offset: int = 0, since: int | datetime = 0, scope_id: int | None = None, - scope_type: Literal["Organization", "Chapter"] | None = None, + scope_type: ScopeType | None = None, ) -> str: """ Retrieve a list of custom user properties. @@ -59,12 +59,10 @@ def get_custom_user_properties( def create_custom_user_property( self, label: str, - field_type: Literal[ - "input", "textarea", "number", "date", "checkbox", "select", "radios", "checkboxes" - ], + field_type: FieldType, description: str | None = None, options: list[dict[str, str | dict[str, str]]] | None = None, - scope_type: Literal["Organization", "Chapter"] | None = None, + scope_type: ScopeType | None = None, scope_id: int | None = None, ) -> bool: """ diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index c40d1b2aef..0c248a631f 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -1,11 +1,11 @@ import logging from datetime import datetime -from typing import Literal import numpy as np from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_literals import AttendanceType logger = logging.getLogger(__name__) @@ -103,7 +103,7 @@ def create_event_rsvp( self, event_id: np.int64, event_session_id: np.int64, - is_attending: Literal["yes", "no", "maybe", "waitlisted"], + is_attending: AttendanceType, agent_user_id: np.int64 | None, user_id: np.int64 | None = None, is_confirmed: bool | None = None, @@ -168,7 +168,7 @@ def create_event_rsvp( def update_event_rsvp( self, id: int, - is_attending: Literal["yes", "no", "maybe", "waitlisted"] | None = None, + is_attending: AttendanceType | None = None, is_confirmed: bool | None = None, agent_user_id: np.int64 | None = None, source: str | None = None, diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index 1423449253..d58de8dccb 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -1,12 +1,12 @@ import logging from datetime import datetime -from typing import Literal import numpy as np from requests.exceptions import HTTPError from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_literals import EventType logger = logging.getLogger(__name__) @@ -107,7 +107,7 @@ def create_event_sessions( start_time: np.int64, end_time: np.int64, title: str, - event_type: Literal["virtual", "in_person"] | None = None, + event_type: EventType | None = None, location_name: str | None = None, location_data: dict[str, str] | None = None, location_address: str | None = None, diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index a71b67c39b..d15a14253d 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -7,6 +7,7 @@ from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_literals import EventType, ScopeType logger = logging.getLogger(__name__) @@ -18,7 +19,7 @@ def get_events( offset: int = 0, since: int | datetime = 0, scope_id: int | None = None, - scope_type: Literal["Organization", "Chapter"] | None = None, + scope_type: ScopeType | None = None, ) -> str: """ Lists events accessible within the given scope. @@ -72,11 +73,11 @@ def get_events( def create_event( self, title: str, - event_type: Literal["virtual", "in_person", "hybrid"], + event_type: EventType | Literal["hybrid"], start_time: np.int64, end_time: np.int64, scope_id: str, - scope_type: Literal["Organization", "Chapter"], + scope_type: ScopeType, location_address: str | None = None, virtual_url: str | None = None, location_name: str | None = None, diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py index a39308c5b0..5e16fa6631 100644 --- a/parsons/solidarity_tech/st_team_members.py +++ b/parsons/solidarity_tech/st_team_members.py @@ -1,11 +1,11 @@ import logging from datetime import datetime -from typing import Literal from requests.exceptions import HTTPError from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_literals import InviteType, ScopeType logger = logging.getLogger(__name__) @@ -51,9 +51,9 @@ def get_team_members( def create_team_member( self, role_id: int, - scope_type: Literal["Organization", "Chapter"], + scope_type: ScopeType, scope_id: int, - invite_via: Literal["sms", "email"], + invite_via: InviteType, member_id: str | None = None, phone_number: str | None = None, email: str | None = None, @@ -134,7 +134,7 @@ def update_team_member( self, id: int, role_id: int, - scope_type: Literal["Organization", "Chapter"], + scope_type: ScopeType, scope_id: int, ) -> bool: """ From 76dc032be41aff297d4ed1a675be898c8aa42656 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 18:27:50 -0400 Subject: [PATCH 39/94] url fixes --- parsons/solidarity_tech/st_task_assignments.py | 4 ++-- parsons/solidarity_tech/st_team_members.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/parsons/solidarity_tech/st_task_assignments.py b/parsons/solidarity_tech/st_task_assignments.py index 688e7f83d3..b86e5fe78d 100644 --- a/parsons/solidarity_tech/st_task_assignments.py +++ b/parsons/solidarity_tech/st_task_assignments.py @@ -151,7 +151,7 @@ def update_task_assignment( STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. Documentation Reference: - ``__ + ``__ """ payload = { @@ -188,7 +188,7 @@ def delete_task_assignment( STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. Documentation Reference: - ``__ + ``__ """ res = self._del_request("task_assignments", id) diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py index 5e16fa6631..87ae7b1284 100644 --- a/parsons/solidarity_tech/st_team_members.py +++ b/parsons/solidarity_tech/st_team_members.py @@ -33,7 +33,7 @@ def get_team_members( All the team member entries. Documentation Reference: - ``__ + ``__ """ res = self._get_resources( @@ -99,7 +99,7 @@ def create_team_member( STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. Documentation Reference: - ``__ + ``__ """ if not member_id and not phone_number and not email: @@ -158,7 +158,7 @@ def update_team_member( STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. Documentation Reference: - ``__ + ``__ """ payload = { From f652ced7371b0f2bdc9c3af0038db02d13f108bd Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 18:28:13 -0400 Subject: [PATCH 40/94] add text blast methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + parsons/solidarity_tech/st_text_blasts.py | 74 ++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 parsons/solidarity_tech/st_text_blasts.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 2d20b7959b..504492a9b3 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -24,6 +24,7 @@ from parsons.solidarity_tech.st_task_agents import SolidarityTechTaskAgents from parsons.solidarity_tech.st_task_assignments import SolidarityTechTaskAssignments from parsons.solidarity_tech.st_team_members import SolidarityTechTeamMembers +from parsons.solidarity_tech.st_text_blasts import SolidarityTechTextBlasts logger = logging.getLogger(__name__) @@ -53,5 +54,6 @@ class SolidarityTech( SolidarityTechTaskAgents, SolidarityTechTaskAssignments, SolidarityTechTeamMembers, + SolidarityTechTextBlasts, ): pass diff --git a/parsons/solidarity_tech/st_text_blasts.py b/parsons/solidarity_tech/st_text_blasts.py new file mode 100644 index 0000000000..f133db2f22 --- /dev/null +++ b/parsons/solidarity_tech/st_text_blasts.py @@ -0,0 +1,74 @@ +import logging +from datetime import datetime + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechTextBlasts(SolidarityTechBase): + def get_text_blasts( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime = 0, + ) -> str: + """ + Retrieve a list of text blasts. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + + Returns: + All the text blast entries. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "text_blasts", + limit=limit, + offset=offset, + since=since, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_text_blast( + self, + id: int, + ) -> str: + """ + Retrieve a single text blast. + + Args: + id: + ID of the text blast to retrieve. + + Returns: + A single text blast entry. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("text_blasts", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text From 8c378bc442c235fe69d5af7d667cc96544ecca7c Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 18:28:44 -0400 Subject: [PATCH 41/94] add text templates methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + parsons/solidarity_tech/st_text_templates.py | 223 +++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 parsons/solidarity_tech/st_text_templates.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 504492a9b3..afd352311c 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -25,6 +25,7 @@ from parsons.solidarity_tech.st_task_assignments import SolidarityTechTaskAssignments from parsons.solidarity_tech.st_team_members import SolidarityTechTeamMembers from parsons.solidarity_tech.st_text_blasts import SolidarityTechTextBlasts +from parsons.solidarity_tech.st_text_templates import SolidarityTechTextTemplates logger = logging.getLogger(__name__) @@ -55,5 +56,6 @@ class SolidarityTech( SolidarityTechTaskAssignments, SolidarityTechTeamMembers, SolidarityTechTextBlasts, + SolidarityTechTextTemplates, ): pass diff --git a/parsons/solidarity_tech/st_text_templates.py b/parsons/solidarity_tech/st_text_templates.py new file mode 100644 index 0000000000..f9d2997ab0 --- /dev/null +++ b/parsons/solidarity_tech/st_text_templates.py @@ -0,0 +1,223 @@ +import logging +from datetime import datetime + +import numpy as np + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_literals import ScopeType + +logger = logging.getLogger(__name__) + + +class SolidarityTechTextTemplates(SolidarityTechBase): + def get_text_templates( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime = 0, + event_id: int = 0, + ) -> str: + """ + Retrieve a list of text templates. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + event_id: + Filters rsvps by event_id within the accessible scope. + + Returns: + All the text templates. + + Documentation Reference: + ``__ + + """ + params = {"event_id": event_id} + res = self._get_resources( + "text_templates", + limit=limit, + offset=offset, + since=since, + params=params, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_text_template( + self, + id: int, + ) -> str: + """ + Retrieve a single text template. + + Args: + id: + ID of the text template to retrieve. + + Returns: + A single text template. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("text_templates", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_text_template( + self, + scope_id: np.int64, + scope_type: ScopeType, + name: str | None = None, + template: dict[str, str] | None = None, + event_id: np.int64 | None = None, + ) -> bool: + """ + Creates an text template with the specified details. + + Args: + scope_id: + Identifier for the scope. + scope_type: + Type of the scope. + name: + Name of the entity. + template: + Template content in various languages, + where keys are 2-character language codes + (e.g., "en" for English, "fr" for French). + event_id: + Identifier for the associated event, if applicable. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "name": name, + "scope_id": scope_id, + "scope_type": scope_type, + "template": template, + "event_id": event_id, + } + res = self._post_request( + "text_templates", + payload=payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code not in (201, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 201 + + def update_text_template( + self, + id: int, + name: str | None = None, + scope_id: np.int64 | None = None, + scope_type: ScopeType | None = None, + template: dict[str, str] | None = None, + event_id: np.int64 | None = None, + ) -> bool: + """ + Updates an text template with the specified details. + + Args: + id: + Identifier of the text template to update. + name: + Name of the entity. + scope_id: + Identifier for the scope. + scope_type: + Type of the scope. + template: + Template content in various languages, + where keys are 2-character language codes + (e.g., "en" for English, "fr" for French). + event_id: + Identifier for the associated event, if applicable. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "name": name, + "scope_id": scope_id, + "scope_type": scope_type, + "template": template, + "event_id": event_id, + } + res = self._put_request( + "text_templates", + id, + payload=payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 + + def delete_text_template( + self, + id: int, + ) -> bool: + """ + Deletes an text template with the specified ID. + + Args: + id: + Identifier of the text template to delete. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._del_request("text_templates", id) + + if res and res.status_code != 404: + raise STUnexpectedResponseCodeError(res) + + return not res.status_code From 40138bdf1bc44f78e23fcf987895d5358dd6d073 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 19:17:34 -0400 Subject: [PATCH 42/94] add textbank methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + parsons/solidarity_tech/st_textbanks.py | 89 ++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 parsons/solidarity_tech/st_textbanks.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index afd352311c..5319e8a305 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -26,6 +26,7 @@ from parsons.solidarity_tech.st_team_members import SolidarityTechTeamMembers from parsons.solidarity_tech.st_text_blasts import SolidarityTechTextBlasts from parsons.solidarity_tech.st_text_templates import SolidarityTechTextTemplates +from parsons.solidarity_tech.st_textbanks import SolidarityTechTextbanks logger = logging.getLogger(__name__) @@ -57,5 +58,6 @@ class SolidarityTech( SolidarityTechTeamMembers, SolidarityTechTextBlasts, SolidarityTechTextTemplates, + SolidarityTechTextbanks, ): pass diff --git a/parsons/solidarity_tech/st_textbanks.py b/parsons/solidarity_tech/st_textbanks.py new file mode 100644 index 0000000000..7547b79e92 --- /dev/null +++ b/parsons/solidarity_tech/st_textbanks.py @@ -0,0 +1,89 @@ +import logging +from datetime import datetime + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechTextbanks(SolidarityTechBase): + def get_textbanks( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime = 0, + event_id: int = 0, + ids: str | None = None, + include_stats: bool = False, + ) -> str: + """ + Retrieve a list of textbanks. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + event_id: + Filters textbanks by event_id within the accessible scope. + ids: + Filters to specific textbank ids. + Accepts a comma-separated string (e.g. "12,34"). + include_stats: + If True, each textbank row also includes aggregate funnel numbers + ``attempts`` (contact attempts), ``contacted`` (distinct people attempted), + and ``replies`` (distinct attempts that got a response). + Default is False. + + Returns: + All the textbanks. + + Documentation Reference: + ``__ + + """ + params = {"event_id": event_id, "ids": ids, "include_stats": include_stats} + res = self._get_resources( + "textbanks", + limit=limit, + offset=offset, + since=since, + params=params, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_textbank( + self, + id: int, + ) -> str: + """ + Retrieve a single textbank. + + Args: + id: + ID of the textbank to retrieve. + + Returns: + A single textbank. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("textbanks", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text From 6832df729dfc518640f4bad4a5799c87904944da Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 19:45:37 -0400 Subject: [PATCH 43/94] add texts and user_actions methods --- parsons/solidarity_tech/solidarity_tech.py | 4 + parsons/solidarity_tech/st_texts.py | 103 ++++++++++++++ parsons/solidarity_tech/st_user_actions.py | 153 +++++++++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 parsons/solidarity_tech/st_texts.py create mode 100644 parsons/solidarity_tech/st_user_actions.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 5319e8a305..2860dd1785 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -27,6 +27,8 @@ from parsons.solidarity_tech.st_text_blasts import SolidarityTechTextBlasts from parsons.solidarity_tech.st_text_templates import SolidarityTechTextTemplates from parsons.solidarity_tech.st_textbanks import SolidarityTechTextbanks +from parsons.solidarity_tech.st_texts import SolidarityTechTexts +from parsons.solidarity_tech.st_user_actions import SolidarityTechUserActions logger = logging.getLogger(__name__) @@ -59,5 +61,7 @@ class SolidarityTech( SolidarityTechTextBlasts, SolidarityTechTextTemplates, SolidarityTechTextbanks, + SolidarityTechTexts, + SolidarityTechUserActions, ): pass diff --git a/parsons/solidarity_tech/st_texts.py b/parsons/solidarity_tech/st_texts.py new file mode 100644 index 0000000000..84aa35ff04 --- /dev/null +++ b/parsons/solidarity_tech/st_texts.py @@ -0,0 +1,103 @@ +import logging +from datetime import datetime + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechTexts(SolidarityTechBase): + def get_texts( + self, + user_id: int | None = None, + limit: int = 20, + offset: int = 0, + since: int | datetime = 0, + ) -> str: + """ + Retrieve a list of texts. + + Args: + user_id: + The ID of the user to retrieve texts for. + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + + Returns: + All the texts. + + Documentation Reference: + ``__ + + """ + params = {"user_id": user_id} + res = self._get_resources( + "texts", + limit=limit, + offset=offset, + since=since, + params=params, + additional_headers={"accept": "application/json"}, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def send_text( + self, + user_id: int, + body: str, + media_urls: list[str] | None = None, + attach_contact_card: bool | None = None, + shorten_urls: bool | None = None, + ) -> bool: + """ + Sends a text to a specific user. + + Args: + user_id: + The ID of the user to send a text to. + body: + The text body to send. + media_urls: + List of media to include in the text. + attach_contact_card: + Whether to attach the contact card to the text. + shorten_urls: + Whether to shorten URLs in the text. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + params = { + "user_id": user_id, + "body": body, + "media_urls": media_urls, + "attach_contact_card": attach_contact_card, + "shorten_urls": shorten_urls, + } + res = self._post_request( + "texts", + params=params, + ) + + if res.status_code not in (201, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 201 diff --git a/parsons/solidarity_tech/st_user_actions.py b/parsons/solidarity_tech/st_user_actions.py new file mode 100644 index 0000000000..5df17941fa --- /dev/null +++ b/parsons/solidarity_tech/st_user_actions.py @@ -0,0 +1,153 @@ +import logging +from datetime import datetime +from typing import Literal + +import numpy as np +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +logger = logging.getLogger(__name__) + + +class SolidarityTechUserActions(SolidarityTechBase): + def get_user_actions( + self, + user_id: int | None = None, + page_id: int | None = None, + group_by: Literal["referred_by_user"] | None = None, + limit: int = 20, + offset: int = 0, + since: int | datetime = 0, + ) -> str: + """ + Lists user actions (form submissions). + + .. admonition:: Filtering + + Can be filtered by ``user_id``, ``page_id``, or both. + To get custom form responses for event RSVPs, + first get the event's ``event_page_id`` from + ``GET /events/{id}``, then query this endpoint with that page_id. + Match to RSVPs by user_id. + With ``group_by=referred_by_user`` the response becomes a + eferral leaderboard instead of submission rows. + There is one row per referrer + (``{referred_by_user_id, count, user: {id, first_name, last_name}}``), + ordered by submission count descending, honoring the same filters. + + Args: + user_id: + Filter by user ID. + page_id: + Filter by page ID + group_by: + Set to referred_by_user for a referral leaderboard + (see the endpoint description). + Any other value returns 422. + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + + Returns: + All the user actions. + + Documentation Reference: + ``__ + + """ + params = { + "user_id": user_id, + "page_id": page_id, + "group_by": group_by, + } + res = self._get_resources( + "user_actions", + limit=limit, + offset=offset, + since=since, + params=params, + additional_headers={"accept": "application/json"}, + ) + + if res.status_code not in (200, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 422: + err_msg = "Could not process request. group_by value may be invalid." + raise HTTPError(err_msg, response=res) + + return res.text + + def create_user_action( + self, + page_id: np.int64, + user_id: np.int64 | None = None, + created_at: np.int64 | None = None, + data: dict[str, str | int | bool | dict[str, str]] | None = None, + ) -> bool: + """ + Creates a user action for a user. + + .. note:: + + This endpoint cannot be used for creating actions + related to donation pages or scheduled call pages. + + Args: + page_id: + Identifier for the Page, required for new user actions. + user_id: + Identifier for the User. + created_at: + UTC timestamp in seconds since the Unix epoch for the creation time of the user action + data: + Action data. See documentation. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + ValueError: If neither ``user_id``, ``phone_number``, nor ``email`` is provided. + HTTPError: If the operation fails with a 422 status code. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + if ( + not page_id + and isinstance(data, dict) + and "phone_number" not in data + and "email" not in data + ): + raise ValueError("Either user_id, phone_number, or email must be provided") + + payload = { + "page_id": page_id, + "user_id": user_id, + "created_at": created_at, + "data": data, + } + res = self._post_request( + "user_actions", + payload=payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code not in (201, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 422: + raise HTTPError( + "Request could not be processed, provided data may be invalid", response=res + ) + + return res.status_code == 201 From d8417b290b7d716df30bf56d5cbc2d2b87f2aaeb Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 29 Jul 2026 20:14:36 -0400 Subject: [PATCH 44/94] add apiconnector tests including header init and combination --- parsons/utilities/api_connector.py | 2 +- test/test_utilities/test_api_connector.py | 188 ++++++++++++++++++++++ 2 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 test/test_utilities/test_api_connector.py diff --git a/parsons/utilities/api_connector.py b/parsons/utilities/api_connector.py index 7754d18eca..b50b9fd0eb 100644 --- a/parsons/utilities/api_connector.py +++ b/parsons/utilities/api_connector.py @@ -197,7 +197,7 @@ def get_request( or :attr:`requests.Response.content` from the response if `return_format` is ``content``. Raises: - RuntimeError: If return_format is not ``json`` or ``content``. + RuntimeError: If ``return_format`` is not ``json`` or ``content``. """ r = self.request(url, "GET", params=params, raise_on_error=raise_on_error, **kwargs) diff --git a/test/test_utilities/test_api_connector.py b/test/test_utilities/test_api_connector.py new file mode 100644 index 0000000000..b29b8420ad --- /dev/null +++ b/test/test_utilities/test_api_connector.py @@ -0,0 +1,188 @@ +import pytest +import requests +from requests.exceptions import HTTPError + +from parsons import Table +from parsons.utilities.api_connector import APIConnector + + +@pytest.fixture +def connector() -> APIConnector: + return APIConnector( + uri="https://api.example.com/v1", headers={"content-type": "application/json"} + ) + + +def test_init_adds_trailing_slash() -> None: + conn = APIConnector(uri="https://api.example.com/v1") + assert conn.uri == "https://api.example.com/v1/" + + +def test_init_adds_headers(connector: APIConnector, requests_mock) -> None: + requests_mock.get( + "https://api.example.com/v1/data", json={"status": "authorized"}, status_code=200 + ) + + connector.request("data", "GET") + + req = requests_mock.last_request + assert req.headers["content-type"] == "application/json" + + +def test_request_with_additional_headers(connector: APIConnector, requests_mock) -> None: + requests_mock.get( + "https://api.example.com/v1/data", json={"status": "authorized"}, status_code=200 + ) + + connector.request( + "data", + "GET", + additional_headers={"Authorization": "Bearer token123", "X-Custom-Header": "value"}, + ) + + req = requests_mock.last_request + assert req.headers["Authorization"] == "Bearer token123" + assert req.headers["X-Custom-Header"] == "value" + + +def test_request_merges_base_and_additional_headers(connector: APIConnector, requests_mock) -> None: + requests_mock.get("https://api.example.com/v1/data", json={}, status_code=200) + + connector.request( + "data", + "GET", + additional_headers={"Authorization": "Bearer token123", "X-Custom-Header": "value"}, + ) + + req = requests_mock.last_request + assert req.headers["content-type"] == "application/json" + assert req.headers["Authorization"] == "Bearer token123" + assert req.headers["X-Custom-Header"] == "value" + + +def test_request_success(connector: APIConnector, requests_mock) -> None: + requests_mock.get( + "https://api.example.com/v1/users", json={"status": "success"}, status_code=200 + ) + + resp = connector.request("users", "GET") + assert resp.status_code == 200 + assert resp.json() == {"status": "success"} + + +def test_get_request_json(connector: APIConnector, requests_mock) -> None: + requests_mock.get("https://api.example.com/v1/data", json={"data": [1, 2, 3]}, status_code=200) + + result = connector.get_request("data", return_format="json") + assert result == {"data": [1, 2, 3]} + + +def test_get_request_content(connector: APIConnector, requests_mock) -> None: + requests_mock.get( + "https://api.example.com/v1/file", content=b"file content bytes", status_code=200 + ) + + result = connector.get_request("file", return_format="content") + assert result == b"file content bytes" + + +def test_get_request_invalid_format(connector: APIConnector, requests_mock) -> None: + requests_mock.get( + "https://api.example.com/v1/data", content=b"file content bytes", status_code=200 + ) + + with pytest.raises(RuntimeError, match="is not a valid format, change to json or content"): + connector.get_request("data", return_format="invalid") # type: ignore + + +def test_post_request(connector: APIConnector, requests_mock) -> None: + requests_mock.post( + "https://api.example.com/v1/items", json={"id": 123, "created": True}, status_code=201 + ) + + result = connector.post_request("items", json={"name": "test"}) + assert result == {"id": 123, "created": True} + + +def test_delete_request(connector: APIConnector, requests_mock) -> None: + requests_mock.delete("https://api.example.com/v1/items/1", status_code=204) + + result = connector.delete_request("items/1") + assert result == 204 + + +def test_put_request(connector: APIConnector, requests_mock) -> None: + requests_mock.put("https://api.example.com/v1/items/1", json={"updated": True}, status_code=200) + + result = connector.put_request("items/1", json={"name": "new_name"}) + assert result == {"updated": True} + + +def test_patch_request(connector: APIConnector, requests_mock) -> None: + requests_mock.patch( + "https://api.example.com/v1/items/1", json={"patched": True}, status_code=200 + ) + + result = connector.patch_request("items/1", json={"name": "patch_name"}) + assert result == {"patched": True} + + +def test_validate_response_error(connector: APIConnector, requests_mock) -> None: + requests_mock.get( + "https://api.example.com/v1/error", json={"error": "Unauthorized"}, status_code=401 + ) + + with pytest.raises(HTTPError) as exc_info: + connector.get_request("error") + + assert "Code: 401" in str(exc_info.value) + assert "Unauthorized" in str(exc_info.value) + + +def test_data_parse_with_data_key() -> None: + conn = APIConnector(uri="https://api.example.com/v1/", data_key="results") + payload = {"results": [{"id": 1}, {"id": 2}], "count": 2} + parsed = conn.data_parse(payload) + assert parsed == [{"id": 1}, {"id": 2}] + + +def test_data_parse_list_input() -> None: + conn = APIConnector(uri="https://api.example.com/v1/") + payload = [{"id": 1}, {"id": 2}] + parsed = conn.data_parse(payload) + assert parsed == payload + + +def test_next_page_check_url() -> None: + conn = APIConnector(uri="https://api.example.com/v1/", pagination_key="next") + + assert conn.next_page_check_url({"next": "https://api.example.com/v1/data?page=2"}) is True + assert conn.next_page_check_url({"next": None}) is False + assert conn.next_page_check_url({"other_key": "val"}) is False + + +def test_json_check(connector: APIConnector, requests_mock) -> None: + requests_mock.get("https://api.example.com/v1/json-check", json={"test": True}, status_code=200) + requests_mock.get( + "https://api.example.com/v1/text-check", text="Plain text response", status_code=200 + ) + + resp_json = requests.get("https://api.example.com/v1/json-check") + resp_text = requests.get("https://api.example.com/v1/text-check") + + assert connector.json_check(resp_json) is True + assert connector.json_check(resp_text) is False + + +def test_convert_to_table(connector) -> None: + list_data = [{"col1": "A", "col2": 1}, {"col1": "B", "col2": 2}] + dict_data = {"col1": "A", "col2": 1} + + table_from_list = connector.convert_to_table(list_data) + table_from_dict = connector.convert_to_table(dict_data) + + assert isinstance(table_from_list, Table) + assert table_from_list.num_rows == 2 + + assert isinstance(table_from_dict, Table) + assert table_from_dict.num_rows == 1 From 0acb2839b3511f1d6ffa20d5b648f77cbb23313a Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 30 Jul 2026 09:23:41 -0400 Subject: [PATCH 45/94] add user list methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + parsons/solidarity_tech/st_user_lists.py | 234 +++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 parsons/solidarity_tech/st_user_lists.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 2860dd1785..ae67f20d80 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -29,6 +29,7 @@ from parsons.solidarity_tech.st_textbanks import SolidarityTechTextbanks from parsons.solidarity_tech.st_texts import SolidarityTechTexts from parsons.solidarity_tech.st_user_actions import SolidarityTechUserActions +from parsons.solidarity_tech.st_user_lists import SolidarityTechUserLists logger = logging.getLogger(__name__) @@ -63,5 +64,6 @@ class SolidarityTech( SolidarityTechTextbanks, SolidarityTechTexts, SolidarityTechUserActions, + SolidarityTechUserLists, ): pass diff --git a/parsons/solidarity_tech/st_user_lists.py b/parsons/solidarity_tech/st_user_lists.py new file mode 100644 index 0000000000..2e031a38fd --- /dev/null +++ b/parsons/solidarity_tech/st_user_lists.py @@ -0,0 +1,234 @@ +import logging +import numbers +from datetime import datetime + +import numpy as np +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_literals import ScopeType + +CompareValueType = str | numbers.Rational | bool +QueryParamType = dict[ + str, str | bool | list[dict[str, CompareValueType | list[dict[str, CompareValueType]]]] +] + +logger = logging.getLogger(__name__) + + +class SolidarityTecUserLists(SolidarityTechBase): + def get_user_lists( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime = 0, + ) -> str: + """ + Retrieve a list of user lists. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + + Returns: + All the user lists. + + Documentation Reference: + ``__ + + """ + res = self._get_resources( + "user_lists", + limit=limit, + offset=offset, + since=since, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def get_user_list( + self, + id: int, + ) -> str: + """ + Retrieve a single user list. + + Args: + id: + ID of the user list to retrieve. + + Returns: + A single user list. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource("user_lists", id) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_user_list( + self, + name: str, + scope_id: np.int64, + scope_type: ScopeType, + event_id: np.int64 | None = None, + user_id: np.int64 | None = None, + parameters: QueryParamType | None = None, + ) -> bool: + """ + Create a user list with the specified details. + + The parameters field must conform to the QueryBuilder format. + For documentation, see ``__. + + Args: + name: + Name of the user list. + scope_id: + Identifier for the scope. + scope_type: + Type of the scope. + event_id: + Identifier for the associated event, if applicable. + user_id: + Identifier for the associated user. + ``parameters``: + Parameters for filtering users in QueryBuilder format. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: If the operation fails with a 422 status code. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "name": name, + "scope_id": scope_id, + "scope_type": scope_type, + "event_id": event_id, + "user_id": user_id, + "parameters": parameters, + } + res = self._post_request( + "user_lists", payload=payload, additional_headers={"content-type": "application/json"} + ) + + if res.status_code not in (201, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 422: + raise HTTPError("Unprocessable request, likely issue with parameters", response=res) + + return res.status_code == 201 + + def update_user_list( + self, + id: int, + name: str | None = None, + scope_id: np.int64 | None = None, + scope_type: str | None = None, + parameters: QueryParamType | None = None, + event_id: np.int64 | None = None, + ) -> bool: + """ + Update a user list with the specified details. + + Args: + id: + Identifier of the user list to update. + name: + Name of the user list. + scope_id: + Identifier of the scope. + scope_type: + Type of the scope. + ``parameters``: + Parameters for filtering users in QueryBuilder format. + event_id: + Identifier for the associated event, if applicable. + + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + payload = { + "name": name, + "scope_id": scope_id, + "scope_type": scope_type, + "parameters": parameters, + "event_id": event_id, + } + res = self._put_request( + "user_lists", + id, + payload=payload, + additional_headers={"content-type": "application/json"}, + ) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 + + def delete_user_list( + self, + id: str, + ) -> bool: + """ + Delete a user list with the specified ID. + + Args: + id: + Identifier of the user list to delete + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + res = self._del_request( + "user_lists", + id, + ) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 From 2749474464da76ec91bedaa56b842e837aba1cd3 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 30 Jul 2026 09:24:40 -0400 Subject: [PATCH 46/94] docstrings to imperative mood --- parsons/solidarity_tech/st_event_attendances.py | 2 +- parsons/solidarity_tech/st_event_rsvps.py | 4 ++-- parsons/solidarity_tech/st_event_sessions.py | 4 ++-- parsons/solidarity_tech/st_events.py | 2 +- parsons/solidarity_tech/st_task_assignments.py | 4 ++-- parsons/solidarity_tech/st_team_members.py | 4 ++-- parsons/solidarity_tech/st_text_templates.py | 6 +++--- parsons/solidarity_tech/st_user_actions.py | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index d6f2e09a69..5e3f2ff4df 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -63,7 +63,7 @@ def create_event_attendance( attended: bool, ) -> bool: """ - Creates an event attendance with the specified details. + Create an event attendance with the specified details. Args: event_id: diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index 0c248a631f..6f8566194f 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -112,7 +112,7 @@ def create_event_rsvp( skip_email_confirmation: bool = False, ) -> bool: """ - Creates an event rsvp with the specified details. + Create an event rsvp with the specified details. Args: event_id: @@ -175,7 +175,7 @@ def update_event_rsvp( source_system: str | None = None, ) -> bool: """ - Updates an event rsvp with the specified details. + Update an event rsvp with the specified details. Args: id: diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index d58de8dccb..6c8ce80c66 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -118,7 +118,7 @@ def create_event_sessions( tags: list[str] | None = None, ) -> bool: """ - Creates an event rsvp with the specified details. + Create an event rsvp with the specified details. Args: event_id: @@ -241,7 +241,7 @@ def update_event_session( tags: list[str] | None = None, ) -> bool: """ - Updates an event session with the specified details. + Update an event session with the specified details. Args: id: diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index d15a14253d..97e38b55d2 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -89,7 +89,7 @@ def create_event( skip_duplicate_check: bool = False, ) -> bool: """ - Creates an event with its first event session. + Create an event with its first event session. The event session inherits the title from the event unless ``session_title`` is provided. diff --git a/parsons/solidarity_tech/st_task_assignments.py b/parsons/solidarity_tech/st_task_assignments.py index b86e5fe78d..2ce788edc7 100644 --- a/parsons/solidarity_tech/st_task_assignments.py +++ b/parsons/solidarity_tech/st_task_assignments.py @@ -90,7 +90,7 @@ def create_task_assignment( agent_user_id: np.int64 | None = None, ) -> bool: """ - Creates a task assignment. + Create a task assignment. Assigns a user to participate in a phonebank or textbank campaign. @@ -135,7 +135,7 @@ def update_task_assignment( agent_user_id: np.int64 | None = None, ) -> bool: """ - Updates an task assignment with the specified details. + Update an task assignment with the specified details. Args: id: diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py index 87ae7b1284..293376a116 100644 --- a/parsons/solidarity_tech/st_team_members.py +++ b/parsons/solidarity_tech/st_team_members.py @@ -63,7 +63,7 @@ def create_team_member( task_id: int | None = None, ) -> bool: """ - Creates a new team member. + Create a new team member. Args: member_id: @@ -138,7 +138,7 @@ def update_team_member( scope_id: int, ) -> bool: """ - Updates a team member with the specified details. + Update a team member with the specified details. Args: id: diff --git a/parsons/solidarity_tech/st_text_templates.py b/parsons/solidarity_tech/st_text_templates.py index f9d2997ab0..3cabd1a936 100644 --- a/parsons/solidarity_tech/st_text_templates.py +++ b/parsons/solidarity_tech/st_text_templates.py @@ -90,7 +90,7 @@ def create_text_template( event_id: np.int64 | None = None, ) -> bool: """ - Creates an text template with the specified details. + Create an text template with the specified details. Args: scope_id: @@ -145,7 +145,7 @@ def update_text_template( event_id: np.int64 | None = None, ) -> bool: """ - Updates an text template with the specified details. + Update an text template with the specified details. Args: id: @@ -198,7 +198,7 @@ def delete_text_template( id: int, ) -> bool: """ - Deletes an text template with the specified ID. + Delete an text template with the specified ID. Args: id: diff --git a/parsons/solidarity_tech/st_user_actions.py b/parsons/solidarity_tech/st_user_actions.py index 5df17941fa..135a6f4338 100644 --- a/parsons/solidarity_tech/st_user_actions.py +++ b/parsons/solidarity_tech/st_user_actions.py @@ -92,7 +92,7 @@ def create_user_action( data: dict[str, str | int | bool | dict[str, str]] | None = None, ) -> bool: """ - Creates a user action for a user. + Create a user action for a user. .. note:: From a57760b2c0fa0890cd23e93ca32df1d4936680ed Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 30 Jul 2026 09:37:31 -0400 Subject: [PATCH 47/94] fix class name of SolidarityTechUserLists --- parsons/solidarity_tech/st_user_lists.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/parsons/solidarity_tech/st_user_lists.py b/parsons/solidarity_tech/st_user_lists.py index 2e031a38fd..e9d6a9bd7d 100644 --- a/parsons/solidarity_tech/st_user_lists.py +++ b/parsons/solidarity_tech/st_user_lists.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) -class SolidarityTecUserLists(SolidarityTechBase): +class SolidarityTechUserLists(SolidarityTechBase): def get_user_lists( self, limit: int = 20, @@ -156,6 +156,9 @@ def update_user_list( """ Update a user list with the specified details. + The parameters field must conform to the QueryBuilder format. + For documentation, see ``__. + Args: id: Identifier of the user list to update. From a7370f40947763422b07ce580e16c08e8091aa7c Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 30 Jul 2026 09:38:22 -0400 Subject: [PATCH 48/94] add user notes methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + .../solidarity_tech_literals.py | 1 + parsons/solidarity_tech/st_user_notes.py | 108 ++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 parsons/solidarity_tech/st_user_notes.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index ae67f20d80..6ac3b38a59 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -30,6 +30,7 @@ from parsons.solidarity_tech.st_texts import SolidarityTechTexts from parsons.solidarity_tech.st_user_actions import SolidarityTechUserActions from parsons.solidarity_tech.st_user_lists import SolidarityTechUserLists +from parsons.solidarity_tech.st_user_notes import SolidarityTechUserNotes logger = logging.getLogger(__name__) @@ -65,5 +66,6 @@ class SolidarityTech( SolidarityTechTexts, SolidarityTechUserActions, SolidarityTechUserLists, + SolidarityTechUserNotes, ): pass diff --git a/parsons/solidarity_tech/solidarity_tech_literals.py b/parsons/solidarity_tech/solidarity_tech_literals.py index 52b6f7f92e..3352108239 100644 --- a/parsons/solidarity_tech/solidarity_tech_literals.py +++ b/parsons/solidarity_tech/solidarity_tech_literals.py @@ -7,3 +7,4 @@ ] InviteType = Literal["sms", "email"] ScopeType = Literal["Organization", "Chapter"] +InteractionType = Literal["in_person", "call", "text", "email"] diff --git a/parsons/solidarity_tech/st_user_notes.py b/parsons/solidarity_tech/st_user_notes.py new file mode 100644 index 0000000000..9da255a8bd --- /dev/null +++ b/parsons/solidarity_tech/st_user_notes.py @@ -0,0 +1,108 @@ +import logging + +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.solidarity_tech_literals import InteractionType + +logger = logging.getLogger(__name__) + + +class SolidarityTechUserNotes(SolidarityTechBase): + def create_user_note( + self, + user_id: int, + content: str, + agent_id: int | None = None, + created_at: int | None = None, + restricted: bool = False, + interaction_method: InteractionType | None = None, + ) -> bool: + """ + Create a user note with the specified details. + + Args: + user_id: + Identifier for the user the note refers to. + agent_id: + Identifier for the agent to whom the note + is attributed, if applicable. + content: + Content of the user note. + created_at: + Timestamp for when the note was created. + restricted: + If True, the note is only visible to team members + with the View Restricted Properties permission. + interaction_method: + Interaction type that produced the note. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: If the operation fails with a 422 status code. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + params = { + "user_id": user_id, + "agent_id": agent_id, + "content": content, + "created_at": created_at, + "restricted": restricted, + "interaction_method": interaction_method, + } + res = self._post_request("user_notes", params=params) + + if res.status_code not in (201, 404, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 422: + raise HTTPError( + "Unprocessable request, perhaps interaction_method is invalid?", response=res + ) + + return res.status_code == 201 + + def delete_user_note( + self, + id: str, + user_id: int, + agent_id: int | None = None, + ) -> bool: + """ + Delete a user note with the specified ID. + + Args: + id: + Identifier of the user note to delete + user_id: + Identifier for the user the note refers to. + agent_id: + Identifier for the agent to whom the note + is attributed, if applicable. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + params = {"user_id": user_id, "agent_id": agent_id} + res = self._del_request("user_notes", id, params=params) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 From d2706be3a48a41972bb1b5f580c9d1e4371c83dd Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 30 Jul 2026 10:22:31 -0400 Subject: [PATCH 49/94] generalize ratelimited_api_connector --- .../ratelimited_api_connector.py | 20 ------------------ .../solidarity_tech/solidarity_tech_base.py | 7 +++++-- .../utilities/ratelimited_api_connector.py | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+), 22 deletions(-) delete mode 100644 parsons/solidarity_tech/ratelimited_api_connector.py create mode 100644 parsons/utilities/ratelimited_api_connector.py diff --git a/parsons/solidarity_tech/ratelimited_api_connector.py b/parsons/solidarity_tech/ratelimited_api_connector.py deleted file mode 100644 index 778c72029d..0000000000 --- a/parsons/solidarity_tech/ratelimited_api_connector.py +++ /dev/null @@ -1,20 +0,0 @@ -import requests -from pyrate_limiter import Duration, Limiter, Rate - -from parsons.utilities.api_connector import APIConnector - -rates = [ - Rate(60, Duration.SECOND * 30), # 60 requests per 30 seconds -] -limiter = Limiter(rates) - - -class RateLimitedAPIConnector(APIConnector): - @limiter.as_decorator(name="api_call", weight=1) - def request( - self, - *args, - **kwargs, - ) -> requests.Response: - """Make a request with pyrate-limiter.""" - return super().request(*args, **kwargs) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index c55a294bb9..40aaa4a355 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -4,9 +4,10 @@ import numpy as np import requests +from pyrate_limiter import Duration, Rate -from parsons.solidarity_tech.ratelimited_api_connector import RateLimitedAPIConnector from parsons.utilities import check_env +from parsons.utilities.ratelimited_api_connector import RateLimitedAPIConnector logger = logging.getLogger(__name__) @@ -25,7 +26,9 @@ def __init__(self, api_token: str | None = None) -> None: self.api_token: str = check_env.check("SOLIDARITY_TECH_TOKEN", api_token) self.headers = {"authorization": f"Bearer {self.api_token}"} self.api_url = "https://api.solidarity.tech/v1/" - self.api = RateLimitedAPIConnector(self.api_url, headers=self.headers) + self.api = RateLimitedAPIConnector( + self.api_url, headers=self.headers, ratelimit=Rate(60, Duration.SECOND * 30) + ) def _get_resources(self, endpoint: str, **kwargs) -> requests.Response: """Process parameters and handle GET requests for lists of resources.""" diff --git a/parsons/utilities/ratelimited_api_connector.py b/parsons/utilities/ratelimited_api_connector.py new file mode 100644 index 0000000000..ef2be71a0c --- /dev/null +++ b/parsons/utilities/ratelimited_api_connector.py @@ -0,0 +1,21 @@ +import requests +from pyrate_limiter import Limiter, Rate + +from parsons.utilities.api_connector import APIConnector + + +class RateLimitedAPIConnector(APIConnector): + """A wrapper around APIConnector that adds rate limiting using pyrate-limiter.""" + + def __init__(self, *args, ratelimit: Rate, **kwargs) -> None: + self.limiter = Limiter(ratelimit) + super().__init__(*args, **kwargs) + + def request( + self, + *args, + **kwargs, + ) -> requests.Response: + """Make a request with pyrate-limiter.""" + self.limiter.try_acquire("api_call") + return super().request(*args, **kwargs) From 449be4a0db4debba3cef80bb96833a72e4da4454 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 30 Jul 2026 10:23:00 -0400 Subject: [PATCH 50/94] improve documentating and typing of _base --- .../solidarity_tech/solidarity_tech_base.py | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index 40aaa4a355..98b1febe4c 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -11,6 +11,8 @@ logger = logging.getLogger(__name__) +ParamTypes = str | int | np.int64 | float | None + class SolidarityTechBase: def __init__(self, api_token: str | None = None) -> None: @@ -31,7 +33,32 @@ def __init__(self, api_token: str | None = None) -> None: ) def _get_resources(self, endpoint: str, **kwargs) -> requests.Response: - """Process parameters and handle GET requests for lists of resources.""" + """ + Process parameters and handle GET requests for lists of resources. + + If provided as keyword args, ``limit``, ``cursor``, ``offset``, ``since``, and ``include_count`` + will be added to params, prefaced with an underscore, and removed from kwargs. + If the ``params`` kwarg contains pairs with a value of None, they will be removed from ``params``. + + Args: + endpoint: + The url request string. + If ``url`` is a relative URL, + it will be joined with the ``uri`` of the :class:`parsons.utilities.APIConnector`. + If ``url`` is an absolute URL, + it will be used as is. + **kwargs: + Additional parameters to pass to :meth:`parsons.utilities.APIConnector.request`. + + Returns: + The response from the API. + + Raises: + KeyError: + If one of the previously-mentioned parameters is provided as + a discrete kwarg AND via the ``params`` kwarg. + + """ since = kwargs.get("since") if isinstance(since, datetime): kwargs["since"] = int(since.timestamp()) @@ -43,7 +70,7 @@ def _get_resources(self, endpoint: str, **kwargs) -> requests.Response: "since": "_since", "include_count": "_include_count", } - params = {} + params: dict[str, ParamTypes] = {} for key, value in param_mapping.items(): if key in kwargs: params[key] = value @@ -70,7 +97,7 @@ def _get_single_resource(self, endpoint: str, id: int, **kwargs) -> requests.Res def _post_request( self, endpoint: str, - payload: Mapping[str, str | int | np.int64 | float | None] | None = None, + payload: Mapping[str, ParamTypes] | None = None, **kwargs, ) -> requests.Response: """Handle POST requests.""" @@ -81,7 +108,7 @@ def _put_request( self, endpoint: str, id: int, - payload: Mapping[str, str | int | np.int64 | float | None] | None = None, + payload: Mapping[str, ParamTypes] | None = None, **kwargs, ) -> requests.Response: """Handle PUT requests.""" From 983b6bb701a817b2af9f474e7803708cdb81b583 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 30 Jul 2026 10:24:01 -0400 Subject: [PATCH 51/94] move pyrate to general dependencies --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cb73c2b43d..45ac82e8dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ classifiers = [ ] dependencies = [ "petl >= 1.7.17", + "pyrate-limiter >= 4.0", "python-dateutil >= 2.0", "requests >= 2.0", "requests-oauthlib >= 1.0", @@ -130,7 +131,6 @@ smtp = ["email-validator >= 2.2"] solidarity-tech = [ "numpy >= 2.1;python_version<'3.14'", "numpy >= 2.4;python_version>='3.14'", - "pyrate-limiter >= 4.0", ] ssh = [ "sshtunnel >= 0.4", From 252844f7bb37163a2f0015a9753f80453557c3ae Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 30 Jul 2026 11:07:01 -0400 Subject: [PATCH 52/94] add user relationships methods --- parsons/solidarity_tech/solidarity_tech.py | 2 + .../solidarity_tech/st_user_relationships.py | 127 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 parsons/solidarity_tech/st_user_relationships.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 6ac3b38a59..e8854503f6 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -31,6 +31,7 @@ from parsons.solidarity_tech.st_user_actions import SolidarityTechUserActions from parsons.solidarity_tech.st_user_lists import SolidarityTechUserLists from parsons.solidarity_tech.st_user_notes import SolidarityTechUserNotes +from parsons.solidarity_tech.st_user_relationships import SolidarityTechUserRelationships logger = logging.getLogger(__name__) @@ -67,5 +68,6 @@ class SolidarityTech( SolidarityTechUserActions, SolidarityTechUserLists, SolidarityTechUserNotes, + SolidarityTechUserRelationships, ): pass diff --git a/parsons/solidarity_tech/st_user_relationships.py b/parsons/solidarity_tech/st_user_relationships.py new file mode 100644 index 0000000000..a94a6c1996 --- /dev/null +++ b/parsons/solidarity_tech/st_user_relationships.py @@ -0,0 +1,127 @@ +import logging +import numbers + +from requests.exceptions import HTTPError + +from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +CompareValueType = str | numbers.Rational | bool +QueryParamType = dict[ + str, str | bool | list[dict[str, CompareValueType | list[dict[str, CompareValueType]]]] +] + +logger = logging.getLogger(__name__) + + +class SolidarityTechUserRelationships(SolidarityTechBase): + def get_user_relationships( + self, + user_id: int, + ) -> str: + """ + Retrieve a list of user relationships. + + Args: + user_id: + ID of the user to retrieve relationships for. + + Returns: + All the user relationships. + + Documentation Reference: + ``__ + + """ + params = {"user_id": user_id} + res = self._get_resources( + "user_relationships", + params=params, + additional_headers={"accept": "application/json"}, + ) + + if res.status_code != 200: + raise STUnexpectedResponseCodeError(res) + + return res.text + + def create_user_relationship( + self, + user_id: int, + related_user_id: int, + relationship_type: str, + ) -> bool: + """ + Create a user relationship between users of the specified type. + + Args: + user_id: + Identifier for the user. + related_user_id: + Identifier for the related user. + relationship_type: + Type of the relationship. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + HTTPError: If the operation fails with a 422 status code. + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + params = { + "user_id": user_id, + "related_user_id": related_user_id, + "relationship_type": relationship_type, + } + res = self._post_request("user_relationships", params=params) + + if res.status_code not in (201, 422): + raise STUnexpectedResponseCodeError(res) + + if res.status_code == 422: + raise HTTPError("Invalid request", response=res) + + return res.status_code == 201 + + def delete_user_relationship( + self, + id: int, + user_id: int, + ) -> bool: + """ + Delete a user relationship. + + Args: + id: + Identifier of the user relationship to delete. + user_id: + Identifier for the user. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Raises: + STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + + Documentation Reference: + ``__ + + """ + params = {"user_id": user_id} + res = self._del_request( + "user_relationships", + id, + params=params, + ) + + if res.status_code not in (200, 404): + raise STUnexpectedResponseCodeError(res) + + return res.status_code == 200 From b8e06e655bb78584419d4edf2fdd79616845f874 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sat, 1 Aug 2026 19:05:15 -0400 Subject: [PATCH 53/94] Cast self.api_token as str to avoid warning --- parsons/solidarity_tech/solidarity_tech_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index 98b1febe4c..7ec773923e 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -1,6 +1,7 @@ import logging from collections.abc import Mapping from datetime import datetime +from typing import cast import numpy as np import requests @@ -25,7 +26,7 @@ def __init__(self, api_token: str | None = None) -> None: Not required if the `SOLIDARITY_TECH_TOKEN` env variable is set. """ - self.api_token: str = check_env.check("SOLIDARITY_TECH_TOKEN", api_token) + self.api_token = cast("str", check_env.check("SOLIDARITY_TECH_TOKEN", api_token)) self.headers = {"authorization": f"Bearer {self.api_token}"} self.api_url = "https://api.solidarity.tech/v1/" self.api = RateLimitedAPIConnector( From 9c3d7208ac51d1ecd10eb751e2b27e3bc93b998e Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sat, 1 Aug 2026 20:11:02 -0400 Subject: [PATCH 54/94] Consolidate status code checks/error handling --- parsons/solidarity_tech/exceptions.py | 25 +++- parsons/solidarity_tech/solidarity_tech.py | 2 + .../solidarity_tech/solidarity_tech_base.py | 74 ++++++++++- parsons/solidarity_tech/st_activities.py | 12 +- .../solidarity_tech/st_agent_assignments.py | 69 ++++++----- .../st_automation_enrollments.py | 24 ++-- parsons/solidarity_tech/st_calls.py | 12 +- .../st_chapter_phone_numbers.py | 12 +- parsons/solidarity_tech/st_chapters.py | 12 +- .../st_custom_user_properties.py | 79 ++++++------ .../solidarity_tech/st_donation_charges.py | 25 ++-- parsons/solidarity_tech/st_email_blasts.py | 28 +++-- parsons/solidarity_tech/st_email_senders.py | 9 +- parsons/solidarity_tech/st_emails.py | 26 ++-- .../solidarity_tech/st_event_attendances.py | 41 +++--- parsons/solidarity_tech/st_event_rsvps.py | 67 +++++----- parsons/solidarity_tech/st_event_sessions.py | 117 ++++++++++-------- parsons/solidarity_tech/st_events.py | 55 ++++---- .../solidarity_tech/st_field_survey_urls.py | 28 ++--- parsons/solidarity_tech/st_organizations.py | 29 ++--- parsons/solidarity_tech/st_pages.py | 32 +++-- parsons/solidarity_tech/st_phonebanks.py | 37 +++--- parsons/solidarity_tech/st_scheduled_calls.py | 29 ++--- parsons/solidarity_tech/st_scheduled_tasks.py | 70 ++++++----- parsons/solidarity_tech/st_task_agents.py | 49 ++++---- .../solidarity_tech/st_task_assignments.py | 65 +++++----- parsons/solidarity_tech/st_team_members.py | 41 +++--- parsons/solidarity_tech/st_text_blasts.py | 23 ++-- parsons/solidarity_tech/st_text_templates.py | 68 +++++----- parsons/solidarity_tech/st_textbanks.py | 28 +++-- parsons/solidarity_tech/st_texts.py | 22 ++-- parsons/solidarity_tech/st_user_actions.py | 41 +++--- parsons/solidarity_tech/st_user_lists.py | 75 ++++++----- parsons/solidarity_tech/st_user_notes.py | 42 +++---- .../solidarity_tech/st_user_relationships.py | 49 ++++---- 35 files changed, 787 insertions(+), 630 deletions(-) diff --git a/parsons/solidarity_tech/exceptions.py b/parsons/solidarity_tech/exceptions.py index e6d6ddcfc0..69f64607f5 100644 --- a/parsons/solidarity_tech/exceptions.py +++ b/parsons/solidarity_tech/exceptions.py @@ -2,12 +2,25 @@ from requests.exceptions import HTTPError -class STUnexpectedResponseCodeError(HTTPError): +class STUnexpectedResponseError(HTTPError): """Status code is not expected.""" - def __init__(self, res: requests.Response) -> None: - super() - self.status_code = res.status_code + def __init__(self, message: str | None = None, *, response: requests.Response) -> None: + err_msg = "Unexpected Response" + if response and response.status_code: + err_msg += f" (Status Code {response.status_code})" + if message: + err_msg += f" -- {message}" + super().__init__(err_msg) - def __str__(self): - return f"Received unexpected response. (Status Code: {self.status_code})" + +class STFailedResponseError(HTTPError): + """Status code indicates a known failure.""" + + def __init__(self, message: str, *, response: requests.Response) -> None: + err_msg = "Request Failed" + if response and response.status_code: + err_msg += f" (Status Code {response.status_code})" + if message: + err_msg += f" -- {message}" + super().__init__(err_msg) diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index e8854503f6..b2fbf07370 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -32,6 +32,7 @@ from parsons.solidarity_tech.st_user_lists import SolidarityTechUserLists from parsons.solidarity_tech.st_user_notes import SolidarityTechUserNotes from parsons.solidarity_tech.st_user_relationships import SolidarityTechUserRelationships +from parsons.solidarity_tech.st_users import SolidarityTechUsers logger = logging.getLogger(__name__) @@ -69,5 +70,6 @@ class SolidarityTech( SolidarityTechUserLists, SolidarityTechUserNotes, SolidarityTechUserRelationships, + SolidarityTechUsers, ): pass diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index 7ec773923e..559a0b289b 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -7,6 +7,7 @@ import requests from pyrate_limiter import Duration, Rate +from parsons.solidarity_tech.exceptions import STFailedResponseError, STUnexpectedResponseError from parsons.utilities import check_env from parsons.utilities.ratelimited_api_connector import RateLimitedAPIConnector @@ -51,14 +52,14 @@ def _get_resources(self, endpoint: str, **kwargs) -> requests.Response: **kwargs: Additional parameters to pass to :meth:`parsons.utilities.APIConnector.request`. - Returns: - The response from the API. - Raises: KeyError: If one of the previously-mentioned parameters is provided as a discrete kwarg AND via the ``params`` kwarg. + Returns: + The response from the API. + """ since = kwargs.get("since") if isinstance(since, datetime): @@ -122,3 +123,70 @@ def _del_request(self, endpoint: str, id: int | str, **kwargs) -> requests.Respo complete_endpoint = f"{endpoint}/{id}" logger.debug("Processing DEL request at endpoint: %s", complete_endpoint) return self.api.request(url=complete_endpoint, req_type="DEL", **kwargs) + + def _handle_status_codes( + self, res: requests.Response, codes: dict[int, tuple[bool, str]] + ) -> bool: + """ + Handle status codes. + + Args: + res: The response object. + codes: Expected status codes and their corresponding pass/fail status and descriptive messages. + + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + + Returns: + bool: True if the status code indicates success, False otherwise. + + """ + if res.status_code in codes: + status_code = codes[res.status_code][0] + result_message = codes[res.status_code][1] + if status_code is True: + logger.debug(result_message, extra={"status_code": res.status_code}) + return status_code + raise STFailedResponseError(result_message, response=res) + + raise STUnexpectedResponseError(response=res) + + def _add_if_field_not_empty( + self, receiving_dict: dict, key: str, value, overwrite: bool = False + ) -> dict: + """ + Add a key/value pair to a dictionary if the value is not None. + + Args: + receiving_dict: + The dictionary to add the key/value pair to. + key: + The key to add. + value: + The value to add. + overwrite: + Whether to overwrite the value if the key already exists. + + Raises: + KeyError: If the key already exists in the dictionary and overwrite is not True. + + Returns: + The updated dictionary. + + """ + if overwrite is not True and key in receiving_dict: + err_msg = f"'{key}' already exists." + raise KeyError(err_msg) + if value: + receiving_dict[key] = value + logger.debug( + "Added '%s' with value '%s' to payload or parameters dictionary", key, value + ) + else: + logger.debug( + "Skipping adding '%s' to payload or parameters dictionary as value is None", + key, + value, + ) + return receiving_dict diff --git a/parsons/solidarity_tech/st_activities.py b/parsons/solidarity_tech/st_activities.py index b7869346c1..f43b07d97c 100644 --- a/parsons/solidarity_tech/st_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -1,7 +1,6 @@ import logging from datetime import datetime -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -37,12 +36,13 @@ def get_activities( user_id: User ID to filter activities for a specific user. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the activities entries. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -58,7 +58,7 @@ def get_activities( additional_headers={"accept": "application/json"}, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "successful")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index 7b852f6f21..73cde10bd7 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -3,7 +3,6 @@ import numpy as np -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -34,6 +33,10 @@ def get_agent_assignments( agent_user_id: Agent User ID to filter agent user assignments related to a specific agent user. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the agent assignment entries. @@ -50,8 +53,8 @@ def get_agent_assignments( params=params, ) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "successful")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -66,20 +69,24 @@ def get_agent_assignment( id: ID of the agent assignment to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single agent assignment entry. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("agent_assignments", id) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = { + 200: (True, "agent assignment found"), + 404: (False, "agent assignment not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -100,13 +107,14 @@ def create_agent_assignment( is_active: Whether the assignment is currently active. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -118,10 +126,11 @@ def create_agent_assignment( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (201, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "agent assignment created"), + 404: (False, "agent or user agent not in organization"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def update_agent_assignment( self, @@ -143,13 +152,14 @@ def update_agent_assignment( is_active: Whether the assignment is currently active. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -162,10 +172,12 @@ def update_agent_assignment( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (200, 404, 422): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "agent assignment updated"), + 404: (False, "agent assignment not found"), + 422: (False, "unprocessable entity"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def delete_agent_assignment( self, @@ -178,20 +190,19 @@ def delete_agent_assignment( id: Identifier for the agent assignment to update. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._del_request("agent_assignments", id) - if res and res.status_code != 404: - raise STUnexpectedResponseCodeError(res) - - return not res.status_code + expected_responses = {404: (False, "agent assignment not found")} + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_automation_enrollments.py b/parsons/solidarity_tech/st_automation_enrollments.py index f93d90bdb9..2f371cc487 100644 --- a/parsons/solidarity_tech/st_automation_enrollments.py +++ b/parsons/solidarity_tech/st_automation_enrollments.py @@ -1,8 +1,5 @@ import logging -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -23,14 +20,14 @@ def enroll_user_in_automation( user_id: The ID of the user to enroll. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: Operation failed because automation is inactive. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -42,10 +39,9 @@ def enroll_user_in_automation( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (201, 403, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 422: - raise HTTPError("Automation is inactive", response=res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "enrollment created"), + 403: (False, "automation not accessible"), + 422: (False, "inactive automation"), + } + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_calls.py b/parsons/solidarity_tech/st_calls.py index 3ce6f6b8b2..bc578d9601 100644 --- a/parsons/solidarity_tech/st_calls.py +++ b/parsons/solidarity_tech/st_calls.py @@ -1,7 +1,6 @@ import logging from datetime import datetime -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -29,12 +28,13 @@ def get_calls( since: UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the calls entries. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -49,7 +49,7 @@ def get_calls( additional_headers={"accept": "application/json"}, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "successful")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_chapter_phone_numbers.py b/parsons/solidarity_tech/st_chapter_phone_numbers.py index 689063e203..93d5afb8ee 100644 --- a/parsons/solidarity_tech/st_chapter_phone_numbers.py +++ b/parsons/solidarity_tech/st_chapter_phone_numbers.py @@ -1,7 +1,6 @@ import logging from datetime import datetime -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -29,12 +28,13 @@ def get_chapter_phone_numbers( chapter_id: Filters chapter phone numbers by chapter_id within the accessible scope. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the chapter phone numbers entries. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -48,7 +48,7 @@ def get_chapter_phone_numbers( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "chapter phone numbers listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_chapters.py b/parsons/solidarity_tech/st_chapters.py index ffb666996e..a5fddefc7a 100644 --- a/parsons/solidarity_tech/st_chapters.py +++ b/parsons/solidarity_tech/st_chapters.py @@ -1,7 +1,6 @@ import logging from datetime import datetime -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -26,12 +25,13 @@ def get_chapters( since: UTC timestamp in seconds since the Unix epoch to filter chapters created after this time. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the chapters entries. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -44,7 +44,7 @@ def get_chapters( additional_headers={"accept": "application/json"}, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "successful")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index 1d50b102a7..587d994b01 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -1,9 +1,6 @@ import logging from datetime import datetime -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import FieldType, ScopeType @@ -34,6 +31,10 @@ def get_custom_user_properties( scope_type: Type of the scope to filter custom user properties by. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the custom user properties entries @@ -51,8 +52,8 @@ def get_custom_user_properties( additional_headers={"accept": "application/json"}, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "successful")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -83,14 +84,14 @@ def create_custom_user_property( scope_id: ID of the scope for the property. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: Validation failed. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -109,13 +110,11 @@ def create_custom_user_property( additional_headers={"accept": "application/json", "content-type": "application/json"}, ) - if res.status_code not in (201, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 422: - raise HTTPError("Validation failed", response=res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "created"), + 422: (False, "validation failed"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def delete_custom_user_property_option( self, @@ -131,14 +130,14 @@ def delete_custom_user_property_option( id: Value of the option to remove + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: Validation failed. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -149,16 +148,12 @@ def delete_custom_user_property_option( additional_headers={"accept": "application/json"}, ) - if res.status_code not in (200, 404, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 404: - raise HTTPError("Option or custom user property not found", response=res) - - if res.status_code == 422: - raise HTTPError("Validation failed", response=res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "option removed"), + 404: (False, "option or custom user property not found"), + 422: (False, "validation failed"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def create_custom_user_property_option( self, @@ -178,14 +173,14 @@ def create_custom_user_property_option( value: Internal value for the option (will be auto-generated if not provided) + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: Validation failed. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -200,13 +195,9 @@ def create_custom_user_property_option( additional_headers={"accept": "application/json", "content-type": "application/json"}, ) - if res.status_code not in (201, 404, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 404: - raise HTTPError("Custom user property not found", response=res) - - if res.status_code == 422: - raise HTTPError("Validation failed", response=res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "option created"), + 404: (False, "custom user property not found"), + 422: (False, "validation failed"), + } + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_donation_charges.py b/parsons/solidarity_tech/st_donation_charges.py index b689113f37..26bdcff375 100644 --- a/parsons/solidarity_tech/st_donation_charges.py +++ b/parsons/solidarity_tech/st_donation_charges.py @@ -1,9 +1,6 @@ import logging from datetime import datetime -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -28,6 +25,10 @@ def get_donation_charges( since: UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the donation charges. @@ -43,8 +44,8 @@ def get_donation_charges( additional_headers={"accept": "application/json"}, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "donation charges listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -59,22 +60,20 @@ def get_donation_charge( id: ID of the donation charge to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single agent assignment entry. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("donation_charges", id) - if res.status_code == 404: - raise HTTPError("Donation charge not found", response=res) - - if res.status_code: - raise STUnexpectedResponseCodeError(res) + expected_responses = {404: (False, "donation charge not found")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_email_blasts.py b/parsons/solidarity_tech/st_email_blasts.py index 1b569c906f..78263f9f1b 100644 --- a/parsons/solidarity_tech/st_email_blasts.py +++ b/parsons/solidarity_tech/st_email_blasts.py @@ -1,9 +1,6 @@ import logging from datetime import datetime -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -28,6 +25,10 @@ def get_email_blasts( since: UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the email blasts. @@ -42,8 +43,8 @@ def get_email_blasts( since=since, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "email blasts listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -58,22 +59,23 @@ def get_email_blast( id: ID of the email blast to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single email blast entry. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("email_blasts", id) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 404: - raise HTTPError("Email blast not found", response=res) + expected_responses = { + 200: (True, "email blast found"), + 422: (False, "email blast not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_email_senders.py b/parsons/solidarity_tech/st_email_senders.py index 2c1e529f14..8fd781a62f 100644 --- a/parsons/solidarity_tech/st_email_senders.py +++ b/parsons/solidarity_tech/st_email_senders.py @@ -1,6 +1,5 @@ import logging -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -23,6 +22,10 @@ def get_email_senders( offset: Number of items to skip before starting to return the results. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the email senders. @@ -37,7 +40,7 @@ def get_email_senders( additional_headers={"accept": "application/json"}, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "email senders listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_emails.py b/parsons/solidarity_tech/st_emails.py index 7cdd110e27..e539dbc422 100644 --- a/parsons/solidarity_tech/st_emails.py +++ b/parsons/solidarity_tech/st_emails.py @@ -1,8 +1,5 @@ import logging -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -45,14 +42,14 @@ def send_one_off_email( track_clicks: Enable click tracking. Default is True. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: Missing required parameters. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -68,16 +65,13 @@ def send_one_off_email( "track_opens": track_opens, "track_clicks": track_clicks, } - res = self._post_request("emails", params=email_params) - if res.status_code not in (201, 404, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 404: - raise HTTPError("User not found", response=res) - - if res.status_code == 422: - raise HTTPError("Missing required parameters", response=res) + expected_responses = { + 201: (True, "email sent successfully"), + 404: (False, "user not found"), + 422: (False, "missing required parameters"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index 5e3f2ff4df..a77e7262b7 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -3,7 +3,6 @@ import numpy as np -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -34,6 +33,10 @@ def get_event_attendances( session_id: Filters attendances by session_id (calendar item id) within the accessible scope. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the event attendance entries. @@ -50,8 +53,8 @@ def get_event_attendances( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "event attendances listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -75,13 +78,14 @@ def create_event_attendance( attended: Indicates if the user attended the event. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -98,10 +102,11 @@ def create_event_attendance( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (201, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "event attendance created"), + 404: (False, "event not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def delete_event_attendance( self, @@ -114,13 +119,14 @@ def delete_event_attendance( id: Identifier of the event attendance to delete + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -130,7 +136,8 @@ def delete_event_attendance( id, ) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "event attendance deleted"), + 404: (False, "event attendance not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index 6f8566194f..f281f5cd5e 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -3,7 +3,6 @@ import numpy as np -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import AttendanceType @@ -41,6 +40,10 @@ def get_event_rsvps( full_user_payload: If True, includes complete user data in the response instead of just basic details. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the event rsvps. @@ -62,8 +65,8 @@ def get_event_rsvps( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "event rsvps listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -81,12 +84,13 @@ def get_event_rsvp( full_user_payload: If True, includes complete user data in the response instead of just basic details. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single event rsvp. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -94,8 +98,11 @@ def get_event_rsvp( params = {"full_user_payload": full_user_payload} res = self._get_single_resource("event_rsvps", id, params=params) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = { + 200: (True, "event rsvp found"), + 404: (False, "event rsvp not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -134,13 +141,14 @@ def create_event_rsvp( skip_email_confirmation: If True, skips sending the initial email confirmation to the user. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -160,10 +168,11 @@ def create_event_rsvp( "event_rsvps", payload=payload, additional_headers={"content-type": "application/json"} ) - if res.status_code not in (201, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "event rsvp created"), + 404: (False, "event not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def update_event_rsvp( self, @@ -191,14 +200,14 @@ def update_event_rsvp( source_system: System from which the RSVP originated. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -217,10 +226,11 @@ def update_event_rsvp( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "event rsvp updated"), + 404: (False, "event rsvp not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def delete_event_rsvp( self, @@ -233,13 +243,14 @@ def delete_event_rsvp( id: Identifier of the event rsvp to delete + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -249,7 +260,5 @@ def delete_event_rsvp( id, ) - if res and res.status_code != 404: - raise STUnexpectedResponseCodeError(res) - - return not res.status_code + expected_responses = {404: (False, "event rsvp not found")} + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index 6c8ce80c66..0caacbf325 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -2,9 +2,7 @@ from datetime import datetime import numpy as np -from requests.exceptions import HTTPError -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import EventType @@ -22,7 +20,7 @@ def get_event_sessions( starts_after: int | datetime | None = None, starts_before: int | datetime | None = None, chapter_id: int | None = None, - event_tags: str | None = None, + event_tags: list[str] | str | None = None, include_rsvp_counts: bool | None = None, include_confirmed_counts: bool | None = None, include_hosts: bool | None = None, @@ -69,6 +67,10 @@ def get_event_sessions( If True, returns {"count": n} of matching sessions instead of the rows. Combines with all other filters. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the event sessions. @@ -76,6 +78,9 @@ def get_event_sessions( ``__ """ + if isinstance(event_tags, list): + event_tags = ",".join(str(tag) for tag in event_tags) + params = { "event_id": event_id, "upcoming": upcoming, @@ -96,8 +101,8 @@ def get_event_sessions( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "filtered event sessions listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -152,14 +157,14 @@ def create_event_sessions( tags: Array of tags for the event session. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: If the entity is not processable. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -183,13 +188,11 @@ def create_event_sessions( "event_rsvps", payload=payload, additional_headers={"content-type": "application/json"} ) - if res.status_code not in (201, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 422: - raise HTTPError("Unprocessable entity", response=res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "event session created"), + 422: (False, "unprocessable entity"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def get_event_session( self, @@ -207,12 +210,13 @@ def get_event_session( {id, first_name, last_name} objects resolved from host_user_ids, in host order. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single event session. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -220,9 +224,11 @@ def get_event_session( params = {"include_hosts": include_hosts} res = self._get_single_resource("event_sessions", id, params=params) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - + expected_responses = { + 200: (True, "event session found"), + 404: (False, "event session not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text def update_event_session( @@ -272,14 +278,14 @@ def update_event_session( tags: List of tags for the event session. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: Unprocessable Entity (422) error. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -304,13 +310,12 @@ def update_event_session( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (200, 404, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 422: - raise HTTPError("Unprocessable entity", response=res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "event session updated"), + 404: (False, "event session not found"), + 422: (False, "unprocessable entity"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def delete_event_session( self, @@ -323,13 +328,14 @@ def delete_event_session( id: Identifier of the event session to delete + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -339,10 +345,10 @@ def delete_event_session( id, ) - if res and res.status_code != 404: - raise STUnexpectedResponseCodeError(res) - - return not res.status_code + expected_responses = { + 404: (False, "event session not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def add_event_host( self, @@ -366,13 +372,14 @@ def add_event_host( user_id: ID of the user to add as a host. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -386,10 +393,11 @@ def add_event_host( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "host added"), + 404: (False, "user or event session not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def remove_event_host( self, @@ -410,13 +418,14 @@ def remove_event_host( user_id: ID of the user to remove from hosts. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -426,7 +435,7 @@ def remove_event_host( f"{id}/hosts/{user_id}", ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "host removed"), + } + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index 97e38b55d2..de1cd54751 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -3,9 +3,7 @@ from typing import Literal import numpy as np -from requests.exceptions import HTTPError -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import EventType, ScopeType @@ -46,6 +44,10 @@ def get_events( scope_type: Type of the scope to filter events by. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the events. @@ -65,8 +67,8 @@ def get_events( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "events listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -126,16 +128,14 @@ def create_event( skip_duplicate_check: If True, bypasses duplicate event detection. Default is False. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: If the required scope is not found. - HTTPError: If a duplicate event is detected. - HTTPError: If event data validation fails. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -160,20 +160,13 @@ def create_event( res = self._post_request( "events", payload=payload, additional_headers={"content-type": "application/json"} ) - - if res.status_code not in (201, 404, 409, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 404: - raise HTTPError("Event scope not found", response=res) - - if res.status_code == 409: - raise HTTPError("Duplicate event detected", response=res) - - if res.status_code == 422: - raise HTTPError("Data validation error", response=res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "event created"), + 404: (False, "scope not found"), + 409: (False, "duplicate event detected"), + 422: (False, "validation error"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def get_event( self, @@ -197,12 +190,13 @@ def get_event( id: ID of the event to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single event. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -210,7 +204,10 @@ def get_event( params = {"include_hosts": include_hosts} res = self._get_single_resource("event_sessions", id, params=params) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = { + 200: (True, "event found"), + 404: (False, "event not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_field_survey_urls.py b/parsons/solidarity_tech/st_field_survey_urls.py index b76c380e73..8762e1a82e 100644 --- a/parsons/solidarity_tech/st_field_survey_urls.py +++ b/parsons/solidarity_tech/st_field_survey_urls.py @@ -1,8 +1,5 @@ import logging -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -29,15 +26,14 @@ def generate_field_survey_url( page_id: The ID of the action page (field survey). + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: If user, agent, or page are not found. - HTTPError: If required parameters are missing. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -53,13 +49,9 @@ def generate_field_survey_url( additional_headers={"accept": "application/json", "content-type": "application/json"}, ) - if res.status_code not in (200, 404, 409, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 404: - raise HTTPError("User, agent, or page not found", response=res) - - if res.status_code == 422: - raise HTTPError("Required parameters are missing", response=res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "field survey URL generated"), + 404: (False, "user, agent, or page not found"), + 422: (False, "missing required parameters"), + } + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_organizations.py b/parsons/solidarity_tech/st_organizations.py index c6d9db7b4c..1238b1f484 100644 --- a/parsons/solidarity_tech/st_organizations.py +++ b/parsons/solidarity_tech/st_organizations.py @@ -1,9 +1,6 @@ import logging from datetime import datetime -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -28,6 +25,10 @@ def get_organizations( since: UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the organizations. @@ -42,8 +43,8 @@ def get_organizations( since=since, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "organizations listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -58,23 +59,23 @@ def get_organization( id: ID of the organization to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single organization entry. - Raises: - HTTPError: If the organization is not found. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("organizations", id) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 404: - raise HTTPError("Organization not found.", response=res) + expected_responses = { + 200: (True, "organization found"), + 404: (False, "organization not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_pages.py b/parsons/solidarity_tech/st_pages.py index 79426454e5..d0e3394d88 100644 --- a/parsons/solidarity_tech/st_pages.py +++ b/parsons/solidarity_tech/st_pages.py @@ -1,9 +1,6 @@ import logging from datetime import datetime -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -33,6 +30,10 @@ def get_pages( (the next milestone the public progress bar would display for that count). Default is False. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the pages. @@ -49,8 +50,8 @@ def get_pages( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "pages listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -70,23 +71,28 @@ def get_page( (the next milestone the public progress bar would display for that count). Default is False. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single page entry. Raises: - HTTPError: If the page is not found. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + STFailedResponseError: If the page is not found. + STUnexpectedResponseError: If the operation fails with an unexpected status code. Documentation Reference: ``__ """ - res = self._get_single_resource("pages", id) - - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + params = {"include_action_counts": include_action_counts} + res = self._get_single_resource("pages", id, params=params) - if res.status_code == 404: - raise HTTPError("page not found.", response=res) + expected_responses = { + 200: (True, "page found"), + 404: (False, "page not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py index 0dd3e04c3a..1ca3d2fdf8 100644 --- a/parsons/solidarity_tech/st_phonebanks.py +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -1,9 +1,6 @@ import logging from datetime import datetime -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -16,7 +13,7 @@ def get_phonebanks( offset: int = 0, since: int | datetime = 0, event_id: int = 0, - ids: list[int] | None = None, + ids: list[int] | str | None = None, include_stats: bool = False, ) -> str: """ @@ -33,13 +30,18 @@ def get_phonebanks( event_id: Filters phonebanks by event_id within the accessible scope. ids: - Filters to specific phonebank ids. Accepts a comma-separated string (e.g. "12,34"). + Filters to specific phonebank ids. + Accepts a comma-separated string (e.g. "12,34"). include_stats: If True, each phonebank row also includes aggregate funnel numbers ``attempts`` (contact attempts), ``contacted`` (distinct people attempted), and ``reached`` (distinct people on answered calls). Default is False. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the phonebanks. @@ -47,6 +49,9 @@ def get_phonebanks( ``__ """ + if isinstance(ids, list): + ids = ",".join(str(id) for id in ids) + params = {"event_id": event_id, "ids": ids, "include_stats": include_stats} res = self._get_resources( "phonebanks", @@ -56,8 +61,8 @@ def get_phonebanks( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "phonebanks listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -72,23 +77,23 @@ def get_phonebank( id: ID of the phonebank to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single phonebank entry. - Raises: - HTTPError: If the phonebank is not found. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("phonebanks", id) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 404: - raise HTTPError("Phonebank not found.", response=res) + expected_responses = { + 200: (True, "phonebank found"), + 404: (False, "phonebank not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_scheduled_calls.py b/parsons/solidarity_tech/st_scheduled_calls.py index b5e7f686ef..9eb95cb792 100644 --- a/parsons/solidarity_tech/st_scheduled_calls.py +++ b/parsons/solidarity_tech/st_scheduled_calls.py @@ -1,9 +1,6 @@ import logging from datetime import datetime -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -34,6 +31,10 @@ def get_scheduled_calls( agent_user_id: Agent User ID to filter agent user assignments related to a specific agent user. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the scheduled calls. @@ -50,8 +51,8 @@ def get_scheduled_calls( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "scheduled calls listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -66,23 +67,23 @@ def get_scheduled_call( id: ID of the scheduled call to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single scheduled call entry. - Raises: - HTTPError: If the scheduled call is not found. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("scheduled_calls", id) - if res and res.status_code != 404: - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 404: - raise HTTPError("Scheduled call not found.", response=res) + expected_responses = { + 200: (True, "scheduled call found"), + 404: (False, "scheduled call not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py index 4e51ef3942..f0e8d250ef 100644 --- a/parsons/solidarity_tech/st_scheduled_tasks.py +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -3,7 +3,6 @@ import numpy as np -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -34,6 +33,10 @@ def get_scheduled_tasks( agent_user_id: Agent User ID to filter agent user assignments related to a specific agent user. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the scheduled task entries. @@ -50,8 +53,8 @@ def get_scheduled_tasks( params=params, ) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "scheduled tasks listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -66,20 +69,24 @@ def get_scheduled_task( id: ID of the scheduled task to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single scheduled task entry. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("scheduled_tasks", id) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = { + 200: (True, "scheduled task found"), + 404: (False, "scheduled task not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -113,13 +120,14 @@ def create_scheduled_task( marked_as_completed: Indicates if the task has been marked as completed. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -138,10 +146,11 @@ def create_scheduled_task( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (201, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "scheduled task created"), + 404: (False, "agent or user agent not in organization"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def update_scheduled_task( self, @@ -174,14 +183,14 @@ def update_scheduled_task( marked_as_completed: Indicates if the task has been marked as completed. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: If the update could not be processed. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -201,10 +210,12 @@ def update_scheduled_task( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (200, 404, 422): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "scheduled task updated"), + 404: (False, "scheduled task not found"), + 422: (False, "unprocessable entity"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def delete_scheduled_task( self, @@ -217,20 +228,19 @@ def delete_scheduled_task( id: Identifier for the scheduled task to delete. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._del_request("scheduled_tasks", id) - if res and res.status_code != 404: - raise STUnexpectedResponseCodeError(res) - - return not res.status_code + expected_responses = {404: (False, "scheduled task not found")} + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_task_agents.py b/parsons/solidarity_tech/st_task_agents.py index f4baf75212..a3140909da 100644 --- a/parsons/solidarity_tech/st_task_agents.py +++ b/parsons/solidarity_tech/st_task_agents.py @@ -3,7 +3,6 @@ import numpy as np -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -31,6 +30,10 @@ def get_task_agents( task_id: Filters task agents by task within the accessible scope. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the task agent entries. @@ -47,8 +50,8 @@ def get_task_agents( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "task agents listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -63,20 +66,24 @@ def get_task_agent( id: ID of the task agent to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single task agent entry. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("task_agents", id) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = { + 200: (True, "task agent found"), + 404: (False, "task agent not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -94,13 +101,14 @@ def create_task_agent( task_id: Identifier for the task. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -113,10 +121,8 @@ def create_task_agent( "task_agents", payload=payload, additional_headers={"content-type": "application/json"} ) - if res.status_code not in (201, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 201 + expected_responses = {201: (True, "task agent created")} + return self._handle_status_codes(res=res, codes=expected_responses) def delete_task_agent( self, @@ -129,20 +135,19 @@ def delete_task_agent( id: Identifier for the task agent to delete. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._del_request("task_agents", id) - if res and res.status_code != 404: - raise STUnexpectedResponseCodeError(res) - - return not res.status_code + expected_responses = {404: (False, "task agent not found")} + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_task_assignments.py b/parsons/solidarity_tech/st_task_assignments.py index 2ce788edc7..2c0e38862a 100644 --- a/parsons/solidarity_tech/st_task_assignments.py +++ b/parsons/solidarity_tech/st_task_assignments.py @@ -3,7 +3,6 @@ import numpy as np -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -34,6 +33,10 @@ def get_task_assignments( agent_user_id: Filters task assignments by agent user within the accessible scope. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the task assignment entries. @@ -50,8 +53,8 @@ def get_task_assignments( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "task assignments listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -66,20 +69,24 @@ def get_task_assignment( id: ID of the task assignment to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single task assignment entry. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("task_assignments", id) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = { + 200: (True, "task assignment found"), + 404: (False, "task assignment not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -102,13 +109,14 @@ def create_task_assignment( agent_user_id: Identifier for the agent user who will conduct outreach (volunteer or staff member). + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -124,10 +132,8 @@ def create_task_assignment( additional_headers={"content-type": "application/json"}, ) - if res.status_code != 201: - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 201 + expected_responses = {201: (True, "task assignment created")} + return self._handle_status_codes(res=res, codes=expected_responses) def update_task_assignment( self, @@ -143,13 +149,14 @@ def update_task_assignment( agent_user_id: Identifier for the agent user, if applicable. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -164,10 +171,11 @@ def update_task_assignment( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "task assignment updated"), + 404: (False, "event rsvp not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def delete_task_assignment( self, @@ -180,20 +188,19 @@ def delete_task_assignment( id: Identifier of the task assignment to delete. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._del_request("task_assignments", id) - if res and res.status_code != 404: - raise STUnexpectedResponseCodeError(res) - - return not res.status_code + expected_responses = {404: (False, "task assignment not found")} + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py index 293376a116..244dd07ee1 100644 --- a/parsons/solidarity_tech/st_team_members.py +++ b/parsons/solidarity_tech/st_team_members.py @@ -1,9 +1,6 @@ import logging from datetime import datetime -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import InviteType, ScopeType @@ -29,6 +26,10 @@ def get_team_members( since: UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the team member entries. @@ -43,8 +44,8 @@ def get_team_members( since=since, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "team members listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -89,15 +90,15 @@ def create_team_member( task_id: Optional task ID to assign the member to. + Raises: + :class:`ValueError`: If none of ``member_id``, ``phone_number`` or ``email`` is provided. + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - ValueError: If ``member_id``, ``phone_number``, and ``email`` are all None. - HTTPError: If the parameters are invalid. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -122,13 +123,11 @@ def create_team_member( "team_members", payload=payload, additional_headers={"content-type": "application/json"} ) - if res.status_code not in (201, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 422: - raise HTTPError("Invalid parameters", response=res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "team member created"), + 422: (False, "invalid parameters"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def update_team_member( self, @@ -155,7 +154,7 @@ def update_team_member( True if the operation was successful, False otherwise. Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. Documentation Reference: ``__ @@ -173,7 +172,5 @@ def update_team_member( additional_headers={"content-type": "application/json"}, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = {200: (True, "team member updated")} + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_text_blasts.py b/parsons/solidarity_tech/st_text_blasts.py index f133db2f22..0f8288920c 100644 --- a/parsons/solidarity_tech/st_text_blasts.py +++ b/parsons/solidarity_tech/st_text_blasts.py @@ -1,7 +1,6 @@ import logging from datetime import datetime -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -26,6 +25,10 @@ def get_text_blasts( since: UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the text blast entries. @@ -40,8 +43,8 @@ def get_text_blasts( since=since, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "text blasts listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -56,19 +59,23 @@ def get_text_blast( id: ID of the text blast to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single text blast entry. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("text_blasts", id) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = { + 200: (True, "text blast found"), + 404: (False, "text blast not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_text_templates.py b/parsons/solidarity_tech/st_text_templates.py index 3cabd1a936..831496aa5c 100644 --- a/parsons/solidarity_tech/st_text_templates.py +++ b/parsons/solidarity_tech/st_text_templates.py @@ -3,7 +3,6 @@ import numpy as np -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import ScopeType @@ -32,6 +31,10 @@ def get_text_templates( event_id: Filters rsvps by event_id within the accessible scope. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the text templates. @@ -48,8 +51,8 @@ def get_text_templates( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "text templates listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -64,20 +67,24 @@ def get_text_template( id: ID of the text template to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single text template. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("text_templates", id) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = { + 200: (True, "text template found"), + 404: (False, "text template not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -106,13 +113,14 @@ def create_text_template( event_id: Identifier for the associated event, if applicable. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -130,10 +138,11 @@ def create_text_template( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (201, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "text template created"), + 404: (False, "event not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def update_text_template( self, @@ -163,13 +172,14 @@ def update_text_template( event_id: Identifier for the associated event, if applicable. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -188,10 +198,11 @@ def update_text_template( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "text template updated"), + 404: (False, "text template not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def delete_text_template( self, @@ -204,20 +215,19 @@ def delete_text_template( id: Identifier of the text template to delete. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._del_request("text_templates", id) - if res and res.status_code != 404: - raise STUnexpectedResponseCodeError(res) - - return not res.status_code + expected_responses = {404: (False, "text template not found")} + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_textbanks.py b/parsons/solidarity_tech/st_textbanks.py index 7547b79e92..d40ebb0b0f 100644 --- a/parsons/solidarity_tech/st_textbanks.py +++ b/parsons/solidarity_tech/st_textbanks.py @@ -1,7 +1,6 @@ import logging from datetime import datetime -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -14,7 +13,7 @@ def get_textbanks( offset: int = 0, since: int | datetime = 0, event_id: int = 0, - ids: str | None = None, + ids: list[int] | str | None = None, include_stats: bool = False, ) -> str: """ @@ -39,6 +38,10 @@ def get_textbanks( and ``replies`` (distinct attempts that got a response). Default is False. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the textbanks. @@ -46,6 +49,9 @@ def get_textbanks( ``__ """ + if isinstance(ids, list): + ids = ",".join(str(id) for id in ids) + params = {"event_id": event_id, "ids": ids, "include_stats": include_stats} res = self._get_resources( "textbanks", @@ -55,8 +61,8 @@ def get_textbanks( params=params, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "textbanks listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -71,19 +77,23 @@ def get_textbank( id: ID of the textbank to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single textbank. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("textbanks", id) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = { + 200: (True, "textbank found"), + 404: (False, "textbank not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text diff --git a/parsons/solidarity_tech/st_texts.py b/parsons/solidarity_tech/st_texts.py index 84aa35ff04..64302f4678 100644 --- a/parsons/solidarity_tech/st_texts.py +++ b/parsons/solidarity_tech/st_texts.py @@ -1,7 +1,6 @@ import logging from datetime import datetime -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -29,6 +28,10 @@ def get_texts( since: UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the texts. @@ -46,8 +49,8 @@ def get_texts( additional_headers={"accept": "application/json"}, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "texts listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -74,13 +77,14 @@ def send_text( shorten_urls: Whether to shorten URLs in the text. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -97,7 +101,5 @@ def send_text( params=params, ) - if res.status_code not in (201, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 201 + expected_responses = {201: (True, "text sent")} + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_user_actions.py b/parsons/solidarity_tech/st_user_actions.py index 135a6f4338..74abeb40f0 100644 --- a/parsons/solidarity_tech/st_user_actions.py +++ b/parsons/solidarity_tech/st_user_actions.py @@ -3,9 +3,7 @@ from typing import Literal import numpy as np -from requests.exceptions import HTTPError -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -54,6 +52,10 @@ def get_user_actions( since: UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the user actions. @@ -75,12 +77,11 @@ def get_user_actions( additional_headers={"accept": "application/json"}, ) - if res.status_code not in (200, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 422: - err_msg = "Could not process request. group_by value may be invalid." - raise HTTPError(err_msg, response=res) + expected_responses = { + 200: (True, "user actions retrieved"), + 422: (False, "unprocessable entity"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -109,15 +110,15 @@ def create_user_action( data: Action data. See documentation. + Raises: + :class:`ValueError`: If none of ``user_id``, ``phone_number`` or ``email`` is provided. + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - ValueError: If neither ``user_id``, ``phone_number``, nor ``email`` is provided. - HTTPError: If the operation fails with a 422 status code. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -142,12 +143,8 @@ def create_user_action( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (201, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 422: - raise HTTPError( - "Request could not be processed, provided data may be invalid", response=res - ) - - return res.status_code == 201 + expected_responses = { + 201: (True, "user action created"), + 422: (False, "unprocessable entity"), + } + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_user_lists.py b/parsons/solidarity_tech/st_user_lists.py index e9d6a9bd7d..a43b9636a2 100644 --- a/parsons/solidarity_tech/st_user_lists.py +++ b/parsons/solidarity_tech/st_user_lists.py @@ -3,9 +3,7 @@ from datetime import datetime import numpy as np -from requests.exceptions import HTTPError -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import ScopeType @@ -36,6 +34,10 @@ def get_user_lists( since: UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the user lists. @@ -50,8 +52,8 @@ def get_user_lists( since=since, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = {200: (True, "user lists listed")} + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -66,20 +68,24 @@ def get_user_list( id: ID of the user list to retrieve. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: A single user list. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ """ res = self._get_single_resource("user_lists", id) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) + expected_responses = { + 200: (True, "user list found"), + 404: (False, "user list not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -112,14 +118,14 @@ def create_user_list( ``parameters``: Parameters for filtering users in QueryBuilder format. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: If the operation fails with a 422 status code. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -136,13 +142,11 @@ def create_user_list( "user_lists", payload=payload, additional_headers={"content-type": "application/json"} ) - if res.status_code not in (201, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 422: - raise HTTPError("Unprocessable request, likely issue with parameters", response=res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "user list created"), + 422: (False, "unprocessable entity"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def update_user_list( self, @@ -173,14 +177,14 @@ def update_user_list( event_id: Identifier for the associated event, if applicable. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -199,10 +203,11 @@ def update_user_list( additional_headers={"content-type": "application/json"}, ) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "user list updated"), + 404: (False, "user list not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def delete_user_list( self, @@ -215,13 +220,14 @@ def delete_user_list( id: Identifier of the user list to delete + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -231,7 +237,8 @@ def delete_user_list( id, ) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "user list deleted"), + 404: (False, "user list not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_user_notes.py b/parsons/solidarity_tech/st_user_notes.py index 9da255a8bd..71ea54a9eb 100644 --- a/parsons/solidarity_tech/st_user_notes.py +++ b/parsons/solidarity_tech/st_user_notes.py @@ -1,8 +1,5 @@ import logging -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import InteractionType @@ -38,14 +35,14 @@ def create_user_note( interaction_method: Interaction type that produced the note. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: If the operation fails with a 422 status code. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -60,15 +57,12 @@ def create_user_note( } res = self._post_request("user_notes", params=params) - if res.status_code not in (201, 404, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 422: - raise HTTPError( - "Unprocessable request, perhaps interaction_method is invalid?", response=res - ) - - return res.status_code == 201 + expected_responses = { + 201: (True, "user note created successfully"), + 404: (False, "unprocessable entity"), + 422: (False, "unprocessable entity"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def delete_user_note( self, @@ -88,13 +82,14 @@ def delete_user_note( Identifier for the agent to whom the note is attributed, if applicable. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -102,7 +97,8 @@ def delete_user_note( params = {"user_id": user_id, "agent_id": agent_id} res = self._del_request("user_notes", id, params=params) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "user note deleted"), + 404: (False, "user note not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_user_relationships.py b/parsons/solidarity_tech/st_user_relationships.py index a94a6c1996..e0ca8253a0 100644 --- a/parsons/solidarity_tech/st_user_relationships.py +++ b/parsons/solidarity_tech/st_user_relationships.py @@ -1,9 +1,6 @@ import logging import numbers -from requests.exceptions import HTTPError - -from parsons.solidarity_tech.exceptions import STUnexpectedResponseCodeError from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase CompareValueType = str | numbers.Rational | bool @@ -26,6 +23,10 @@ def get_user_relationships( user_id: ID of the user to retrieve relationships for. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: All the user relationships. @@ -40,8 +41,10 @@ def get_user_relationships( additional_headers={"accept": "application/json"}, ) - if res.status_code != 200: - raise STUnexpectedResponseCodeError(res) + expected_responses = { + 200: (True, "user relationships listed"), + } + self._handle_status_codes(res=res, codes=expected_responses) return res.text @@ -62,14 +65,14 @@ def create_user_relationship( relationship_type: Type of the relationship. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - HTTPError: If the operation fails with a 422 status code. - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -81,13 +84,11 @@ def create_user_relationship( } res = self._post_request("user_relationships", params=params) - if res.status_code not in (201, 422): - raise STUnexpectedResponseCodeError(res) - - if res.status_code == 422: - raise HTTPError("Invalid request", response=res) - - return res.status_code == 201 + expected_responses = { + 201: (True, "user relationship created"), + 422: (False, "invalid request"), + } + return self._handle_status_codes(res=res, codes=expected_responses) def delete_user_relationship( self, @@ -103,13 +104,14 @@ def delete_user_relationship( user_id: Identifier for the user. + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + Returns: Boolean representing success of the operation. True if the operation was successful, False otherwise. - Raises: - STUnexpectedResponseCodeError: If the operation fails with an unexpected status code. - Documentation Reference: ``__ @@ -121,7 +123,8 @@ def delete_user_relationship( params=params, ) - if res.status_code not in (200, 404): - raise STUnexpectedResponseCodeError(res) - - return res.status_code == 200 + expected_responses = { + 200: (True, "user relationship deleted"), + 404: (False, "user relationship not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) From cc8a7547b71990ea6d794001c5521ad541f22d0d Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sat, 1 Aug 2026 20:11:34 -0400 Subject: [PATCH 55/94] add users methods --- parsons/solidarity_tech/st_users.py | 503 ++++++++++++++++++++++++++++ 1 file changed, 503 insertions(+) create mode 100644 parsons/solidarity_tech/st_users.py diff --git a/parsons/solidarity_tech/st_users.py b/parsons/solidarity_tech/st_users.py new file mode 100644 index 0000000000..366c84013f --- /dev/null +++ b/parsons/solidarity_tech/st_users.py @@ -0,0 +1,503 @@ +import logging +import numbers +from datetime import datetime +from zoneinfo import ZoneInfo + +from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase + +CompareValueType = str | numbers.Rational | bool +QueryParamType = dict[ + str, str | bool | list[dict[str, CompareValueType | list[dict[str, CompareValueType]]]] +] + +logger = logging.getLogger(__name__) + + +class SolidarityTechUsers(SolidarityTechBase): + def get_users( + self, + limit: int = 20, + offset: int = 0, + since: int | datetime = 0, + user_list_ids: str | list[int] | None = None, + phone_number: str | None = None, + email: str | None = None, + ) -> str: + """ + Retrieve a list of users. + + Args: + limit: + Limits the number of items returned. + Default is 20, maximum is 100. + offset: + Number of items to skip before starting to return the results. + since: + UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + user_list_ids: + Comma-separated list of user list IDs to apply. + Or a list of user ID integers. + phone_number: + Filter by phone number (any format accepted, will be normalized). + email: + Filter by email address (case-insensitive). + + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + + Returns: + All the users. + + Documentation Reference: + ``__ + + """ + if isinstance(user_list_ids, list): + user_list_ids = ",".join(str(id) for id in user_list_ids) + + params = { + "user_list_ids": user_list_ids, + "phone_number": phone_number, + "email": email, + } + res = self._get_resources( + "users", + limit=limit, + offset=offset, + since=since, + params=params, + additional_headers={"accept": "application/json"}, + ) + + expected_responses = { + 200: (True, "users listed"), + 422: (False, "invalid user filter"), + } + self._handle_status_codes(res=res, codes=expected_responses) + + return res.text + + def get_user( + self, + id: int, + ) -> str: + """ + Retrieve a single user. + + Args: + id: + ID of the user to retrieve. + + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + + Returns: + A single user. + + Documentation Reference: + ``__ + + """ + res = self._get_single_resource( + "users", id, additional_headers={"accept": "application/json"} + ) + + expected_responses = { + 200: (True, "user found"), + 404: (False, "user not found"), + } + self._handle_status_codes(res=res, codes=expected_responses) + + return res.text + + def create_user( + self, + phone_number: str | None = None, + email: str | None = None, + first_name: str | None = None, + last_name: str | None = None, + alternate_name: str | None = None, + preferred_language: str | None = None, + second_language: str | None = None, + chapter_id: int | None = None, + chapter_ids: list[int] | None = None, + referred_by_user_id: int | None = None, + custom_user_properties: dict[str, str | list[str]] | None = None, + append_custom_user_properties: bool = True, + add_tags: list[str] | None = None, + remove_tags: list[str] | None = None, + donation_charge: dict[str, numbers.Number | str] | None = None, + address: dict[str, str | float] | None = None, + assessment: str | None = None, + sms_permission: bool | None = None, + call_permission: bool | None = None, + email_permission: bool | None = None, + timezone: ZoneInfo | str | None = None, + require_contact_info: bool = True, + phone_number_textable_validation: bool = True, + lookup_key: str | None = None, + ) -> bool: + """ + Create or update a user with the specified details. + + Args: + phone_number: + Phone number of the user. + email: + Email address of the user. + first_name: + First name of the user. + last_name: + Last name of the user. + alternate_name: + Alternate name (nickname, community name, or alternate romanization). Searchable. + Blank values are ignored; an existing alternate name cannot be cleared via the API. + preferred_language: + Preferred language of the user. + second_language: + Second language of the user. + chapter_id: + Primary chapter ID. + Required for new users unless ``chapter_ids`` is provided. + chapter_ids: + Array of chapter IDs for multi-chapter membership. + First element becomes primary if ``chapter_id`` is not provided. + Requires multi-chapter feature for more than one chapter. + referred_by_user_id: + Identifier for the user who referred this user. + custom_user_properties: + Custom property values keyed by ``internal_name``. + Use a string for single-value fields + (text, number, radio, dropdown, single checkbox). + Use an array of strings for Multiple Checkboxes fields + (e.g. ``["Option A", "Option B"]``). + Comma-separated strings are also accepted for Multiple Checkboxes + (e.g. ``"Option A, Option B"``). + For Multiple Checkboxes, see ``append_custom_user_properties`` to + control whether values are merged with or replace existing values. + append_custom_user_properties: + Controls how Multiple Checkboxes custom properties are written. + Defaults to True (union new values with existing values, the long-standing API behavior). + Set to False to overwrite existing values, mirroring bulk update REPLACE mode. + Has no effect on non-array field types. + add_tags: + List of tags to add to the user. + remove_tags: + List of tags to remove to the user. + donation_charge: + Optional external donation charge to create. + address: + Optional address to create. + We will attempt to geocode the address if ``latitude`` and ``longitude`` are not provided. + assessment: + Assessment status key to set on the user (maps to classification). + sms_permission: + SMS permission status. + call_permission: + Call permission status. + email_permission: + Email permission status. + timezone: + IANA timezone identifier (e.g., "America/New_York", "Europe/London"). + require_contact_info: + Whether to require phone_number or email for user creation. + Defaults to True. + phone_number_textable_validation: + Whether to validate that phone number is textable. + Defaults to True. + lookup_key: + Custom property key (internal_name) to use for user lookup/deduplication. + Value is read from ``custom_user_properties[lookup_key]``. + Allows matching existing users by external IDs stored in custom properties. + + Raises: + :class:`ValueError`: If neither ``phone_number`` nor ``email`` is provided. + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Documentation Reference: + ``__ + + """ + if not phone_number and not email: + raise ValueError("Either phone_number or email must be provided") + + if isinstance(timezone, ZoneInfo): + timezone = str(timezone.key) + + payload = { + "phone_number": phone_number, + "email": email, + "first_name": first_name, + "last_name": last_name, + "alternate_name": alternate_name, + "preferred_language": preferred_language, + "second_language": second_language, + "chapter_id": chapter_id, + "chapter_ids": chapter_ids, + "referred_by_user_id": referred_by_user_id, + "custom_user_properties": custom_user_properties, + "append_custom_user_properties": append_custom_user_properties, + "add_tags": add_tags, + "remove_tags": remove_tags, + "donation_charge": donation_charge, + "address": address, + "assessment": assessment, + "sms_permission": sms_permission, + "call_permission": call_permission, + "email_permission": email_permission, + "timezone": timezone, + "require_contact_info": require_contact_info, + "phone_number_textable_validation": phone_number_textable_validation, + "lookup_key": lookup_key, + } + res = self._post_request( + "users", payload=payload, additional_headers={"content-type": "application/json"} + ) + + expected_responses = { + 200: (True, "user updated via lookup_key - existing user found"), + 201: (True, "user created with lookup_key - new user"), + 403: (False, "multi-chapter feature not enabled"), + 422: (False, "provided lookup_key without value in custom_user_properties"), + } + return self._handle_status_codes(res=res, codes=expected_responses) + + def update_user( + self, + id: int, + phone_number: str | None = None, + clear_phone_number: bool | None = None, + email: str | None = None, + first_name: str | None = None, + last_name: str | None = None, + alternate_name: str | None = None, + preferred_language: str | None = None, + chapter_id: int | None = None, + chapter_ids: list[int] | None = None, + add_chapter_ids: list[int] | None = None, + remove_chapter_ids: list[int] | None = None, + set_exclusive_chapter: bool | None = None, + second_language: str | None = None, + referred_by_user_id: int | None = None, + custom_user_properties: dict[str, str | list[str]] | None = None, + append_custom_user_properties: bool = True, + address: dict[str, str | float] | None = None, + assessment: str | None = None, + sms_permission: bool | None = None, + call_permission: bool | None = None, + email_permission: bool | None = None, + timezone: ZoneInfo | str | None = None, + donation_charge: dict[str, numbers.Number | str] | None = None, + ) -> bool: + """ + Update a user with the specified details. + + Args: + id: + Identifier of the user to update. + phone_number: + Phone number of the user. + clear_phone_number: + If True, clears the user's primary phone number + (and removes it from ``other_phone_numbers``). + Blank ``phone_number`` values are always ignored, so this + explicit flag is the only way to clear a phone number via the API. + Cannot be combined with a non-blank ``phone_number`` + in the same request (returns 422). + email: + Email of the user. + first_name: + First name of the user. + last_name: + Last name of the user. + alternate_name: + Alternate name (nickname, community name, or alternate romanization). Searchable. + Blank values are ignored; an existing alternate name cannot be cleared via the API. + preferred_language: + Preferred language of the user. + chapter_id: + Primary chapter ID (backwards compatible). + chapter_ids: + Full array of chapter IDs (replaces all). Requires multi-chapter feature. + add_chapter_ids: + Array of chapter IDs to add. Requires multi-chapter feature. + remove_chapter_ids: + Array of chapter IDs to remove. Requires multi-chapter feature. + set_exclusive_chapter: + When True with ``chapter_id``, + sets that chapter as the only chapter + (removes all other chapter memberships). + second_language: + Second language of the user. + referred_by_user_id: + Identifier of the user who referred this user. + Custom property values keyed by ``internal_name``. + Use a string for single-value fields + (text, number, radio, dropdown, single checkbox). + Use an array of strings for Multiple Checkboxes fields + (e.g. ``["Option A", "Option B"]``). + Comma-separated strings are also accepted for Multiple Checkboxes + (e.g. ``"Option A, Option B"``). + For Multiple Checkboxes, see ``append_custom_user_properties`` to + control whether values are merged with or replace existing values. + append_custom_user_properties: + Controls how Multiple Checkboxes custom properties are written. + Defaults to True (union new values with existing values, the long-standing API behavior). + Set to False to overwrite existing values, mirroring bulk update REPLACE mode. + Has no effect on non-array field types. + address: + Optional address to update. + We will attempt to geocode the address if + latitude and longitude are not provided. + assessment: + Assessment status key to set on the user (maps to classification). + sms_permission: + If True, the user has permission to receive SMS messages. + call_permission: + If True, the user has permission to receive call messages. + email_permission: + If True, the user has permission to receive email messages. + timezone: + IANA timezone identifier (e.g., "America/New_York", "Europe/London"). + donation_charge: + Optional external donation charge to create. + + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Documentation Reference: + ``__ + + """ + if isinstance(timezone, ZoneInfo): + timezone = str(timezone.key) + + payload = { + "phone_number": phone_number, + "clear_phone_number": clear_phone_number, + "email": email, + "first_name": first_name, + "last_name": last_name, + "alternate_name": alternate_name, + "preferred_language": preferred_language, + "chapter_id": chapter_id, + "chapter_ids": chapter_ids, + "add_chapter_ids": add_chapter_ids, + "remove_chapter_ids": remove_chapter_ids, + "set_exclusive_chapter": set_exclusive_chapter, + "second_language": second_language, + "referred_by_user_id": referred_by_user_id, + "custom_user_properties": custom_user_properties, + "append_custom_user_properties": append_custom_user_properties, + "address": address, + "assessment": assessment, + "sms_permission": sms_permission, + "call_permission": call_permission, + "email_permission": email_permission, + "timezone": timezone, + "donation_charge": donation_charge, + } + res = self._put_request( + "users", + id, + payload=payload, + additional_headers={"content-type": "application/json"}, + ) + + expected_responses = { + 200: (True, "user updated"), + 422: (False, "cannot set and clear the phone number in the same request"), + } + return self._handle_status_codes(res=res, codes=expected_responses) + + def merge_duplicate_users( + self, + primary_user_id: int, + user_ids: list[int] | str, + ) -> bool: + """ + Merge two or more users. + + Args: + primary_user_id: + ID of the user to keep (the survivor). + All data from duplicates will be merged into this user. + user_ids: + IDs of the duplicate users to merge into the primary user. + These users will be deactivated after merge. + Also accepts a comma-separated string. + + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Documentation Reference: + ``__ + + """ + if isinstance(user_ids, list): + user_ids = ",".join(str(id) for id in user_ids) + + payload = { + "primary_user_id": primary_user_id, + "user_ids": user_ids, + } + res = self._post_request( + "users/merge", + payload=payload, + additional_headers={"accept": "application/json", "content-type": "application/json"}, + ) + + expected_responses = { + 200: (True, "users merged successfully"), + 404: (False, "user not found"), + 422: (False, "invalid parameters"), + } + return self._handle_status_codes(res=res, codes=expected_responses) + + def delete_user( + self, + id: str, + ) -> bool: + """ + Delete a user with the specified ID. + + Args: + id: + Identifier of the user to delete + + Raises: + :class:`STFailedResponseError`: If the operation fails with a known error code. + :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. + + Returns: + Boolean representing success of the operation. + True if the operation was successful, False otherwise. + + Documentation Reference: + ``__ + + """ + res = self._del_request("users", id, additional_headers={"accept": "application/json"}) + + expected_responses = { + 200: (True, "user deleted"), + 404: (False, "user not found"), + } + return self._handle_status_codes(res=res, codes=expected_responses) From 4116e58e3c12d6d1ff7cf6da4d4d3af18b6e1151 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sat, 1 Aug 2026 21:08:49 -0400 Subject: [PATCH 56/94] include response data when initializing exceptions based on HTTPError --- parsons/solidarity_tech/exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parsons/solidarity_tech/exceptions.py b/parsons/solidarity_tech/exceptions.py index 69f64607f5..e2f1b588af 100644 --- a/parsons/solidarity_tech/exceptions.py +++ b/parsons/solidarity_tech/exceptions.py @@ -11,7 +11,7 @@ def __init__(self, message: str | None = None, *, response: requests.Response) - err_msg += f" (Status Code {response.status_code})" if message: err_msg += f" -- {message}" - super().__init__(err_msg) + super().__init__(err_msg, response=response) class STFailedResponseError(HTTPError): @@ -23,4 +23,4 @@ def __init__(self, message: str, *, response: requests.Response) -> None: err_msg += f" (Status Code {response.status_code})" if message: err_msg += f" -- {message}" - super().__init__(err_msg) + super().__init__(err_msg, response=response) From 57c126ba3a6d3002d184dace14292dd203acd143 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sat, 1 Aug 2026 21:45:34 -0400 Subject: [PATCH 57/94] Improved return typing and data handling --- parsons/solidarity_tech/st_activities.py | 12 +++++- .../solidarity_tech/st_agent_assignments.py | 9 +++-- parsons/solidarity_tech/st_calls.py | 12 +++++- .../st_chapter_phone_numbers.py | 5 ++- parsons/solidarity_tech/st_chapters.py | 11 ++++- .../st_custom_user_properties.py | 40 ++++++++++++------- .../solidarity_tech/st_donation_charges.py | 18 ++++++--- parsons/solidarity_tech/st_email_blasts.py | 9 +++-- parsons/solidarity_tech/st_email_senders.py | 10 ++++- parsons/solidarity_tech/st_emails.py | 10 ++--- .../solidarity_tech/st_event_attendances.py | 5 ++- parsons/solidarity_tech/st_event_rsvps.py | 9 +++-- parsons/solidarity_tech/st_event_sessions.py | 10 +++-- parsons/solidarity_tech/st_events.py | 9 +++-- .../solidarity_tech/st_field_survey_urls.py | 12 ++++-- parsons/solidarity_tech/st_organizations.py | 9 +++-- parsons/solidarity_tech/st_pages.py | 9 +++-- parsons/solidarity_tech/st_phonebanks.py | 9 +++-- parsons/solidarity_tech/st_scheduled_calls.py | 9 +++-- parsons/solidarity_tech/st_scheduled_tasks.py | 9 +++-- parsons/solidarity_tech/st_task_agents.py | 9 +++-- .../solidarity_tech/st_task_assignments.py | 9 +++-- parsons/solidarity_tech/st_team_members.py | 5 ++- parsons/solidarity_tech/st_text_blasts.py | 9 +++-- parsons/solidarity_tech/st_text_templates.py | 9 +++-- parsons/solidarity_tech/st_textbanks.py | 9 +++-- parsons/solidarity_tech/st_texts.py | 5 ++- parsons/solidarity_tech/st_user_actions.py | 5 ++- parsons/solidarity_tech/st_user_lists.py | 9 +++-- .../solidarity_tech/st_user_relationships.py | 12 ++++-- parsons/solidarity_tech/st_users.py | 39 +++++++++++------- 31 files changed, 216 insertions(+), 130 deletions(-) diff --git a/parsons/solidarity_tech/st_activities.py b/parsons/solidarity_tech/st_activities.py index f43b07d97c..963612e28b 100644 --- a/parsons/solidarity_tech/st_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -1,10 +1,15 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) +ActionData = dict[str, int | str] +ActivityData = dict[str, int | str | ActionData] +ActivityMetadata = dict[str, int] + class SolidarityTechActivities(SolidarityTechBase): def get_activities( @@ -14,7 +19,7 @@ def get_activities( since: int | datetime = 0, include_count: bool = False, user_id: int | None = None, - ) -> str: + ) -> tuple[Table, ActivityMetadata]: """ Retrieve a list of activities. @@ -61,4 +66,7 @@ def get_activities( expected_responses = {200: (True, "successful")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + data: list[ActivityData] = res.json()["data"] + meta: ActivityMetadata = res.json()["meta"] + + return Table(data), meta diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index 73cde10bd7..5df9b665c6 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -3,6 +3,7 @@ import numpy as np +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ def get_agent_assignments( since: int | datetime = 0, user_id: int | None = None, agent_user_id: int | None = None, - ) -> str: + ) -> Table: """ Retrieve a list of agent assignments. @@ -56,12 +57,12 @@ def get_agent_assignments( expected_responses = {200: (True, "successful")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_agent_assignment( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single agent assignment. @@ -88,7 +89,7 @@ def get_agent_assignment( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() def create_agent_assignment( self, diff --git a/parsons/solidarity_tech/st_calls.py b/parsons/solidarity_tech/st_calls.py index bc578d9601..e6e2d2ba40 100644 --- a/parsons/solidarity_tech/st_calls.py +++ b/parsons/solidarity_tech/st_calls.py @@ -1,10 +1,15 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) +TranscriptData = dict[str, str | int] +CallData = dict[str, int | str | bool | TranscriptData] +CallMetadata = dict[str, int] + class SolidarityTechCalls(SolidarityTechBase): def get_calls( @@ -13,7 +18,7 @@ def get_calls( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> str: + ) -> tuple[Table, CallMetadata]: """ Retrieve a list of calls. @@ -52,4 +57,7 @@ def get_calls( expected_responses = {200: (True, "successful")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + data: list[CallData] = res.json()["data"] + meta: CallMetadata = res.json()["meta"] + + return Table(data), meta diff --git a/parsons/solidarity_tech/st_chapter_phone_numbers.py b/parsons/solidarity_tech/st_chapter_phone_numbers.py index 93d5afb8ee..15b0301dc2 100644 --- a/parsons/solidarity_tech/st_chapter_phone_numbers.py +++ b/parsons/solidarity_tech/st_chapter_phone_numbers.py @@ -1,6 +1,7 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -13,7 +14,7 @@ def get_chapter_phone_numbers( offset: int = 0, since: int | datetime = 0, chapter_id: int = 0, - ) -> str: + ) -> Table: """ Retrieve a list of chapter phone numbers. @@ -51,4 +52,4 @@ def get_chapter_phone_numbers( expected_responses = {200: (True, "chapter phone numbers listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) diff --git a/parsons/solidarity_tech/st_chapters.py b/parsons/solidarity_tech/st_chapters.py index a5fddefc7a..686c323f4a 100644 --- a/parsons/solidarity_tech/st_chapters.py +++ b/parsons/solidarity_tech/st_chapters.py @@ -1,10 +1,14 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) +ChapterData = dict[str, int | str] +ChapterMetadata = dict[str, int] + class SolidarityTechChapters(SolidarityTechBase): def get_chapters( @@ -12,7 +16,7 @@ def get_chapters( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> str: + ) -> tuple[Table, ChapterMetadata]: """ Retrieve a list of chapters. @@ -47,4 +51,7 @@ def get_chapters( expected_responses = {200: (True, "successful")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + data: list[ChapterData] = res.json()["data"] + meta: ChapterMetadata = res.json()["meta"] + + return Table(data), meta diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index 587d994b01..b03aba3c17 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -1,11 +1,15 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import FieldType, ScopeType logger = logging.getLogger(__name__) +UserPropertyData = dict[str, int | str | list[dict[str, str | dict]]] +UserPropertyMetadata = dict[str, int] + class SolidarityTechCustomUserProperties(SolidarityTechBase): def get_custom_user_properties( @@ -15,7 +19,7 @@ def get_custom_user_properties( since: int | datetime = 0, scope_id: int | None = None, scope_type: ScopeType | None = None, - ) -> str: + ) -> tuple[Table, UserPropertyMetadata]: """ Retrieve a list of custom user properties. @@ -36,7 +40,7 @@ def get_custom_user_properties( :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. Returns: - All the custom user properties entries + All custom user properties entries. Documentation Reference: ``__ @@ -55,7 +59,10 @@ def get_custom_user_properties( expected_responses = {200: (True, "successful")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + data: list[UserPropertyData] = res.json()["data"] + meta: UserPropertyMetadata = res.json()["meta"] + + return Table(data), meta def create_custom_user_property( self, @@ -65,7 +72,7 @@ def create_custom_user_property( options: list[dict[str, str | dict[str, str]]] | None = None, scope_type: ScopeType | None = None, scope_id: int | None = None, - ) -> bool: + ) -> UserPropertyData: """ Create a custom user property. @@ -89,8 +96,7 @@ def create_custom_user_property( :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. Returns: - Boolean representing success of the operation. - True if the operation was successful, False otherwise. + Created custom user property entry. Documentation Reference: ``__ @@ -114,13 +120,15 @@ def create_custom_user_property( 201: (True, "created"), 422: (False, "validation failed"), } - return self._handle_status_codes(res=res, codes=expected_responses) + self._handle_status_codes(res=res, codes=expected_responses) + + return res.json()["data"] def delete_custom_user_property_option( self, custom_user_property_id: int, id: str, - ) -> bool: + ) -> UserPropertyData: """ Remove an option from a custom user property. @@ -135,8 +143,7 @@ def delete_custom_user_property_option( :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. Returns: - Boolean representing success of the operation. - True if the operation was successful, False otherwise. + Custom user property entry, as it exists after deleting the option. Documentation Reference: ``__ @@ -153,14 +160,16 @@ def delete_custom_user_property_option( 404: (False, "option or custom user property not found"), 422: (False, "validation failed"), } - return self._handle_status_codes(res=res, codes=expected_responses) + self._handle_status_codes(res=res, codes=expected_responses) + + return res.json()["data"] def create_custom_user_property_option( self, id: int, label: list[dict[str, str | dict[str, str]]], value: str | None = None, - ) -> bool: + ) -> UserPropertyData: """ Create an option for a custom user property. @@ -178,8 +187,7 @@ def create_custom_user_property_option( :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. Returns: - Boolean representing success of the operation. - True if the operation was successful, False otherwise. + Custom user property entry, as it exists after creating the new option. Documentation Reference: ``__ @@ -200,4 +208,6 @@ def create_custom_user_property_option( 404: (False, "custom user property not found"), 422: (False, "validation failed"), } - return self._handle_status_codes(res=res, codes=expected_responses) + self._handle_status_codes(res=res, codes=expected_responses) + + return res.json()["data"] diff --git a/parsons/solidarity_tech/st_donation_charges.py b/parsons/solidarity_tech/st_donation_charges.py index 26bdcff375..ca1fac2484 100644 --- a/parsons/solidarity_tech/st_donation_charges.py +++ b/parsons/solidarity_tech/st_donation_charges.py @@ -1,10 +1,15 @@ import logging from datetime import datetime +from typing import Any +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) +DonationChargeData = dict[str, int | str | bool | dict | dict[str, Any]] +DonationChargeMetadata = dict[str, int] + class SolidarityTechDonationCharges(SolidarityTechBase): def get_donation_charges( @@ -12,7 +17,7 @@ def get_donation_charges( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> str: + ) -> tuple[Table, DonationChargeMetadata]: """ Retrieve a list of donation charges. @@ -47,12 +52,15 @@ def get_donation_charges( expected_responses = {200: (True, "donation charges listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + data: list[DonationChargeData] = res.json()["data"] + meta: DonationChargeMetadata = res.json()["meta"] + + return Table(data), meta def get_donation_charge( self, id: int, - ) -> str: + ) -> DonationChargeData: """ Retrieve a single donation charge. @@ -65,7 +73,7 @@ def get_donation_charge( :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. Returns: - A single agent assignment entry. + A single donation charge entry. Documentation Reference: ``__ @@ -76,4 +84,4 @@ def get_donation_charge( expected_responses = {404: (False, "donation charge not found")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() diff --git a/parsons/solidarity_tech/st_email_blasts.py b/parsons/solidarity_tech/st_email_blasts.py index 78263f9f1b..193cf8c52f 100644 --- a/parsons/solidarity_tech/st_email_blasts.py +++ b/parsons/solidarity_tech/st_email_blasts.py @@ -1,6 +1,7 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -12,7 +13,7 @@ def get_email_blasts( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> str: + ) -> Table: """ Retrieve a list of email blasts. @@ -46,12 +47,12 @@ def get_email_blasts( expected_responses = {200: (True, "email blasts listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_email_blast( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single email blast. @@ -78,4 +79,4 @@ def get_email_blast( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() diff --git a/parsons/solidarity_tech/st_email_senders.py b/parsons/solidarity_tech/st_email_senders.py index 8fd781a62f..f4eab2d047 100644 --- a/parsons/solidarity_tech/st_email_senders.py +++ b/parsons/solidarity_tech/st_email_senders.py @@ -1,16 +1,20 @@ import logging +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) +EmailSenderData = dict[str, int | str | bool] +EmailSenderMetadata = dict[str, int] + class SolidarityTechEmailSenders(SolidarityTechBase): def get_email_senders( self, limit: int = 20, offset: int = 0, - ) -> str: + ) -> tuple[Table, EmailSenderMetadata]: """ Returns a list of email senders available for the API key's scope. Use these sender IDs when sending emails via the POST /emails endpoint. @@ -43,4 +47,6 @@ def get_email_senders( expected_responses = {200: (True, "email senders listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + data: list[EmailSenderData] = res.json()["data"] + meta: EmailSenderMetadata = res.json()["meta"] + return Table(data), meta diff --git a/parsons/solidarity_tech/st_emails.py b/parsons/solidarity_tech/st_emails.py index e539dbc422..719643fb28 100644 --- a/parsons/solidarity_tech/st_emails.py +++ b/parsons/solidarity_tech/st_emails.py @@ -17,10 +17,10 @@ def send_one_off_email( attachment_urls: list[str] | None = None, track_opens: bool = True, track_clicks: bool = True, - ) -> str: + ) -> bool: """ - Returns a list of email senders available for the API key's scope. - Use these sender IDs when sending emails via the POST /emails endpoint. + Sends a single transactional email to a user. + Supports Liquid templating for personalization (e.g., {{ first_name }}). Args: user_id: @@ -72,6 +72,4 @@ def send_one_off_email( 404: (False, "user not found"), 422: (False, "missing required parameters"), } - self._handle_status_codes(res=res, codes=expected_responses) - - return res.text + return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index a77e7262b7..02eaaedd92 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -3,6 +3,7 @@ import numpy as np +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ def get_event_attendances( since: int | datetime = 0, event_id: int | None = None, session_id: int | None = None, - ) -> str: + ) -> Table: """ Retrieve a list of event attendances. @@ -56,7 +57,7 @@ def get_event_attendances( expected_responses = {200: (True, "event attendances listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def create_event_attendance( self, diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index f281f5cd5e..bbffa0dca9 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -3,6 +3,7 @@ import numpy as np +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import AttendanceType @@ -19,7 +20,7 @@ def get_event_rsvps( session_id: int | None = None, user_id: int | None = None, full_user_payload: bool = False, - ) -> str: + ) -> Table: """ Retrieve a list of event rsvps. @@ -68,13 +69,13 @@ def get_event_rsvps( expected_responses = {200: (True, "event rsvps listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_event_rsvp( self, id: int, full_user_payload: bool = False, - ) -> str: + ) -> dict: """ Retrieve a single event rsvp. @@ -104,7 +105,7 @@ def get_event_rsvp( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() def create_event_rsvp( self, diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index 0caacbf325..b1499f5429 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -3,6 +3,7 @@ import numpy as np +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import EventType @@ -25,7 +26,7 @@ def get_event_sessions( include_confirmed_counts: bool | None = None, include_hosts: bool | None = None, count: bool | None = None, - ) -> str: + ) -> Table: """ Retrieve a list of event rsvps. @@ -104,7 +105,7 @@ def get_event_sessions( expected_responses = {200: (True, "filtered event sessions listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def create_event_sessions( self, @@ -198,7 +199,7 @@ def get_event_session( self, id: int, include_hosts: bool = False, - ) -> str: + ) -> dict: """ Retrieve a single event session. @@ -229,7 +230,8 @@ def get_event_session( 404: (False, "event session not found"), } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + + return res.json() def update_event_session( self, diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index de1cd54751..17d49f21c3 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -4,6 +4,7 @@ import numpy as np +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import EventType, ScopeType @@ -18,7 +19,7 @@ def get_events( since: int | datetime = 0, scope_id: int | None = None, scope_type: ScopeType | None = None, - ) -> str: + ) -> Table: """ Lists events accessible within the given scope. @@ -70,7 +71,7 @@ def get_events( expected_responses = {200: (True, "events listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def create_event( self, @@ -172,7 +173,7 @@ def get_event( self, id: int, include_hosts: bool = False, - ) -> str: + ) -> dict: """ Returns a single event. @@ -210,4 +211,4 @@ def get_event( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() diff --git a/parsons/solidarity_tech/st_field_survey_urls.py b/parsons/solidarity_tech/st_field_survey_urls.py index 8762e1a82e..58a9269119 100644 --- a/parsons/solidarity_tech/st_field_survey_urls.py +++ b/parsons/solidarity_tech/st_field_survey_urls.py @@ -1,9 +1,12 @@ import logging +from typing import Literal from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) +FieldSurveyURL = dict[Literal["url", "expires_at"], str] + class SolidarityTechFieldSurveyURLs(SolidarityTechBase): def generate_field_survey_url( @@ -11,7 +14,7 @@ def generate_field_survey_url( user_id: int, agent_user_id: int, page_id: int, - ) -> bool: + ) -> FieldSurveyURL: """ Generates a field survey URL for the given user, agent, and page. @@ -31,8 +34,7 @@ def generate_field_survey_url( :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. Returns: - Boolean representing success of the operation. - True if the operation was successful, False otherwise. + URL and expiration timestamp. Documentation Reference: ``__ @@ -54,4 +56,6 @@ def generate_field_survey_url( 404: (False, "user, agent, or page not found"), 422: (False, "missing required parameters"), } - return self._handle_status_codes(res=res, codes=expected_responses) + self._handle_status_codes(res=res, codes=expected_responses) + + return res.json() diff --git a/parsons/solidarity_tech/st_organizations.py b/parsons/solidarity_tech/st_organizations.py index 1238b1f484..bc838d1ab3 100644 --- a/parsons/solidarity_tech/st_organizations.py +++ b/parsons/solidarity_tech/st_organizations.py @@ -1,6 +1,7 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -12,7 +13,7 @@ def get_organizations( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> str: + ) -> Table: """ Retrieve a list of organizations. @@ -46,12 +47,12 @@ def get_organizations( expected_responses = {200: (True, "organizations listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_organization( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single organization. @@ -78,4 +79,4 @@ def get_organization( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() diff --git a/parsons/solidarity_tech/st_pages.py b/parsons/solidarity_tech/st_pages.py index d0e3394d88..4982b7b0e6 100644 --- a/parsons/solidarity_tech/st_pages.py +++ b/parsons/solidarity_tech/st_pages.py @@ -1,6 +1,7 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -13,7 +14,7 @@ def get_pages( offset: int = 0, since: int | datetime = 0, include_action_counts: bool = False, - ) -> str: + ) -> Table: """ Retrieve a list of pages. @@ -53,13 +54,13 @@ def get_pages( expected_responses = {200: (True, "pages listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_page( self, id: int, include_action_counts: bool = False, - ) -> str: + ) -> dict: """ Retrieve a single page. @@ -95,4 +96,4 @@ def get_page( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py index 1ca3d2fdf8..66e9b66bfb 100644 --- a/parsons/solidarity_tech/st_phonebanks.py +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -1,6 +1,7 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -15,7 +16,7 @@ def get_phonebanks( event_id: int = 0, ids: list[int] | str | None = None, include_stats: bool = False, - ) -> str: + ) -> Table: """ Retrieve a list of phonebanks. @@ -64,12 +65,12 @@ def get_phonebanks( expected_responses = {200: (True, "phonebanks listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_phonebank( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single phonebank. @@ -96,4 +97,4 @@ def get_phonebank( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() diff --git a/parsons/solidarity_tech/st_scheduled_calls.py b/parsons/solidarity_tech/st_scheduled_calls.py index 9eb95cb792..b36ddc8295 100644 --- a/parsons/solidarity_tech/st_scheduled_calls.py +++ b/parsons/solidarity_tech/st_scheduled_calls.py @@ -1,6 +1,7 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -14,7 +15,7 @@ def get_scheduled_calls( since: int | datetime = 0, user_id: int | None = None, agent_user_id: int | None = None, - ) -> str: + ) -> Table: """ Retrieve a list of scheduled calls. @@ -54,12 +55,12 @@ def get_scheduled_calls( expected_responses = {200: (True, "scheduled calls listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_scheduled_call( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single scheduled call. @@ -86,4 +87,4 @@ def get_scheduled_call( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py index f0e8d250ef..18bbc223cd 100644 --- a/parsons/solidarity_tech/st_scheduled_tasks.py +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -3,6 +3,7 @@ import numpy as np +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ def get_scheduled_tasks( since: int | datetime = 0, user_id: int | None = None, agent_user_id: int | None = None, - ) -> str: + ) -> Table: """ Retrieve a list of scheduled tasks. @@ -56,12 +57,12 @@ def get_scheduled_tasks( expected_responses = {200: (True, "scheduled tasks listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_scheduled_task( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single scheduled task. @@ -88,7 +89,7 @@ def get_scheduled_task( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() def create_scheduled_task( self, diff --git a/parsons/solidarity_tech/st_task_agents.py b/parsons/solidarity_tech/st_task_agents.py index a3140909da..4b030306ff 100644 --- a/parsons/solidarity_tech/st_task_agents.py +++ b/parsons/solidarity_tech/st_task_agents.py @@ -3,6 +3,7 @@ import numpy as np +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -15,7 +16,7 @@ def get_task_agents( offset: int = 0, since: int | datetime = 0, task_id: int | None = None, - ) -> str: + ) -> Table: """ Retrieve a list of task agents. @@ -53,12 +54,12 @@ def get_task_agents( expected_responses = {200: (True, "task agents listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_task_agent( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single task agent. @@ -85,7 +86,7 @@ def get_task_agent( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() def create_task_agent( self, diff --git a/parsons/solidarity_tech/st_task_assignments.py b/parsons/solidarity_tech/st_task_assignments.py index 2c0e38862a..b9880e3a67 100644 --- a/parsons/solidarity_tech/st_task_assignments.py +++ b/parsons/solidarity_tech/st_task_assignments.py @@ -3,6 +3,7 @@ import numpy as np +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ def get_task_assignments( since: int | datetime = 0, task_id: int = 0, agent_user_id: int = 0, - ) -> str: + ) -> Table: """ Retrieve a list of task assignments. @@ -56,12 +57,12 @@ def get_task_assignments( expected_responses = {200: (True, "task assignments listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_task_assignment( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single task assignment. @@ -88,7 +89,7 @@ def get_task_assignment( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() def create_task_assignment( self, diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py index 244dd07ee1..7e5b4a69d0 100644 --- a/parsons/solidarity_tech/st_team_members.py +++ b/parsons/solidarity_tech/st_team_members.py @@ -1,6 +1,7 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import InviteType, ScopeType @@ -13,7 +14,7 @@ def get_team_members( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> str: + ) -> Table: """ Retrieve a list of team members. @@ -47,7 +48,7 @@ def get_team_members( expected_responses = {200: (True, "team members listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def create_team_member( self, diff --git a/parsons/solidarity_tech/st_text_blasts.py b/parsons/solidarity_tech/st_text_blasts.py index 0f8288920c..0a820955d6 100644 --- a/parsons/solidarity_tech/st_text_blasts.py +++ b/parsons/solidarity_tech/st_text_blasts.py @@ -1,6 +1,7 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -12,7 +13,7 @@ def get_text_blasts( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> str: + ) -> Table: """ Retrieve a list of text blasts. @@ -46,12 +47,12 @@ def get_text_blasts( expected_responses = {200: (True, "text blasts listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_text_blast( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single text blast. @@ -78,4 +79,4 @@ def get_text_blast( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() diff --git a/parsons/solidarity_tech/st_text_templates.py b/parsons/solidarity_tech/st_text_templates.py index 831496aa5c..3491f970c4 100644 --- a/parsons/solidarity_tech/st_text_templates.py +++ b/parsons/solidarity_tech/st_text_templates.py @@ -3,6 +3,7 @@ import numpy as np +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import ScopeType @@ -16,7 +17,7 @@ def get_text_templates( offset: int = 0, since: int | datetime = 0, event_id: int = 0, - ) -> str: + ) -> Table: """ Retrieve a list of text templates. @@ -54,12 +55,12 @@ def get_text_templates( expected_responses = {200: (True, "text templates listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_text_template( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single text template. @@ -86,7 +87,7 @@ def get_text_template( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() def create_text_template( self, diff --git a/parsons/solidarity_tech/st_textbanks.py b/parsons/solidarity_tech/st_textbanks.py index d40ebb0b0f..8c5115a3fc 100644 --- a/parsons/solidarity_tech/st_textbanks.py +++ b/parsons/solidarity_tech/st_textbanks.py @@ -1,6 +1,7 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -15,7 +16,7 @@ def get_textbanks( event_id: int = 0, ids: list[int] | str | None = None, include_stats: bool = False, - ) -> str: + ) -> Table: """ Retrieve a list of textbanks. @@ -64,12 +65,12 @@ def get_textbanks( expected_responses = {200: (True, "textbanks listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_textbank( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single textbank. @@ -96,4 +97,4 @@ def get_textbank( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() diff --git a/parsons/solidarity_tech/st_texts.py b/parsons/solidarity_tech/st_texts.py index 64302f4678..3f45b0822e 100644 --- a/parsons/solidarity_tech/st_texts.py +++ b/parsons/solidarity_tech/st_texts.py @@ -1,6 +1,7 @@ import logging from datetime import datetime +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -13,7 +14,7 @@ def get_texts( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> str: + ) -> Table: """ Retrieve a list of texts. @@ -52,7 +53,7 @@ def get_texts( expected_responses = {200: (True, "texts listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def send_text( self, diff --git a/parsons/solidarity_tech/st_user_actions.py b/parsons/solidarity_tech/st_user_actions.py index 74abeb40f0..2798139f67 100644 --- a/parsons/solidarity_tech/st_user_actions.py +++ b/parsons/solidarity_tech/st_user_actions.py @@ -4,6 +4,7 @@ import numpy as np +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase logger = logging.getLogger(__name__) @@ -18,7 +19,7 @@ def get_user_actions( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> str: + ) -> Table: """ Lists user actions (form submissions). @@ -83,7 +84,7 @@ def get_user_actions( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def create_user_action( self, diff --git a/parsons/solidarity_tech/st_user_lists.py b/parsons/solidarity_tech/st_user_lists.py index a43b9636a2..294cf2a374 100644 --- a/parsons/solidarity_tech/st_user_lists.py +++ b/parsons/solidarity_tech/st_user_lists.py @@ -4,6 +4,7 @@ import numpy as np +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_literals import ScopeType @@ -21,7 +22,7 @@ def get_user_lists( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> str: + ) -> Table: """ Retrieve a list of user lists. @@ -55,12 +56,12 @@ def get_user_lists( expected_responses = {200: (True, "user lists listed")} self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return Table(res.json()) def get_user_list( self, id: int, - ) -> str: + ) -> dict: """ Retrieve a single user list. @@ -87,7 +88,7 @@ def get_user_list( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() def create_user_list( self, diff --git a/parsons/solidarity_tech/st_user_relationships.py b/parsons/solidarity_tech/st_user_relationships.py index e0ca8253a0..9a79f87d79 100644 --- a/parsons/solidarity_tech/st_user_relationships.py +++ b/parsons/solidarity_tech/st_user_relationships.py @@ -1,21 +1,24 @@ import logging import numbers +from typing import Literal +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +logger = logging.getLogger(__name__) + CompareValueType = str | numbers.Rational | bool QueryParamType = dict[ str, str | bool | list[dict[str, CompareValueType | list[dict[str, CompareValueType]]]] ] - -logger = logging.getLogger(__name__) +UserRelationshipData = dict[Literal["id", "text"], str] class SolidarityTechUserRelationships(SolidarityTechBase): def get_user_relationships( self, user_id: int, - ) -> str: + ) -> Table: """ Retrieve a list of user relationships. @@ -46,7 +49,8 @@ def get_user_relationships( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + data: list[UserRelationshipData] = res.json() + return Table(data) def create_user_relationship( self, diff --git a/parsons/solidarity_tech/st_users.py b/parsons/solidarity_tech/st_users.py index 366c84013f..6bc1a4ecb6 100644 --- a/parsons/solidarity_tech/st_users.py +++ b/parsons/solidarity_tech/st_users.py @@ -1,16 +1,22 @@ import logging import numbers from datetime import datetime +from typing import Any from zoneinfo import ZoneInfo +from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +logger = logging.getLogger(__name__) + CompareValueType = str | numbers.Rational | bool QueryParamType = dict[ str, str | bool | list[dict[str, CompareValueType | list[dict[str, CompareValueType]]]] ] - -logger = logging.getLogger(__name__) +UserData = dict[str, str | int | list[int] | list[str] | dict[str, Any] | bool] +UserMetadata = dict[str, int] +UserMergeMetadata = dict[str, str | int | list[int]] +UserDeleteMetadata = dict[str, int] class SolidarityTechUsers(SolidarityTechBase): @@ -22,7 +28,7 @@ def get_users( user_list_ids: str | list[int] | None = None, phone_number: str | None = None, email: str | None = None, - ) -> str: + ) -> tuple[Table, UserMetadata]: """ Retrieve a list of users. @@ -76,12 +82,15 @@ def get_users( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + data: list[UserData] = res.json()["data"] + meta: UserMetadata = res.json()["meta"] + + return Table(data), meta def get_user( self, id: int, - ) -> str: + ) -> UserData: """ Retrieve a single user. @@ -110,7 +119,7 @@ def get_user( } self._handle_status_codes(res=res, codes=expected_responses) - return res.text + return res.json() def create_user( self, @@ -426,7 +435,7 @@ def merge_duplicate_users( self, primary_user_id: int, user_ids: list[int] | str, - ) -> bool: + ) -> UserMergeMetadata: """ Merge two or more users. @@ -444,8 +453,7 @@ def merge_duplicate_users( :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. Returns: - Boolean representing success of the operation. - True if the operation was successful, False otherwise. + Data about the merge attempt. Documentation Reference: ``__ @@ -469,12 +477,14 @@ def merge_duplicate_users( 404: (False, "user not found"), 422: (False, "invalid parameters"), } - return self._handle_status_codes(res=res, codes=expected_responses) + self._handle_status_codes(res=res, codes=expected_responses) + + return res.json() def delete_user( self, id: str, - ) -> bool: + ) -> UserDeleteMetadata: """ Delete a user with the specified ID. @@ -487,8 +497,7 @@ def delete_user( :class:`STUnexpectedResponseError`: If the operation fails with an unexpected status code. Returns: - Boolean representing success of the operation. - True if the operation was successful, False otherwise. + Data about the delete operation. Documentation Reference: ``__ @@ -500,4 +509,6 @@ def delete_user( 200: (True, "user deleted"), 404: (False, "user not found"), } - return self._handle_status_codes(res=res, codes=expected_responses) + self._handle_status_codes(res=res, codes=expected_responses) + + return res.json() From 335d44bc8e72ceba05ae54c1bf2105a15386a3cf Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 17:51:12 -0400 Subject: [PATCH 58/94] Revised additional_headers implementation (#55) * Enhance request method to include additional headers Added support for additional headers in the request method. * Implement tests for APIConnector headers Add tests for APIConnector initialization and header handling. * Remove unused imports in test_api_connector.py * Remove unused import and add Mocker type hints for requests_mock --- parsons/utilities/api_connector.py | 24 ++-- test/test_utilities/test_api_connector.py | 141 +--------------------- 2 files changed, 17 insertions(+), 148 deletions(-) diff --git a/parsons/utilities/api_connector.py b/parsons/utilities/api_connector.py index b50b9fd0eb..9bd18e0d1e 100644 --- a/parsons/utilities/api_connector.py +++ b/parsons/utilities/api_connector.py @@ -103,8 +103,7 @@ def request( The url request string. If ``url`` is a relative URL, it will be joined with the ``uri`` of the ``APIConnector`. - If ``url`` is an absolute URL, - it will be used as is. + If ``url`` is an absolute URL, it will be used as is. req_type: The request type. json: The payload of the request object. @@ -117,28 +116,33 @@ def request( E.g. ``http://myapi.com/things?id=1`` raise_on_error: If the request yields an error status code (anything above 400), - raise an error. In most cases, this should be ``True``, + raise an :class:`HTTPError`. In most cases, this should be ``True``, however in some cases, if you are looping through data, you might want to ignore individual failures. + additional_headers: + Additional headers to include in this specific request. + If a header key exists in both ``self.headers`` and + ``additional_headers``, the value from ``additional_headers`` + takes precedence. This does not mutate ``self.headers``. `**kwargs`: Additional keyword arguments to pass to :func:`requests.request`. """ full_url = urllib.parse.urljoin(self.uri, url) - complete_headers = ( - {**self.headers, **additional_headers} - if (self.headers and additional_headers) - else (self.headers or additional_headers) - ) + complete_headers: _Headers = {} + if self.headers: + complete_headers.update(self.headers) + if additional_headers: + complete_headers.update(additional_headers) resp = requests.request( req_type, full_url, - headers=complete_headers, + headers=complete_headers, # type: ignore[arg-type] auth=self.auth, json=json, data=data, - params=params, + params=params, # type: ignore[arg-type] **kwargs, ) diff --git a/test/test_utilities/test_api_connector.py b/test/test_utilities/test_api_connector.py index b29b8420ad..d464e07960 100644 --- a/test/test_utilities/test_api_connector.py +++ b/test/test_utilities/test_api_connector.py @@ -1,8 +1,6 @@ import pytest -import requests -from requests.exceptions import HTTPError +from requests_mock import Mocker -from parsons import Table from parsons.utilities.api_connector import APIConnector @@ -13,12 +11,7 @@ def connector() -> APIConnector: ) -def test_init_adds_trailing_slash() -> None: - conn = APIConnector(uri="https://api.example.com/v1") - assert conn.uri == "https://api.example.com/v1/" - - -def test_init_adds_headers(connector: APIConnector, requests_mock) -> None: +def test_init_adds_headers(connector: APIConnector, requests_mock: Mocker) -> None: requests_mock.get( "https://api.example.com/v1/data", json={"status": "authorized"}, status_code=200 ) @@ -45,7 +38,7 @@ def test_request_with_additional_headers(connector: APIConnector, requests_mock) assert req.headers["X-Custom-Header"] == "value" -def test_request_merges_base_and_additional_headers(connector: APIConnector, requests_mock) -> None: +def test_request_merges_base_and_additional_headers(connector: APIConnector, requests_mock: Mocker) -> None: requests_mock.get("https://api.example.com/v1/data", json={}, status_code=200) connector.request( @@ -58,131 +51,3 @@ def test_request_merges_base_and_additional_headers(connector: APIConnector, req assert req.headers["content-type"] == "application/json" assert req.headers["Authorization"] == "Bearer token123" assert req.headers["X-Custom-Header"] == "value" - - -def test_request_success(connector: APIConnector, requests_mock) -> None: - requests_mock.get( - "https://api.example.com/v1/users", json={"status": "success"}, status_code=200 - ) - - resp = connector.request("users", "GET") - assert resp.status_code == 200 - assert resp.json() == {"status": "success"} - - -def test_get_request_json(connector: APIConnector, requests_mock) -> None: - requests_mock.get("https://api.example.com/v1/data", json={"data": [1, 2, 3]}, status_code=200) - - result = connector.get_request("data", return_format="json") - assert result == {"data": [1, 2, 3]} - - -def test_get_request_content(connector: APIConnector, requests_mock) -> None: - requests_mock.get( - "https://api.example.com/v1/file", content=b"file content bytes", status_code=200 - ) - - result = connector.get_request("file", return_format="content") - assert result == b"file content bytes" - - -def test_get_request_invalid_format(connector: APIConnector, requests_mock) -> None: - requests_mock.get( - "https://api.example.com/v1/data", content=b"file content bytes", status_code=200 - ) - - with pytest.raises(RuntimeError, match="is not a valid format, change to json or content"): - connector.get_request("data", return_format="invalid") # type: ignore - - -def test_post_request(connector: APIConnector, requests_mock) -> None: - requests_mock.post( - "https://api.example.com/v1/items", json={"id": 123, "created": True}, status_code=201 - ) - - result = connector.post_request("items", json={"name": "test"}) - assert result == {"id": 123, "created": True} - - -def test_delete_request(connector: APIConnector, requests_mock) -> None: - requests_mock.delete("https://api.example.com/v1/items/1", status_code=204) - - result = connector.delete_request("items/1") - assert result == 204 - - -def test_put_request(connector: APIConnector, requests_mock) -> None: - requests_mock.put("https://api.example.com/v1/items/1", json={"updated": True}, status_code=200) - - result = connector.put_request("items/1", json={"name": "new_name"}) - assert result == {"updated": True} - - -def test_patch_request(connector: APIConnector, requests_mock) -> None: - requests_mock.patch( - "https://api.example.com/v1/items/1", json={"patched": True}, status_code=200 - ) - - result = connector.patch_request("items/1", json={"name": "patch_name"}) - assert result == {"patched": True} - - -def test_validate_response_error(connector: APIConnector, requests_mock) -> None: - requests_mock.get( - "https://api.example.com/v1/error", json={"error": "Unauthorized"}, status_code=401 - ) - - with pytest.raises(HTTPError) as exc_info: - connector.get_request("error") - - assert "Code: 401" in str(exc_info.value) - assert "Unauthorized" in str(exc_info.value) - - -def test_data_parse_with_data_key() -> None: - conn = APIConnector(uri="https://api.example.com/v1/", data_key="results") - payload = {"results": [{"id": 1}, {"id": 2}], "count": 2} - parsed = conn.data_parse(payload) - assert parsed == [{"id": 1}, {"id": 2}] - - -def test_data_parse_list_input() -> None: - conn = APIConnector(uri="https://api.example.com/v1/") - payload = [{"id": 1}, {"id": 2}] - parsed = conn.data_parse(payload) - assert parsed == payload - - -def test_next_page_check_url() -> None: - conn = APIConnector(uri="https://api.example.com/v1/", pagination_key="next") - - assert conn.next_page_check_url({"next": "https://api.example.com/v1/data?page=2"}) is True - assert conn.next_page_check_url({"next": None}) is False - assert conn.next_page_check_url({"other_key": "val"}) is False - - -def test_json_check(connector: APIConnector, requests_mock) -> None: - requests_mock.get("https://api.example.com/v1/json-check", json={"test": True}, status_code=200) - requests_mock.get( - "https://api.example.com/v1/text-check", text="Plain text response", status_code=200 - ) - - resp_json = requests.get("https://api.example.com/v1/json-check") - resp_text = requests.get("https://api.example.com/v1/text-check") - - assert connector.json_check(resp_json) is True - assert connector.json_check(resp_text) is False - - -def test_convert_to_table(connector) -> None: - list_data = [{"col1": "A", "col2": 1}, {"col1": "B", "col2": 2}] - dict_data = {"col1": "A", "col2": 1} - - table_from_list = connector.convert_to_table(list_data) - table_from_dict = connector.convert_to_table(dict_data) - - assert isinstance(table_from_list, Table) - assert table_from_list.num_rows == 2 - - assert isinstance(table_from_dict, Table) - assert table_from_dict.num_rows == 1 From 9d9f4bf70f4e359b762fedc2a19cdd9c436d6d99 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 17:52:45 -0400 Subject: [PATCH 59/94] ruff format --- test/test_utilities/test_api_connector.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_utilities/test_api_connector.py b/test/test_utilities/test_api_connector.py index d464e07960..161b040429 100644 --- a/test/test_utilities/test_api_connector.py +++ b/test/test_utilities/test_api_connector.py @@ -38,7 +38,9 @@ def test_request_with_additional_headers(connector: APIConnector, requests_mock) assert req.headers["X-Custom-Header"] == "value" -def test_request_merges_base_and_additional_headers(connector: APIConnector, requests_mock: Mocker) -> None: +def test_request_merges_base_and_additional_headers( + connector: APIConnector, requests_mock: Mocker +) -> None: requests_mock.get("https://api.example.com/v1/data", json={}, status_code=200) connector.request( From 24c032503700b6e03facd3280227cd51879fb575 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 18:19:12 -0400 Subject: [PATCH 60/94] Updated ratelimited api connector (#56) * Add ratelimited APIConnector wrapper * add to docs * Fix install issue on python 3.10 * add pyrate-limiter sphinx documentation links * Documentation improvement * Remove unecessary method overrides * Add basic tests for RateLimitedAPIConnector * revert dbt changes * Clean up whitespace in request method docstring Removed unnecessary whitespace in docstring. --------- Co-authored-by: Matthew Krausse <69082853+matthewkrausse@users.noreply.github.com> --- docs/conf.py | 1 + docs/framework/utilities.rst | 7 ++ parsons/utilities/api_connector.py | 2 +- .../utilities/ratelimited_api_connector.py | 21 +++++- .../test_ratelimited_api_connector.py | 72 +++++++++++++++++++ 5 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 test/test_utilities/test_ratelimited_api_connector.py diff --git a/docs/conf.py b/docs/conf.py index d31f39ee00..942e92c342 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -92,6 +92,7 @@ "psycopg2-binary": ("https://www.psycopg.org/docs/", None), "pyairtable": ("https://pyairtable.readthedocs.io/en/stable/", None), "PyGitHub": ("https://pygithub.readthedocs.io/en/latest/", None), + "pyratelimiter": ("https://pyratelimiter.readthedocs.io/en/latest/", None), "requests-toolbelt": ("https://toolbelt.readthedocs.io/en/stable/", None), "rich": ("https://rich.readthedocs.io/en/stable/", None), "simple-salesforce": ("https://simple-salesforce.readthedocs.io/en/latest/", None), diff --git a/docs/framework/utilities.rst b/docs/framework/utilities.rst index 8b99515c22..0916d3d499 100644 --- a/docs/framework/utilities.rst +++ b/docs/framework/utilities.rst @@ -132,6 +132,13 @@ OAuth API Connector :inherited-members: :members: +Rate-Limited API Connector +========================== + +.. automodule:: parsons.utilities.ratelimited_api_connector + :inherited-members: + :members: + SQL Helpers =========== diff --git a/parsons/utilities/api_connector.py b/parsons/utilities/api_connector.py index 9bd18e0d1e..e547f93f44 100644 --- a/parsons/utilities/api_connector.py +++ b/parsons/utilities/api_connector.py @@ -181,7 +181,7 @@ def get_request( return_format: Literal["json", "content"] = "json", raise_on_error: bool = True, **kwargs, - ) -> dict | bytes: + ) -> dict[str, Any] | bytes: """ Make a GET request. diff --git a/parsons/utilities/ratelimited_api_connector.py b/parsons/utilities/ratelimited_api_connector.py index ef2be71a0c..63ffcded1f 100644 --- a/parsons/utilities/ratelimited_api_connector.py +++ b/parsons/utilities/ratelimited_api_connector.py @@ -5,9 +5,20 @@ class RateLimitedAPIConnector(APIConnector): - """A wrapper around APIConnector that adds rate limiting using pyrate-limiter.""" + """A wrapper around :class:`APIConnector` that adds rate limiting.""" def __init__(self, *args, ratelimit: Rate, **kwargs) -> None: + """ + Initialize the RateLimitedAPIConnector. + + Accepts and passes through all the args and kwargs of :class:`APIConnector`. + + Args: + ratelimit: + The rate limit to apply to API calls, + as a pyrate-limiter :class:`pyrate_limiter.abstracts.Rate` object. + + """ self.limiter = Limiter(ratelimit) super().__init__(*args, **kwargs) @@ -16,6 +27,12 @@ def request( *args, **kwargs, ) -> requests.Response: - """Make a request with pyrate-limiter.""" + """ + Make a rate limited request. + + If the rate limit has been exceeded, the request will be held for the next available opportunity. + All args will be passed through to :class:`APIConnector`. + + """ self.limiter.try_acquire("api_call") return super().request(*args, **kwargs) diff --git a/test/test_utilities/test_ratelimited_api_connector.py b/test/test_utilities/test_ratelimited_api_connector.py new file mode 100644 index 0000000000..1425560fe2 --- /dev/null +++ b/test/test_utilities/test_ratelimited_api_connector.py @@ -0,0 +1,72 @@ +import pytest +from pyrate_limiter import Duration, Rate +from requests_mock import DELETE, GET, PATCH, POST, PUT, Mocker + +from parsons.utilities.ratelimited_api_connector import RateLimitedAPIConnector + +EXAMPLE_URL = "https://api.example.com" +EXAMPLE_ENDPOINT = f"{EXAMPLE_URL}/test-endpoint" + + +@pytest.fixture +def connector() -> RateLimitedAPIConnector: + rate = Rate(1, Duration.MINUTE) + return RateLimitedAPIConnector(EXAMPLE_URL, ratelimit=rate) + + +@pytest.fixture +def limiter_spy(connector: RateLimitedAPIConnector) -> list[tuple[str, bool]]: + """Spies on connector.limiter.try_acquire and records call results.""" + calls = [] + original_try_acquire = connector.limiter.try_acquire + + def spy_try_acquire(name: str = "pyrate", *, blocking: bool = False): + is_blocking = not original_try_acquire(name, blocking=blocking) + calls.append((name, is_blocking)) + return is_blocking + + connector.limiter.try_acquire = spy_try_acquire # type: ignore + return calls + + +@pytest.mark.parametrize( + ("request_type", "method_name"), + [ + (GET, "get_request"), + (POST, "post_request"), + (PUT, "put_request"), + (PATCH, "patch_request"), + (DELETE, "delete_request"), + ], +) +def test_methods_triggers_limiter( + connector: RateLimitedAPIConnector, + requests_mock: Mocker, + limiter_spy: list[tuple[str, bool]], + request_type: str, + method_name: str, +): + requests_mock.register_uri( + method=request_type, + url=EXAMPLE_ENDPOINT, + json={"status": "ok"}, + status_code=200, + ) + + method = getattr(connector, method_name) + method(EXAMPLE_ENDPOINT) + + assert limiter_spy == [("api_call", False)] + + +def test_rate_limiter_blocks_exceeding_calls( + connector: RateLimitedAPIConnector, + requests_mock: Mocker, + limiter_spy: list[tuple[str, bool]], +): + requests_mock.get(url=EXAMPLE_ENDPOINT, json={"data": "ok"}) + + connector.request(EXAMPLE_ENDPOINT, req_type=GET) + connector.request(EXAMPLE_ENDPOINT, req_type=GET) + + assert limiter_spy == [("api_call", False), ("api_call", True)] From a749f00a19a8e1ceab280fc439e52b986d1ea2c0 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 18:44:48 -0400 Subject: [PATCH 61/94] add first-pass documentation --- docs/solidarity_tech.rst | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/solidarity_tech.rst diff --git a/docs/solidarity_tech.rst b/docs/solidarity_tech.rst new file mode 100644 index 0000000000..e65742c31a --- /dev/null +++ b/docs/solidarity_tech.rst @@ -0,0 +1,54 @@ +############### +Solidarity Tech +############### + +Overview +======== + +Solidarity Tech's all-in-one nonprofit CRM includes digital tools like +texting, calling, email & websites for advocacy groups, unions & grassroots organizers. + +As of September 2026, the :class:`~parsons.solidarity_tech.solidarity_tech.SolidarityTech` connector supports +all endpoints described in the `Solidarity Tech API Documentation `_. + +Quickstart +========== + +To instantiate the :class:`~parsons.solidarity_tech.solidarity_tech.SolidarityTech` class, +you can either store your Solidarity Tech bearer authorization key as +an environmental variable (``SOLIDARITY_TECH_BEARER_KEY``) or pass it as a keyword argument. + +.. code-block:: python + :caption: Load bearer authorization key from environment variable + + from parsons import SolidarityTech + st = SolidarityTech() + +.. code-block:: python + :caption: Pass bearer authorization key as argument + + from parsons import SolidarityTech + st = SolidarityTech(api_token='my_bearer_key') + +You can then call various endpoints: + +.. code-block:: python + :caption: Get all events + + events = st.get_events() + +.. code-block:: python + :caption: Create a new user + + st.create_user( + first_name="Elizabeth", + last_name="Flynn", + email="egflynn@example.com" + ) + +API +==== + +.. autoclass:: parsons.solidarity_tech.solidarity_tech.SolidarityTech + :inherited-members: + :members: From 307ae3d8033d18a05c72d8db5c9ce73637a44cee Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 19:14:08 -0400 Subject: [PATCH 62/94] add solidarity tech to docs index --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index 2af5cc9647..39893776e9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -187,6 +187,7 @@ Indices and tables sftp shopify sisense + solidarity_tech targetsmart turbovote twilio From 3a1f7c27a0d2d1a7fb31fd4460b92a6de636322f Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 19:22:14 -0400 Subject: [PATCH 63/94] Update env variable name --- docs/solidarity_tech.rst | 2 +- parsons/solidarity_tech/solidarity_tech_base.py | 4 ++-- .../test_solidarity_tech_init.py | 17 +++++++++-------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/solidarity_tech.rst b/docs/solidarity_tech.rst index e65742c31a..8df14b83ec 100644 --- a/docs/solidarity_tech.rst +++ b/docs/solidarity_tech.rst @@ -28,7 +28,7 @@ an environmental variable (``SOLIDARITY_TECH_BEARER_KEY``) or pass it as a keywo :caption: Pass bearer authorization key as argument from parsons import SolidarityTech - st = SolidarityTech(api_token='my_bearer_key') + st = SolidarityTech(api_token='SOME_BEARER_KEY') You can then call various endpoints: diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index 559a0b289b..ec2c4f994f 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -24,10 +24,10 @@ def __init__(self, api_token: str | None = None) -> None: Args: api_token: A valid Bearer token for authorization. - Not required if the `SOLIDARITY_TECH_TOKEN` env variable is set. + Not required if the `SOLIDARITY_TECH_BEARER_KEY` env variable is set. """ - self.api_token = cast("str", check_env.check("SOLIDARITY_TECH_TOKEN", api_token)) + self.api_token = cast("str", check_env.check("SOLIDARITY_TECH_BEARER_KEY", api_token)) self.headers = {"authorization": f"Bearer {self.api_token}"} self.api_url = "https://api.solidarity.tech/v1/" self.api = RateLimitedAPIConnector( diff --git a/test/test_solidarity-tech/test_solidarity_tech_init.py b/test/test_solidarity-tech/test_solidarity_tech_init.py index 49b21fbed7..45b309f0df 100644 --- a/test/test_solidarity-tech/test_solidarity_tech_init.py +++ b/test/test_solidarity-tech/test_solidarity_tech_init.py @@ -5,22 +5,23 @@ from parsons.solidarity_tech.solidarity_tech import SolidarityTech -PLACEHOLDER_TOKEN = "SOME_API_KEY" +TOKEN_ENV_NAME = "SOLIDARITY_TECH_BEARER_KEY" +TOKEN_PLACEHOLDER = "SOME_BEARER_KEY" def test_init_with_arg() -> None: - st = SolidarityTech(api_token=PLACEHOLDER_TOKEN) - assert st.api_token == PLACEHOLDER_TOKEN - assert st.headers.get("authorization") == f"Bearer {PLACEHOLDER_TOKEN}" + st = SolidarityTech(api_token=TOKEN_PLACEHOLDER) + assert st.api_token == TOKEN_PLACEHOLDER + assert st.headers.get("authorization") == f"Bearer {TOKEN_PLACEHOLDER}" -@mock.patch.dict(os.environ, {"SOLIDARITY_TECH_TOKEN": PLACEHOLDER_TOKEN}) +@mock.patch.dict(os.environ, {TOKEN_ENV_NAME: TOKEN_PLACEHOLDER}) def test_init_with_env() -> None: st = SolidarityTech() - assert st.api_token == PLACEHOLDER_TOKEN - assert st.headers.get("authorization") == f"Bearer {PLACEHOLDER_TOKEN}" + assert st.api_token == TOKEN_PLACEHOLDER + assert st.headers.get("authorization") == f"Bearer {TOKEN_PLACEHOLDER}" def test_init_with_no_api_token() -> None: - with pytest.raises(KeyError, match="No 'SOLIDARITY_TECH_TOKEN' found."): + with pytest.raises(KeyError, match="No '{TOKEN_ENV_NAME}' found."): SolidarityTech() From e3ac4c2668355a78ee3a1fa71c4f6f18b45bf3ff Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 20:47:10 -0400 Subject: [PATCH 64/94] Improve exceptions --- parsons/solidarity_tech/exceptions.py | 35 +++++++++++++++++---------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/parsons/solidarity_tech/exceptions.py b/parsons/solidarity_tech/exceptions.py index e2f1b588af..fef11ec42c 100644 --- a/parsons/solidarity_tech/exceptions.py +++ b/parsons/solidarity_tech/exceptions.py @@ -2,25 +2,34 @@ from requests.exceptions import HTTPError -class STUnexpectedResponseError(HTTPError): - """Status code is not expected.""" +class STResponseError(HTTPError): + """Base exception for all Solidarity Tech response errors.""" - def __init__(self, message: str | None = None, *, response: requests.Response) -> None: - err_msg = "Unexpected Response" - if response and response.status_code: + def __init__( + self, message: str | None = None, *args, err_msg: str, response: requests.Response, **kwargs + ) -> None: + if response.status_code: err_msg += f" (Status Code {response.status_code})" if message: err_msg += f" -- {message}" - super().__init__(err_msg, response=response) + super().__init__(err_msg, *args, **kwargs) + + +class STUnexpectedResponseError(STResponseError): + """Status code is not expected.""" + + def __init__( + self, message: str | None = None, *args, response: requests.Response, **kwargs + ) -> None: + err_msg = "Unexpected Response" + super().__init__(message, *args, err_msg=err_msg, response=response, **kwargs) -class STFailedResponseError(HTTPError): +class STFailedResponseError(STResponseError): """Status code indicates a known failure.""" - def __init__(self, message: str, *, response: requests.Response) -> None: + def __init__( + self, message: str | None = None, *args, response: requests.Response, **kwargs + ) -> None: err_msg = "Request Failed" - if response and response.status_code: - err_msg += f" (Status Code {response.status_code})" - if message: - err_msg += f" -- {message}" - super().__init__(err_msg, response=response) + super().__init__(message, *args, err_msg=err_msg, response=response, **kwargs) From 62cda3be3d949a4f3a66f30f3b51bb1b62102b91 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 20:47:41 -0400 Subject: [PATCH 65/94] add SolidarityTech to parsons init --- parsons/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/parsons/__init__.py b/parsons/__init__.py index 5bdcbba920..38db6d1184 100644 --- a/parsons/__init__.py +++ b/parsons/__init__.py @@ -100,6 +100,7 @@ "SFTP": "parsons.sftp.sftp", "Shopify": "parsons.shopify.shopify", "Sisense": "parsons.sisense.sisense", + "SolidarityTech": "parsons.solidarity_tech.solidarity_tech", "TargetSmartAPI": "parsons.targetsmart.targetsmart_api", "TargetSmartAutomation": "parsons.targetsmart.targetsmart_automation", "TurboVote": "parsons.turbovote.turbovote", From 68b7fcf80cab7de1c01e4914f6f6c2f9833d38a0 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 20:48:09 -0400 Subject: [PATCH 66/94] improve tests --- .../solidarity_tech/solidarity_tech_base.py | 8 +- test/test_solidarity-tech/conftest.py | 2 +- test/test_solidarity-tech/test_st_base.py | 93 +++++++++++++++++++ ...olidarity_tech_init.py => test_st_init.py} | 12 ++- 4 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 test/test_solidarity-tech/test_st_base.py rename test/test_solidarity-tech/{test_solidarity_tech_init.py => test_st_init.py} (63%) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index ec2c4f994f..4b636011ba 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -29,7 +29,7 @@ def __init__(self, api_token: str | None = None) -> None: """ self.api_token = cast("str", check_env.check("SOLIDARITY_TECH_BEARER_KEY", api_token)) self.headers = {"authorization": f"Bearer {self.api_token}"} - self.api_url = "https://api.solidarity.tech/v1/" + self.api_url = "https://api.solidarity.tech/v1" self.api = RateLimitedAPIConnector( self.api_url, headers=self.headers, ratelimit=Rate(60, Duration.SECOND * 30) ) @@ -143,11 +143,11 @@ def _handle_status_codes( """ if res.status_code in codes: - status_code = codes[res.status_code][0] + success = codes[res.status_code][0] result_message = codes[res.status_code][1] - if status_code is True: + if success is True: logger.debug(result_message, extra={"status_code": res.status_code}) - return status_code + return success raise STFailedResponseError(result_message, response=res) raise STUnexpectedResponseError(response=res) diff --git a/test/test_solidarity-tech/conftest.py b/test/test_solidarity-tech/conftest.py index 415896e671..18fced3596 100644 --- a/test/test_solidarity-tech/conftest.py +++ b/test/test_solidarity-tech/conftest.py @@ -5,4 +5,4 @@ @pytest.fixture def st() -> SolidarityTech: - return SolidarityTech(api_token="SOME_API_KEY") + return SolidarityTech(api_token="SOME_BEARER_KEY") diff --git a/test/test_solidarity-tech/test_st_base.py b/test/test_solidarity-tech/test_st_base.py new file mode 100644 index 0000000000..d608c2b2d7 --- /dev/null +++ b/test/test_solidarity-tech/test_st_base.py @@ -0,0 +1,93 @@ +import re + +import pytest +import requests +from requests_mock import Mocker + +from parsons import SolidarityTech +from parsons.solidarity_tech.exceptions import STFailedResponseError, STUnexpectedResponseError + + +@pytest.fixture +def known_status_codes() -> dict[int, tuple[bool, str]]: + return { + 200: (True, "OK"), + 201: (True, "updated resource"), + 404: (False, "could not find resource"), + 422: (False, "could not process request"), + } + + +@pytest.mark.parametrize( + ("key", "value", "expected"), + [ + ("test_key1", "test_value", {"test_key1": "test_value"}), + ("test_key2", 123456, {"test_key2": 123456}), + ("test_key_none", None, {}), + ], +) +def test_get_resources( + st: SolidarityTech, + key: str, + value: str | int | None, + expected: dict[str, str | int], +) -> None: + init_dict = {} + result = st._add_if_field_not_empty(init_dict, key, value) + assert result == expected + + +def test_get_resources_overwrite(st: SolidarityTech) -> None: + init_dict = {"test_key": "original_value"} + result = st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value", overwrite=True) + assert result["test_key"] == "overwrite_value" + + +def test_get_resources_no_overwrite_default(st: SolidarityTech) -> None: + init_dict = {"test_key": "original_value"} + with pytest.raises(KeyError, match="'test_key' already exists"): + st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value") + + +def test_get_resources_no_overwrite(st: SolidarityTech) -> None: + init_dict = {"test_key": "original_value"} + with pytest.raises(KeyError, match="'test_key' already exists"): + st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value", overwrite=False) + + +@pytest.mark.parametrize( + "status_code", + [200, 201, 404, 422], +) +def test_handle_status_codes( + st: SolidarityTech, + requests_mock: Mocker, + known_status_codes: dict[int, tuple[bool, str]], + status_code: int, +) -> None: + requests_mock.get("https://api.example.com", status_code=status_code) + res = requests.get("https://api.example.com") + + success_expected = known_status_codes[status_code][0] + if success_expected: + assert st._handle_status_codes(res, known_status_codes) + else: + failure_description = known_status_codes[status_code][1] + err_msg = re.escape(f"Request Failed (Status Code {status_code}) -- {failure_description}") + with pytest.raises(STFailedResponseError, match=err_msg): + st._handle_status_codes(res, known_status_codes) + + +def test_handle_status_codes_unrecognized( + st: SolidarityTech, requests_mock: Mocker, known_status_codes: dict[int, tuple[bool, str]] +) -> None: + status_code = 500 + + requests_mock.get("https://api.example.com", status_code=status_code) + res = requests.get("https://api.example.com") + + with pytest.raises( + STUnexpectedResponseError, + match=re.escape(f"Unexpected Response (Status Code {status_code})"), + ): + st._handle_status_codes(res, known_status_codes) diff --git a/test/test_solidarity-tech/test_solidarity_tech_init.py b/test/test_solidarity-tech/test_st_init.py similarity index 63% rename from test/test_solidarity-tech/test_solidarity_tech_init.py rename to test/test_solidarity-tech/test_st_init.py index 45b309f0df..aad4dd6b1b 100644 --- a/test/test_solidarity-tech/test_solidarity_tech_init.py +++ b/test/test_solidarity-tech/test_st_init.py @@ -1,7 +1,7 @@ import os -from unittest import mock import pytest +from pytest_mock import MockerFixture from parsons.solidarity_tech.solidarity_tech import SolidarityTech @@ -15,13 +15,17 @@ def test_init_with_arg() -> None: assert st.headers.get("authorization") == f"Bearer {TOKEN_PLACEHOLDER}" -@mock.patch.dict(os.environ, {TOKEN_ENV_NAME: TOKEN_PLACEHOLDER}) -def test_init_with_env() -> None: +def test_init_with_env(mocker: MockerFixture) -> None: + mocker.patch.dict(os.environ, {TOKEN_ENV_NAME: TOKEN_PLACEHOLDER}) st = SolidarityTech() assert st.api_token == TOKEN_PLACEHOLDER assert st.headers.get("authorization") == f"Bearer {TOKEN_PLACEHOLDER}" def test_init_with_no_api_token() -> None: - with pytest.raises(KeyError, match="No '{TOKEN_ENV_NAME}' found."): + with pytest.raises(KeyError, match=f"No '{TOKEN_ENV_NAME}' found."): SolidarityTech() + + +def test_init_api_url(st: SolidarityTech) -> None: + assert st.api_url == "https://api.solidarity.tech/v1" From 03905a036c29a39cd42e44dd528d1554d0daa3e4 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 21:32:41 -0400 Subject: [PATCH 67/94] fix kwarg remapping in get_resources --- parsons/solidarity_tech/solidarity_tech_base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index 4b636011ba..10f4f934da 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -75,7 +75,7 @@ def _get_resources(self, endpoint: str, **kwargs) -> requests.Response: params: dict[str, ParamTypes] = {} for key, value in param_mapping.items(): if key in kwargs: - params[key] = value + params[value] = kwargs[key] del kwargs[key] if "params" in kwargs: @@ -87,8 +87,11 @@ def _get_resources(self, endpoint: str, **kwargs) -> requests.Response: continue params[key] = value + if params: + kwargs["params"] = params + logger.debug("Processing GET request at endpoint: %s", endpoint, extra=params) - return self.api.request(url=endpoint, req_type="GET", params=params, **kwargs) + return self.api.request(url=endpoint, req_type="GET", **kwargs) def _get_single_resource(self, endpoint: str, id: int, **kwargs) -> requests.Response: """Handle GET requests for single resources.""" From 32290df538fb3ff938b5eeef2e6a39d47dc5f61a Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 21:34:06 -0400 Subject: [PATCH 68/94] improved testing --- test/test_solidarity-tech/conftest.py | 2 +- test/test_solidarity-tech/test_st_base.py | 208 ++++++++++++++-------- test/test_solidarity-tech/test_st_init.py | 2 +- 3 files changed, 136 insertions(+), 76 deletions(-) diff --git a/test/test_solidarity-tech/conftest.py b/test/test_solidarity-tech/conftest.py index 18fced3596..15a743b35a 100644 --- a/test/test_solidarity-tech/conftest.py +++ b/test/test_solidarity-tech/conftest.py @@ -1,6 +1,6 @@ import pytest -from parsons.solidarity_tech.solidarity_tech import SolidarityTech +from parsons.solidarity_tech import SolidarityTech @pytest.fixture diff --git a/test/test_solidarity-tech/test_st_base.py b/test/test_solidarity-tech/test_st_base.py index d608c2b2d7..aea9cbba34 100644 --- a/test/test_solidarity-tech/test_st_base.py +++ b/test/test_solidarity-tech/test_st_base.py @@ -2,9 +2,10 @@ import pytest import requests -from requests_mock import Mocker +from pytest_mock import MockerFixture +from requests_mock import GET, Mocker -from parsons import SolidarityTech +from parsons.solidarity_tech import SolidarityTech from parsons.solidarity_tech.exceptions import STFailedResponseError, STUnexpectedResponseError @@ -18,76 +19,135 @@ def known_status_codes() -> dict[int, tuple[bool, str]]: } -@pytest.mark.parametrize( - ("key", "value", "expected"), - [ - ("test_key1", "test_value", {"test_key1": "test_value"}), - ("test_key2", 123456, {"test_key2": 123456}), - ("test_key_none", None, {}), - ], -) -def test_get_resources( - st: SolidarityTech, - key: str, - value: str | int | None, - expected: dict[str, str | int], -) -> None: - init_dict = {} - result = st._add_if_field_not_empty(init_dict, key, value) - assert result == expected - - -def test_get_resources_overwrite(st: SolidarityTech) -> None: - init_dict = {"test_key": "original_value"} - result = st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value", overwrite=True) - assert result["test_key"] == "overwrite_value" - - -def test_get_resources_no_overwrite_default(st: SolidarityTech) -> None: - init_dict = {"test_key": "original_value"} - with pytest.raises(KeyError, match="'test_key' already exists"): - st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value") - - -def test_get_resources_no_overwrite(st: SolidarityTech) -> None: - init_dict = {"test_key": "original_value"} - with pytest.raises(KeyError, match="'test_key' already exists"): - st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value", overwrite=False) - - -@pytest.mark.parametrize( - "status_code", - [200, 201, 404, 422], -) -def test_handle_status_codes( - st: SolidarityTech, - requests_mock: Mocker, - known_status_codes: dict[int, tuple[bool, str]], - status_code: int, -) -> None: - requests_mock.get("https://api.example.com", status_code=status_code) - res = requests.get("https://api.example.com") - - success_expected = known_status_codes[status_code][0] - if success_expected: - assert st._handle_status_codes(res, known_status_codes) - else: - failure_description = known_status_codes[status_code][1] - err_msg = re.escape(f"Request Failed (Status Code {status_code}) -- {failure_description}") - with pytest.raises(STFailedResponseError, match=err_msg): +class Test_Get_Single_Resource: + def test_get_single_resource_makes_request_with_id( + self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture + ) -> None: + id = 42 + endpoint = f"{st.api_url}/{id}" + + requests_mock.get(endpoint) + spy = mocker.spy(st.api, "request") + + st._get_single_resource(st.api_url, id) + spy.assert_called_once_with(url=endpoint, req_type=GET) + + +class Test_Get_Resources: + def test_get_resources_makes_request( + self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture + ) -> None: + requests_mock.get(st.api_url) + spy = mocker.spy(st.api, "request") + st._get_resources(st.api_url) + spy.assert_called_once_with(url=st.api_url, req_type=GET) + + def test_get_resources_remaps_special_query_strings( + self, + st: SolidarityTech, + requests_mock: Mocker, + mocker: MockerFixture, + ) -> None: + requests_mock.get(st.api_url) + spy = mocker.spy(st.api, "request") + st._get_resources( + st.api_url, + limit=123456, + cursor=654321, + offset=321456, + since=456321, + include_count=123654, + ) + spy.assert_called_once_with( + url=st.api_url, + req_type=GET, + params={ + "_limit": 123456, + "_cursor": 654321, + "_offset": 321456, + "_since": 456321, + "_include_count": 123654, + }, + ) + + +class Test_Add_If_Field_Not_Empty: + @pytest.mark.parametrize( + ("key", "value", "expected"), + [ + ("test_key1", "test_value", {"test_key1": "test_value"}), + ("test_key2", 123456, {"test_key2": 123456}), + ("test_key_none", None, {}), + ], + ) + def test_add_if_field_not_empty( + self, + st: SolidarityTech, + key: str, + value: str | int | None, + expected: dict[str, str | int], + ) -> None: + init_dict = {} + result = st._add_if_field_not_empty(init_dict, key, value) + assert result == expected + + def test_add_if_field_not_empty_overwrite(self, st: SolidarityTech) -> None: + init_dict = {"test_key": "original_value"} + result = st._add_if_field_not_empty( + init_dict, "test_key", "overwrite_value", overwrite=True + ) + assert result["test_key"] == "overwrite_value" + + def test_add_if_field_not_empty_no_overwrite_default(self, st: SolidarityTech) -> None: + init_dict = {"test_key": "original_value"} + with pytest.raises(KeyError, match="'test_key' already exists"): + st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value") + + def test_add_if_field_not_empty_no_overwrite(self, st: SolidarityTech) -> None: + init_dict = {"test_key": "original_value"} + with pytest.raises(KeyError, match="'test_key' already exists"): + st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value", overwrite=False) + + +class Test_Handle_Status_Codes: + @pytest.mark.parametrize( + "status_code", + [200, 201, 404, 422], + ) + def test_handle_status_codes( + self, + st: SolidarityTech, + requests_mock: Mocker, + known_status_codes: dict[int, tuple[bool, str]], + status_code: int, + ) -> None: + requests_mock.get("https://api.example.com", status_code=status_code) + res = requests.get("https://api.example.com") + + success_expected = known_status_codes[status_code][0] + if success_expected: + assert st._handle_status_codes(res, known_status_codes) + else: + failure_description = known_status_codes[status_code][1] + err_msg = re.escape( + f"Request Failed (Status Code {status_code}) -- {failure_description}" + ) + with pytest.raises(STFailedResponseError, match=err_msg): + st._handle_status_codes(res, known_status_codes) + + def test_handle_status_codes_unrecognized( + self, + st: SolidarityTech, + requests_mock: Mocker, + known_status_codes: dict[int, tuple[bool, str]], + ) -> None: + status_code = 500 + + requests_mock.get("https://api.example.com", status_code=status_code) + res = requests.get("https://api.example.com") + + with pytest.raises( + STUnexpectedResponseError, + match=re.escape(f"Unexpected Response (Status Code {status_code})"), + ): st._handle_status_codes(res, known_status_codes) - - -def test_handle_status_codes_unrecognized( - st: SolidarityTech, requests_mock: Mocker, known_status_codes: dict[int, tuple[bool, str]] -) -> None: - status_code = 500 - - requests_mock.get("https://api.example.com", status_code=status_code) - res = requests.get("https://api.example.com") - - with pytest.raises( - STUnexpectedResponseError, - match=re.escape(f"Unexpected Response (Status Code {status_code})"), - ): - st._handle_status_codes(res, known_status_codes) diff --git a/test/test_solidarity-tech/test_st_init.py b/test/test_solidarity-tech/test_st_init.py index aad4dd6b1b..75ee14f2c7 100644 --- a/test/test_solidarity-tech/test_st_init.py +++ b/test/test_solidarity-tech/test_st_init.py @@ -3,7 +3,7 @@ import pytest from pytest_mock import MockerFixture -from parsons.solidarity_tech.solidarity_tech import SolidarityTech +from parsons.solidarity_tech import SolidarityTech TOKEN_ENV_NAME = "SOLIDARITY_TECH_BEARER_KEY" TOKEN_PLACEHOLDER = "SOME_BEARER_KEY" From 48ca8de2d5d31304e244082ac416787a14c34918 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 21:54:31 -0400 Subject: [PATCH 69/94] add post tests --- test/test_solidarity-tech/test_st_base.py | 26 ++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/test_solidarity-tech/test_st_base.py b/test/test_solidarity-tech/test_st_base.py index aea9cbba34..575850e666 100644 --- a/test/test_solidarity-tech/test_st_base.py +++ b/test/test_solidarity-tech/test_st_base.py @@ -3,7 +3,7 @@ import pytest import requests from pytest_mock import MockerFixture -from requests_mock import GET, Mocker +from requests_mock import GET, POST, Mocker from parsons.solidarity_tech import SolidarityTech from parsons.solidarity_tech.exceptions import STFailedResponseError, STUnexpectedResponseError @@ -19,6 +19,30 @@ def known_status_codes() -> dict[int, tuple[bool, str]]: } +class Test_Post_Request: + def test_get_single_resource_makes_request_with_payload( + self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture + ) -> None: + payload = {"user_id": 654123} + + requests_mock.post(st.api_url) + spy = mocker.spy(st.api, "request") + + st._post_request(st.api_url, payload=payload) + spy.assert_called_once_with(url=st.api_url, req_type=POST, json=payload) + + def test_get_single_resource_makes_request_with_params( + self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture + ) -> None: + params = {"automation_id": 35876} + + requests_mock.post(st.api_url) + spy = mocker.spy(st.api, "request") + + st._post_request(st.api_url, params=params) + spy.assert_called_once_with(url=st.api_url, req_type=POST, json=None, params=params) + + class Test_Get_Single_Resource: def test_get_single_resource_makes_request_with_id( self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture From fe8f943e620559adb8aa333d801a5b6969c30ea1 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 21:54:59 -0400 Subject: [PATCH 70/94] test for params collisison in get_resources --- test/test_solidarity-tech/test_st_base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/test_solidarity-tech/test_st_base.py b/test/test_solidarity-tech/test_st_base.py index 575850e666..894a4059f5 100644 --- a/test/test_solidarity-tech/test_st_base.py +++ b/test/test_solidarity-tech/test_st_base.py @@ -94,6 +94,15 @@ def test_get_resources_remaps_special_query_strings( }, ) + def test_get_resources_param_collision_error( + self, + st: SolidarityTech, + requests_mock: Mocker, + ) -> None: + requests_mock.get(st.api_url) + with pytest.raises(KeyError, match="Request param '_limit' already exists"): + st._get_resources(st.api_url, limit=15, params={"_limit": 30}) + class Test_Add_If_Field_Not_Empty: @pytest.mark.parametrize( From 1b20126b6e28a605befb9f8111d85a0684cecdfe Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sun, 2 Aug 2026 21:55:17 -0400 Subject: [PATCH 71/94] test since datetime conversion in get_resources --- test/test_solidarity-tech/test_st_base.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/test_solidarity-tech/test_st_base.py b/test/test_solidarity-tech/test_st_base.py index 894a4059f5..9bc0f4ace3 100644 --- a/test/test_solidarity-tech/test_st_base.py +++ b/test/test_solidarity-tech/test_st_base.py @@ -1,4 +1,5 @@ import re +from datetime import datetime, timezone import pytest import requests @@ -66,6 +67,28 @@ def test_get_resources_makes_request( st._get_resources(st.api_url) spy.assert_called_once_with(url=st.api_url, req_type=GET) + def test_get_resources_datetime( + self, + st: SolidarityTech, + requests_mock: Mocker, + mocker: MockerFixture, + ) -> None: + now_datetime = datetime.now(tz=timezone.utc) + now_timestamp = int(now_datetime.timestamp()) + + requests_mock.get(st.api_url) + spy = mocker.spy(st.api, "request") + + st._get_resources( + st.api_url, + since=now_datetime, + ) + spy.assert_called_once_with( + url=st.api_url, + req_type=GET, + params={"_since": now_timestamp}, + ) + def test_get_resources_remaps_special_query_strings( self, st: SolidarityTech, From f3d3607da18dff0d42973578af10e98f33066a76 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 5 Aug 2026 11:50:46 -0400 Subject: [PATCH 72/94] Convert literals to enums and use _add_if_field_not_empty to conditionally handle adding args without values to payload/params dicts --- .../solidarity_tech/solidarity_tech_base.py | 7 +- .../solidarity_tech/solidarity_tech_enums.py | 42 +++++++ .../solidarity_tech_literals.py | 10 -- parsons/solidarity_tech/st_activities.py | 5 +- .../solidarity_tech/st_agent_assignments.py | 14 ++- .../st_automation_enrollments.py | 4 +- parsons/solidarity_tech/st_calls.py | 4 +- .../st_chapter_phone_numbers.py | 4 +- .../st_custom_user_properties.py | 31 +++--- parsons/solidarity_tech/st_emails.py | 14 ++- .../solidarity_tech/st_event_attendances.py | 9 +- parsons/solidarity_tech/st_event_rsvps.py | 48 ++++---- parsons/solidarity_tech/st_event_sessions.py | 82 +++++++------- parsons/solidarity_tech/st_events.py | 39 ++++--- .../solidarity_tech/st_field_survey_urls.py | 5 +- parsons/solidarity_tech/st_pages.py | 7 +- parsons/solidarity_tech/st_phonebanks.py | 5 +- parsons/solidarity_tech/st_scheduled_calls.py | 6 +- parsons/solidarity_tech/st_scheduled_tasks.py | 55 +++++---- parsons/solidarity_tech/st_task_agents.py | 11 +- .../solidarity_tech/st_task_assignments.py | 15 ++- parsons/solidarity_tech/st_team_members.py | 29 ++--- parsons/solidarity_tech/st_text_templates.py | 31 +++--- parsons/solidarity_tech/st_textbanks.py | 5 +- parsons/solidarity_tech/st_texts.py | 14 ++- parsons/solidarity_tech/st_user_actions.py | 23 ++-- parsons/solidarity_tech/st_user_lists.py | 28 ++--- parsons/solidarity_tech/st_user_notes.py | 16 ++- .../solidarity_tech/st_user_relationships.py | 11 +- parsons/solidarity_tech/st_users.py | 105 +++++++++--------- 30 files changed, 399 insertions(+), 280 deletions(-) create mode 100644 parsons/solidarity_tech/solidarity_tech_enums.py delete mode 100644 parsons/solidarity_tech/solidarity_tech_literals.py diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index 10f4f934da..b13c5b5e0b 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -1,7 +1,8 @@ import logging from collections.abc import Mapping from datetime import datetime -from typing import cast +from enum import Enum +from typing import Any, cast import numpy as np import requests @@ -156,7 +157,7 @@ def _handle_status_codes( raise STUnexpectedResponseError(response=res) def _add_if_field_not_empty( - self, receiving_dict: dict, key: str, value, overwrite: bool = False + self, receiving_dict: dict, key: str, value: Any | None, overwrite: bool = False ) -> dict: """ Add a key/value pair to a dictionary if the value is not None. @@ -182,7 +183,7 @@ def _add_if_field_not_empty( err_msg = f"'{key}' already exists." raise KeyError(err_msg) if value: - receiving_dict[key] = value + receiving_dict[key] = value.value if isinstance(value, Enum) else value logger.debug( "Added '%s' with value '%s' to payload or parameters dictionary", key, value ) diff --git a/parsons/solidarity_tech/solidarity_tech_enums.py b/parsons/solidarity_tech/solidarity_tech_enums.py new file mode 100644 index 0000000000..1e318944d4 --- /dev/null +++ b/parsons/solidarity_tech/solidarity_tech_enums.py @@ -0,0 +1,42 @@ +from enum import Enum + + +class AttendanceStatus(Enum): + YES = "yes" + NO = "no" + MAYBE = "maybe" + WAITLISTED = "waitlisted" + + +class EventType(Enum): + VIRTUAL = "virtual" + IN_PERSON = "in_person" + HYBRID = "hybrid" + + +class FieldType(Enum): + INPUT = "input" + TEXT_AREA = "textarea" + NUMBER = "number" + DATE = "date" + CHECKBOX = "checkbox" + SELECT = "select" + RADIOS = "radios" + CHECKBOXES = "checkboxes" + + +class InviteType(Enum): + SMS = "sms" + EMAIL = "email" + + +class ScopeType(Enum): + ORGANIZATION = "Organization" + CHAPTER = "Chapter" + + +class InteractionType(Enum): + IN_PERSON = "in_person" + CALL = "call" + TEXT = "text" + EMAIL = "email" diff --git a/parsons/solidarity_tech/solidarity_tech_literals.py b/parsons/solidarity_tech/solidarity_tech_literals.py deleted file mode 100644 index 3352108239..0000000000 --- a/parsons/solidarity_tech/solidarity_tech_literals.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Literal - -AttendanceType = Literal["yes", "no", "maybe", "waitlisted"] -EventType = Literal["virtual", "in_person"] -FieldType = Literal[ - "input", "textarea", "number", "date", "checkbox", "select", "radios", "checkboxes" -] -InviteType = Literal["sms", "email"] -ScopeType = Literal["Organization", "Chapter"] -InteractionType = Literal["in_person", "call", "text", "email"] diff --git a/parsons/solidarity_tech/st_activities.py b/parsons/solidarity_tech/st_activities.py index 963612e28b..b6cfa824f6 100644 --- a/parsons/solidarity_tech/st_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -52,7 +53,9 @@ def get_activities( ``__ """ - params = {"user_id": user_id} + params: dict[str, Any] = {} + self._add_if_field_not_empty(params, "user_id", user_id) + res = self._get_resources( "activities", limit=limit, diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index 5df9b665c6..4c65ba491f 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any import numpy as np @@ -45,7 +46,10 @@ def get_agent_assignments( ``__ """ - params = {"user_id": user_id, "agent_user_id": agent_user_id} + params: dict[str, Any] = {} + self._add_if_field_not_empty(params, "user_id", user_id) + self._add_if_field_not_empty(params, "agent_user_id", agent_user_id) + res = self._get_resources( "agent_assignments", limit=limit, @@ -120,7 +124,9 @@ def create_agent_assignment( ``__ """ - payload = {"user_id": user_id, "agent_user_id": agent_user_id, "is_active": is_active} + payload: dict[str, Any] = {"user_id": user_id, "agent_user_id": agent_user_id} + self._add_if_field_not_empty(payload, "is_active", is_active) + res = self._post_request( "agent_assignments", payload=payload, @@ -165,7 +171,9 @@ def update_agent_assignment( ``__ """ - payload = {"user_id": user_id, "agent_user_id": agent_user_id, "is_active": is_active} + payload: dict[str, Any] = {"user_id": user_id, "agent_user_id": agent_user_id} + self._add_if_field_not_empty(payload, "is_active", is_active) + res = self._put_request( "agent_assignments", id, diff --git a/parsons/solidarity_tech/st_automation_enrollments.py b/parsons/solidarity_tech/st_automation_enrollments.py index 2f371cc487..a9123d15c6 100644 --- a/parsons/solidarity_tech/st_automation_enrollments.py +++ b/parsons/solidarity_tech/st_automation_enrollments.py @@ -1,4 +1,5 @@ import logging +from typing import Any from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -32,7 +33,8 @@ def enroll_user_in_automation( ``__ """ - payload = {"automation_id": automation_id, "user_id": user_id} + payload: dict[str, Any] = {"automation_id": automation_id, "user_id": user_id} + res = self._post_request( "automation_enrollments", payload=payload, diff --git a/parsons/solidarity_tech/st_calls.py b/parsons/solidarity_tech/st_calls.py index e6e2d2ba40..b263a279ba 100644 --- a/parsons/solidarity_tech/st_calls.py +++ b/parsons/solidarity_tech/st_calls.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -44,7 +45,8 @@ def get_calls( ``__ """ - params = {"user_id": user_id} + params: dict[str, Any] = {"user_id": user_id} + res = self._get_resources( "calls", limit=limit, diff --git a/parsons/solidarity_tech/st_chapter_phone_numbers.py b/parsons/solidarity_tech/st_chapter_phone_numbers.py index 15b0301dc2..03100d5638 100644 --- a/parsons/solidarity_tech/st_chapter_phone_numbers.py +++ b/parsons/solidarity_tech/st_chapter_phone_numbers.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -40,7 +41,8 @@ def get_chapter_phone_numbers( ``__ """ - params = {"chapter_id": chapter_id} + params: dict[str, Any] = {"chapter_id": chapter_id} + res = self._get_resources( "chapter_phone_numbers", limit=limit, diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index b03aba3c17..0d8d8ae3b3 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -1,9 +1,10 @@ import logging from datetime import datetime +from typing import Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_literals import FieldType, ScopeType +from parsons.solidarity_tech.solidarity_tech_enums import FieldType, ScopeType logger = logging.getLogger(__name__) @@ -46,7 +47,12 @@ def get_custom_user_properties( ``__ """ - params = {"scope_id": scope_id, "scope_type": scope_type} + params: dict[str, Any] = {} + if scope_id is not None: + params["scope_id"] = scope_id + if scope_type is not None: + params["scope_type"] = scope_type.value + res = self._get_resources( "custom_user_properties", limit=limit, @@ -102,14 +108,12 @@ def create_custom_user_property( ``__ """ - payload = { - "label": label, - "description": description, - "field_type": field_type, - "options": options, - "scope_type": scope_type, - "scope_id": scope_id, - } + payload: dict[str, Any] = {"label": label, "field_type": field_type.value} + self._add_if_field_not_empty(payload, "description", description) + self._add_if_field_not_empty(payload, "options", options) + self._add_if_field_not_empty(payload, "scope_type", scope_type) + self._add_if_field_not_empty(payload, "scope_id", scope_id) + res = self._post_request( "custom_user_properties", payload=payload, @@ -193,10 +197,9 @@ def create_custom_user_property_option( ``__ """ - payload = { - "label": label, - "value": value, - } + payload: dict[str, Any] = {"label": label} + self._add_if_field_not_empty(payload, "value", value) + res = self._post_request( f"custom_user_properties/{id}/options", payload=payload, diff --git a/parsons/solidarity_tech/st_emails.py b/parsons/solidarity_tech/st_emails.py index 719643fb28..291de791c5 100644 --- a/parsons/solidarity_tech/st_emails.py +++ b/parsons/solidarity_tech/st_emails.py @@ -1,4 +1,5 @@ import logging +from typing import Any from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -54,18 +55,19 @@ def send_one_off_email( ``__ """ - email_params = { + params: dict[str, Any] = { "user_id": user_id, "subject": subject, "body_html": body_html, - "body_plain": body_plain, - "email_sender_id": email_sender_id, - "reply_to": reply_to, - "attachment_urls": attachment_urls, "track_opens": track_opens, "track_clicks": track_clicks, } - res = self._post_request("emails", params=email_params) + self._add_if_field_not_empty(params, "body_plain", body_plain) + self._add_if_field_not_empty(params, "email_sender_id", email_sender_id) + self._add_if_field_not_empty(params, "reply_to", reply_to) + self._add_if_field_not_empty(params, "attachment_urls", attachment_urls) + + res = self._post_request("emails", params=params) expected_responses = { 201: (True, "email sent successfully"), diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index 02eaaedd92..136f1d2132 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any import numpy as np @@ -45,7 +46,10 @@ def get_event_attendances( ``__ """ - params = {"event_id": event_id, "session_id": session_id} + params: dict[str, Any] = {} + self._add_if_field_not_empty(params, "event_id", event_id) + self._add_if_field_not_empty(params, "session_id", session_id) + res = self._get_resources( "event_attendances", limit=limit, @@ -91,12 +95,13 @@ def create_event_attendance( ``__ """ - payload = { + payload: dict[str, Any] = { "attended": attended, "event_id": event_id, "event_session_id": event_session_id, "user_id": user_id, } + res = self._post_request( "event_attendances", payload=payload, diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index bbffa0dca9..dd30c5ecee 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -1,11 +1,12 @@ import logging from datetime import datetime +from typing import Any import numpy as np from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_literals import AttendanceType +from parsons.solidarity_tech.solidarity_tech_enums import AttendanceStatus logger = logging.getLogger(__name__) @@ -52,12 +53,11 @@ def get_event_rsvps( ``__ """ - params = { - "event_id": event_id, - "session_id": session_id, - "user_id": user_id, - "full_user_payload": full_user_payload, - } + params: dict[str, Any] = {"full_user_payload": full_user_payload} + self._add_if_field_not_empty(params, "event_id", event_id) + self._add_if_field_not_empty(params, "session_id", session_id) + self._add_if_field_not_empty(params, "user_id", user_id) + res = self._get_resources( "event_rsvps", limit=limit, @@ -96,7 +96,8 @@ def get_event_rsvp( ``__ """ - params = {"full_user_payload": full_user_payload} + params: dict[str, Any] = {"full_user_payload": full_user_payload} + res = self._get_single_resource("event_rsvps", id, params=params) expected_responses = { @@ -111,7 +112,7 @@ def create_event_rsvp( self, event_id: np.int64, event_session_id: np.int64, - is_attending: AttendanceType, + is_attending: AttendanceStatus, agent_user_id: np.int64 | None, user_id: np.int64 | None = None, is_confirmed: bool | None = None, @@ -154,17 +155,18 @@ def create_event_rsvp( ``__ """ - payload = { - "is_attending": is_attending, + payload: dict[str, Any] = { + "is_attending": is_attending.value, "agent_user_id": agent_user_id, "event_id": event_id, "event_session_id": event_session_id, - "user_id": user_id, - "is_confirmed": is_confirmed, - "source": source, - "source_system": source_system, "skip_email_confirmation": skip_email_confirmation, } + self._add_if_field_not_empty(payload, "user_id", user_id) + self._add_if_field_not_empty(payload, "is_confirmed", is_confirmed) + self._add_if_field_not_empty(payload, "source", source) + self._add_if_field_not_empty(payload, "source_system", source_system) + res = self._post_request( "event_rsvps", payload=payload, additional_headers={"content-type": "application/json"} ) @@ -178,7 +180,7 @@ def create_event_rsvp( def update_event_rsvp( self, id: int, - is_attending: AttendanceType | None = None, + is_attending: AttendanceStatus | None = None, is_confirmed: bool | None = None, agent_user_id: np.int64 | None = None, source: str | None = None, @@ -213,13 +215,13 @@ def update_event_rsvp( ``__ """ - payload = { - is_attending: is_attending, - is_confirmed: is_confirmed, - agent_user_id: agent_user_id, - source: source, - source_system: source_system, - } + payload: dict[str, Any] = {} + self._add_if_field_not_empty(payload, "is_attending", is_attending) + self._add_if_field_not_empty(payload, "is_confirmed", is_confirmed) + self._add_if_field_not_empty(payload, "agent_user_id", agent_user_id) + self._add_if_field_not_empty(payload, "source", source) + self._add_if_field_not_empty(payload, "source_system", source_system) + res = self._put_request( "event_rsvps", id, diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index b1499f5429..97fb689a7e 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -1,11 +1,12 @@ import logging from datetime import datetime +from typing import Any, Literal import numpy as np from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_literals import EventType +from parsons.solidarity_tech.solidarity_tech_enums import EventType logger = logging.getLogger(__name__) @@ -82,18 +83,19 @@ def get_event_sessions( if isinstance(event_tags, list): event_tags = ",".join(str(tag) for tag in event_tags) - params = { + params: dict[str, Any] = { "event_id": event_id, - "upcoming": upcoming, - "starts_after": starts_after, - "starts_before": starts_before, - "chapter_id": chapter_id, - "event_tags": event_tags, - "include_rsvp_counts": include_rsvp_counts, - "include_confirmed_counts": include_confirmed_counts, - "include_hosts": include_hosts, - "count": count, } + self._add_if_field_not_empty(params, "upcoming", upcoming) + self._add_if_field_not_empty(params, "starts_after", starts_after) + self._add_if_field_not_empty(params, "starts_before", starts_before) + self._add_if_field_not_empty(params, "chapter_id", chapter_id) + self._add_if_field_not_empty(params, "event_tags", event_tags) + self._add_if_field_not_empty(params, "include_rsvp_counts", include_rsvp_counts) + self._add_if_field_not_empty(params, "include_confirmed_counts", include_confirmed_counts) + self._add_if_field_not_empty(params, "include_hosts", include_hosts) + self._add_if_field_not_empty(params, "count", count) + res = self._get_resources( "event_sessions", limit=limit, @@ -113,7 +115,7 @@ def create_event_sessions( start_time: np.int64, end_time: np.int64, title: str, - event_type: EventType | None = None, + event_type: Literal[EventType.VIRTUAL, EventType.IN_PERSON] | None = None, location_name: str | None = None, location_data: dict[str, str] | None = None, location_address: str | None = None, @@ -170,21 +172,22 @@ def create_event_sessions( ``__ """ - payload = { + payload: dict[str, Any] = { "event_id": event_id, "start_time": start_time, "end_time": end_time, - "event_type": event_type, "title": title, - "location_name": location_name, - "location_data": location_data, - "location_address": location_address, - "show_rsvp_bar": show_rsvp_bar, - "show_title_in_form": show_title_in_form, - "note": note, - "max_capacity": max_capacity, - "tags": tags, } + self._add_if_field_not_empty(payload, "event_type", event_type) + self._add_if_field_not_empty(payload, "location_name", location_name) + self._add_if_field_not_empty(payload, "location_data", location_data) + self._add_if_field_not_empty(payload, "location_address", location_address) + self._add_if_field_not_empty(payload, "show_rsvp_bar", show_rsvp_bar) + self._add_if_field_not_empty(payload, "show_title_in_form", show_title_in_form) + self._add_if_field_not_empty(payload, "note", note) + self._add_if_field_not_empty(payload, "max_capacity", max_capacity) + self._add_if_field_not_empty(payload, "tags", tags) + res = self._post_request( "event_rsvps", payload=payload, additional_headers={"content-type": "application/json"} ) @@ -222,7 +225,7 @@ def get_event_session( ``__ """ - params = {"include_hosts": include_hosts} + params: dict[str, Any] = {"include_hosts": include_hosts} res = self._get_single_resource("event_sessions", id, params=params) expected_responses = { @@ -292,19 +295,19 @@ def update_event_session( ``__ """ - payload = { - "start_time": start_time, - "end_time": end_time, - "title": title, - "location_name": location_name, - "location_address": location_address, - "location_data": location_data, - "show_rsvp_bar": show_rsvp_bar, - "show_title_in_form": show_title_in_form, - "note": note, - "max_capacity": max_capacity, - "tags": tags, - } + payload: dict[str, Any] = {} + self._add_if_field_not_empty(payload, "start_time", start_time) + self._add_if_field_not_empty(payload, "end_time", end_time) + self._add_if_field_not_empty(payload, "title", title) + self._add_if_field_not_empty(payload, "location_name", location_name) + self._add_if_field_not_empty(payload, "location_address", location_address) + self._add_if_field_not_empty(payload, "location_data", location_data) + self._add_if_field_not_empty(payload, "show_rsvp_bar", show_rsvp_bar) + self._add_if_field_not_empty(payload, "show_title_in_form", show_title_in_form) + self._add_if_field_not_empty(payload, "note", note) + self._add_if_field_not_empty(payload, "max_capacity", max_capacity) + self._add_if_field_not_empty(payload, "tags", tags) + res = self._put_request( "event_sessions", id, @@ -386,11 +389,10 @@ def add_event_host( ``__ """ - payload = { - "user_id": user_id, - } + payload: dict[str, Any] = {"user_id": user_id} + res = self._post_request( - "event_sessions", + f"event_sessions/{id}/hosts", payload=payload, additional_headers={"content-type": "application/json"}, ) diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index 17d49f21c3..51a299913c 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -1,12 +1,12 @@ import logging from datetime import datetime -from typing import Literal +from typing import Any import numpy as np from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_literals import EventType, ScopeType +from parsons.solidarity_tech.solidarity_tech_enums import EventType, ScopeType logger = logging.getLogger(__name__) @@ -56,10 +56,10 @@ def get_events( ``__ """ - params = { - "scope_id": scope_id, - "scope_type": scope_type, - } + params: dict[str, Any] = {} + self._add_if_field_not_empty(params, "scope_id", scope_id) + self._add_if_field_not_empty(params, "scope_type", scope_type) + res = self._get_resources( "events", limit=limit, @@ -76,7 +76,7 @@ def get_events( def create_event( self, title: str, - event_type: EventType | Literal["hybrid"], + event_type: EventType, start_time: np.int64, end_time: np.int64, scope_id: str, @@ -141,26 +141,28 @@ def create_event( ``__ """ - payload = { + payload: dict[str, Any] = { "title": title, "event_type": event_type, "start_time": start_time, "end_time": end_time, - "location_address": location_address, - "virtual_url": virtual_url, - "location_name": location_name, "scope_id": scope_id, "scope_type": scope_type, - "session_title": session_title, - "tags": tags, - "max_capacity": max_capacity, - "latitude": latitude, - "longitude": longitude, - "skip_duplicate_check": skip_duplicate_check, } + self._add_if_field_not_empty(payload, "location_address", location_address) + self._add_if_field_not_empty(payload, "virtual_url", virtual_url) + self._add_if_field_not_empty(payload, "location_name", location_name) + self._add_if_field_not_empty(payload, "session_title", session_title) + self._add_if_field_not_empty(payload, "tags", tags) + self._add_if_field_not_empty(payload, "max_capacity", max_capacity) + self._add_if_field_not_empty(payload, "latitude", latitude) + self._add_if_field_not_empty(payload, "longitude", longitude) + self._add_if_field_not_empty(payload, "skip_duplicate_check", skip_duplicate_check) + res = self._post_request( "events", payload=payload, additional_headers={"content-type": "application/json"} ) + expected_responses = { 201: (True, "event created"), 404: (False, "scope not found"), @@ -202,7 +204,8 @@ def get_event( ``__ """ - params = {"include_hosts": include_hosts} + params: dict[str, Any] = {"include_hosts": include_hosts} + res = self._get_single_resource("event_sessions", id, params=params) expected_responses = { diff --git a/parsons/solidarity_tech/st_field_survey_urls.py b/parsons/solidarity_tech/st_field_survey_urls.py index 58a9269119..2c1d26c9fa 100644 --- a/parsons/solidarity_tech/st_field_survey_urls.py +++ b/parsons/solidarity_tech/st_field_survey_urls.py @@ -1,5 +1,5 @@ import logging -from typing import Literal +from typing import Any, Literal from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -40,11 +40,12 @@ def generate_field_survey_url( ``__ """ - payload = { + payload: dict[str, Any] = { "user_id": user_id, "agent_user_id": agent_user_id, "page_id": page_id, } + res = self._post_request( "field_survey_urls", payload=payload, diff --git a/parsons/solidarity_tech/st_pages.py b/parsons/solidarity_tech/st_pages.py index 4982b7b0e6..cac4293615 100644 --- a/parsons/solidarity_tech/st_pages.py +++ b/parsons/solidarity_tech/st_pages.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -42,7 +43,8 @@ def get_pages( ``__ """ - params = {"include_action_counts": include_action_counts} + params: dict[str, Any] = {"include_action_counts": include_action_counts} + res = self._get_resources( "pages", limit=limit, @@ -87,7 +89,8 @@ def get_page( ``__ """ - params = {"include_action_counts": include_action_counts} + params: dict[str, Any] = {"include_action_counts": include_action_counts} + res = self._get_single_resource("pages", id, params=params) expected_responses = { diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py index 66e9b66bfb..9d4b738afc 100644 --- a/parsons/solidarity_tech/st_phonebanks.py +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -53,7 +54,9 @@ def get_phonebanks( if isinstance(ids, list): ids = ",".join(str(id) for id in ids) - params = {"event_id": event_id, "ids": ids, "include_stats": include_stats} + params: dict[str, Any] = {"event_id": event_id, "include_stats": include_stats} + self._add_if_field_not_empty(params, "ids", ids) + res = self._get_resources( "phonebanks", limit=limit, diff --git a/parsons/solidarity_tech/st_scheduled_calls.py b/parsons/solidarity_tech/st_scheduled_calls.py index b36ddc8295..b6d6059155 100644 --- a/parsons/solidarity_tech/st_scheduled_calls.py +++ b/parsons/solidarity_tech/st_scheduled_calls.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -43,7 +44,10 @@ def get_scheduled_calls( ``__ """ - params = {"user_id": user_id, "agent_user_id": agent_user_id} + params: dict[str, Any] = {} + self._add_if_field_not_empty(params, "user_id", user_id) + self._add_if_field_not_empty(params, "agent_user_id", agent_user_id) + res = self._get_resources( "scheduled_calls", limit=limit, diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py index 18bbc223cd..07b1497284 100644 --- a/parsons/solidarity_tech/st_scheduled_tasks.py +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any import numpy as np @@ -45,7 +46,10 @@ def get_scheduled_tasks( ``__ """ - params = {"user_id": user_id, "agent_user_id": agent_user_id} + params: dict[str, Any] = {} + self._add_if_field_not_empty(params, "user_id", user_id) + self._add_if_field_not_empty(params, "agent_user_id", agent_user_id) + res = self._get_resources( "scheduled_tasks", limit=limit, @@ -93,8 +97,8 @@ def get_scheduled_task( def create_scheduled_task( self, - due_at: str | int | datetime, - remind_at: str | int | datetime | None = None, + due_at: str | int | float | datetime, + remind_at: str | int | float | datetime | None = None, agent_user_id: np.int64 | None = None, user_id: np.int64 | None = None, notes: str | None = None, @@ -133,14 +137,18 @@ def create_scheduled_task( ``__ """ - payload = { - "due_at": due_at.timestamp() if isinstance(due_at, datetime) else due_at, - "remind_at": remind_at.timestamp() if isinstance(remind_at, datetime) else remind_at, - "agent_user_id": agent_user_id, - "user_id": user_id, - "notes": notes, - "marked_as_completed": marked_as_completed, - } + if isinstance(due_at, datetime): + due_at = due_at.timestamp() + if isinstance(remind_at, datetime): + remind_at = remind_at.timestamp() + + payload: dict[str, Any] = {"due_at": due_at} + self._add_if_field_not_empty(payload, "remind_at", remind_at) + self._add_if_field_not_empty(payload, "agent_user_id", agent_user_id) + self._add_if_field_not_empty(payload, "user_id", user_id) + self._add_if_field_not_empty(payload, "notes", notes) + self._add_if_field_not_empty(payload, "marked_as_completed", marked_as_completed) + res = self._post_request( "scheduled_tasks", payload=payload, @@ -156,8 +164,8 @@ def create_scheduled_task( def update_scheduled_task( self, id: int, - due_at: str | int | datetime | None = None, - remind_at: str | int | datetime | None = None, + due_at: str | int | float | datetime | None = None, + remind_at: str | int | float | datetime | None = None, agent_user_id: np.int64 | None = None, user_id: np.int64 | None = None, notes: str | None = None, @@ -196,14 +204,19 @@ def update_scheduled_task( ``__ """ - payload = { - "due_at": due_at.timestamp() if isinstance(due_at, datetime) else due_at, - "remind_at": remind_at.timestamp() if isinstance(remind_at, datetime) else remind_at, - "agent_user_id": agent_user_id, - "user_id": user_id, - "notes": notes, - "marked_as_completed": marked_as_completed, - } + if isinstance(due_at, datetime): + due_at = due_at.timestamp() + if isinstance(remind_at, datetime): + remind_at = remind_at.timestamp() + + payload: dict[str, Any] = {} + self._add_if_field_not_empty(payload, "due_at", due_at) + self._add_if_field_not_empty(payload, "remind_at", remind_at) + self._add_if_field_not_empty(payload, "agent_user_id", agent_user_id) + self._add_if_field_not_empty(payload, "user_id", user_id) + self._add_if_field_not_empty(payload, "notes", notes) + self._add_if_field_not_empty(payload, "marked_as_completed", marked_as_completed) + res = self._put_request( "scheduled_tasks", id, diff --git a/parsons/solidarity_tech/st_task_agents.py b/parsons/solidarity_tech/st_task_agents.py index 4b030306ff..52c5985f44 100644 --- a/parsons/solidarity_tech/st_task_agents.py +++ b/parsons/solidarity_tech/st_task_agents.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any import numpy as np @@ -42,7 +43,9 @@ def get_task_agents( ``__ """ - params = {"task_id": task_id} + params: dict[str, Any] = {} + self._add_if_field_not_empty(params, "task_id", task_id) + res = self._get_resources( "task_agents", limit=limit, @@ -114,10 +117,8 @@ def create_task_agent( ``__ """ - payload = { - "user_id": user_id, - "task_id": task_id, - } + payload: dict[str, Any] = {"user_id": user_id, "task_id": task_id} + res = self._post_request( "task_agents", payload=payload, additional_headers={"content-type": "application/json"} ) diff --git a/parsons/solidarity_tech/st_task_assignments.py b/parsons/solidarity_tech/st_task_assignments.py index b9880e3a67..ffe304d947 100644 --- a/parsons/solidarity_tech/st_task_assignments.py +++ b/parsons/solidarity_tech/st_task_assignments.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any import numpy as np @@ -45,7 +46,8 @@ def get_task_assignments( ``__ """ - params = {"task_id": task_id, "agent_user_id": agent_user_id} + params: dict[str, Any] = {"task_id": task_id, "agent_user_id": agent_user_id} + res = self._get_resources( "task_assignments", limit=limit, @@ -122,11 +124,12 @@ def create_task_assignment( ``__ """ - payload = { + payload: dict[str, Any] = { "user_id": user_id, "task_id": task_id, - "agent_user_id": agent_user_id, } + self._add_if_field_not_empty(payload, "agent_user_id", agent_user_id) + res = self._post_request( "task_assignments", payload=payload, @@ -162,9 +165,9 @@ def update_task_assignment( ``__ """ - payload = { - "agent_user_id": agent_user_id, - } + payload: dict[str, Any] = {} + self._add_if_field_not_empty(payload, "agent_user_id", agent_user_id) + res = self._put_request( "scheduled_tasks", id, diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py index 7e5b4a69d0..1e5e2d7ede 100644 --- a/parsons/solidarity_tech/st_team_members.py +++ b/parsons/solidarity_tech/st_team_members.py @@ -1,9 +1,10 @@ import logging from datetime import datetime +from typing import Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_literals import InviteType, ScopeType +from parsons.solidarity_tech.solidarity_tech_enums import InviteType, ScopeType logger = logging.getLogger(__name__) @@ -107,19 +108,20 @@ def create_team_member( if not member_id and not phone_number and not email: raise ValueError("One of member_id, phone_number, or email is required.") - payload = { - "member_id": member_id, - "phone_number": phone_number, - "email": email, - "full_name": full_name, - "first_name": first_name, - "last_name": last_name, + payload: dict[str, Any] = { "role_id": role_id, - "scope_type": scope_type, + "scope_type": scope_type.value, "scope_id": scope_id, - "invite_via": invite_via, - "task_id": task_id, + "invite_via": invite_via.value, } + self._add_if_field_not_empty(payload, "member_id", member_id) + self._add_if_field_not_empty(payload, "phone_number", phone_number) + self._add_if_field_not_empty(payload, "email", email) + self._add_if_field_not_empty(payload, "full_name", full_name) + self._add_if_field_not_empty(payload, "first_name", first_name) + self._add_if_field_not_empty(payload, "last_name", last_name) + self._add_if_field_not_empty(payload, "task_id", task_id) + res = self._post_request( "team_members", payload=payload, additional_headers={"content-type": "application/json"} ) @@ -161,11 +163,12 @@ def update_team_member( ``__ """ - payload = { + payload: dict[str, Any] = { "role_id": role_id, - "scope_type": scope_type, + "scope_type": scope_type.value, "scope_id": scope_id, } + res = self._put_request( "team_members", id, diff --git a/parsons/solidarity_tech/st_text_templates.py b/parsons/solidarity_tech/st_text_templates.py index 3491f970c4..e940b06284 100644 --- a/parsons/solidarity_tech/st_text_templates.py +++ b/parsons/solidarity_tech/st_text_templates.py @@ -1,11 +1,12 @@ import logging from datetime import datetime +from typing import Any import numpy as np from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_literals import ScopeType +from parsons.solidarity_tech.solidarity_tech_enums import ScopeType logger = logging.getLogger(__name__) @@ -43,7 +44,8 @@ def get_text_templates( ``__ """ - params = {"event_id": event_id} + params: dict[str, Any] = {"event_id": event_id} + res = self._get_resources( "text_templates", limit=limit, @@ -126,13 +128,14 @@ def create_text_template( ``__ """ - payload = { - "name": name, + payload: dict[str, Any] = { "scope_id": scope_id, - "scope_type": scope_type, - "template": template, - "event_id": event_id, + "scope_type": scope_type.value, } + self._add_if_field_not_empty(payload, "name", name) + self._add_if_field_not_empty(payload, "template", template) + self._add_if_field_not_empty(payload, "event_id", event_id) + res = self._post_request( "text_templates", payload=payload, @@ -185,13 +188,13 @@ def update_text_template( ``__ """ - payload = { - "name": name, - "scope_id": scope_id, - "scope_type": scope_type, - "template": template, - "event_id": event_id, - } + payload: dict[str, Any] = {} + self._add_if_field_not_empty(payload, "name", name) + self._add_if_field_not_empty(payload, "scope_id", scope_id) + self._add_if_field_not_empty(payload, "scope_type", scope_type) + self._add_if_field_not_empty(payload, "template", template) + self._add_if_field_not_empty(payload, "event_id", event_id) + res = self._put_request( "text_templates", id, diff --git a/parsons/solidarity_tech/st_textbanks.py b/parsons/solidarity_tech/st_textbanks.py index 8c5115a3fc..e3385e0fab 100644 --- a/parsons/solidarity_tech/st_textbanks.py +++ b/parsons/solidarity_tech/st_textbanks.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -53,7 +54,9 @@ def get_textbanks( if isinstance(ids, list): ids = ",".join(str(id) for id in ids) - params = {"event_id": event_id, "ids": ids, "include_stats": include_stats} + params: dict[str, Any] = {"event_id": event_id, "include_stats": include_stats} + self._add_if_field_not_empty(params, "ids", ids) + res = self._get_resources( "textbanks", limit=limit, diff --git a/parsons/solidarity_tech/st_texts.py b/parsons/solidarity_tech/st_texts.py index 3f45b0822e..0305ec3a9d 100644 --- a/parsons/solidarity_tech/st_texts.py +++ b/parsons/solidarity_tech/st_texts.py @@ -1,5 +1,6 @@ import logging from datetime import datetime +from typing import Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -40,7 +41,9 @@ def get_texts( ``__ """ - params = {"user_id": user_id} + params: dict[str, Any] = {} + self._add_if_field_not_empty(params, "user_id", user_id) + res = self._get_resources( "texts", limit=limit, @@ -90,13 +93,14 @@ def send_text( ``__ """ - params = { + params: dict[str, Any] = { "user_id": user_id, "body": body, - "media_urls": media_urls, - "attach_contact_card": attach_contact_card, - "shorten_urls": shorten_urls, } + self._add_if_field_not_empty(params, "media_urls", media_urls) + self._add_if_field_not_empty(params, "attach_contact_card", attach_contact_card) + self._add_if_field_not_empty(params, "shorten_urls", shorten_urls) + res = self._post_request( "texts", params=params, diff --git a/parsons/solidarity_tech/st_user_actions.py b/parsons/solidarity_tech/st_user_actions.py index 2798139f67..872f4801a1 100644 --- a/parsons/solidarity_tech/st_user_actions.py +++ b/parsons/solidarity_tech/st_user_actions.py @@ -1,6 +1,6 @@ import logging from datetime import datetime -from typing import Literal +from typing import Any, Literal import numpy as np @@ -64,11 +64,11 @@ def get_user_actions( ``__ """ - params = { - "user_id": user_id, - "page_id": page_id, - "group_by": group_by, - } + params: dict[str, Any] = {} + self._add_if_field_not_empty(params, "user_id", user_id) + self._add_if_field_not_empty(params, "page_id", page_id) + self._add_if_field_not_empty(params, "group_by", group_by) + res = self._get_resources( "user_actions", limit=limit, @@ -132,12 +132,11 @@ def create_user_action( ): raise ValueError("Either user_id, phone_number, or email must be provided") - payload = { - "page_id": page_id, - "user_id": user_id, - "created_at": created_at, - "data": data, - } + payload: dict[str, Any] = {"page_id": page_id} + self._add_if_field_not_empty(payload, "user_id", user_id) + self._add_if_field_not_empty(payload, "created_at", created_at) + self._add_if_field_not_empty(payload, "data", data) + res = self._post_request( "user_actions", payload=payload, diff --git a/parsons/solidarity_tech/st_user_lists.py b/parsons/solidarity_tech/st_user_lists.py index 294cf2a374..9547338370 100644 --- a/parsons/solidarity_tech/st_user_lists.py +++ b/parsons/solidarity_tech/st_user_lists.py @@ -1,12 +1,13 @@ import logging import numbers from datetime import datetime +from typing import Any import numpy as np from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_literals import ScopeType +from parsons.solidarity_tech.solidarity_tech_enums import ScopeType CompareValueType = str | numbers.Rational | bool QueryParamType = dict[ @@ -131,14 +132,15 @@ def create_user_list( ``__ """ - payload = { + payload: dict[str, Any] = { "name": name, "scope_id": scope_id, - "scope_type": scope_type, - "event_id": event_id, - "user_id": user_id, - "parameters": parameters, + "scope_type": scope_type.value, } + self._add_if_field_not_empty(payload, "event_id", event_id) + self._add_if_field_not_empty(payload, "user_id", user_id) + self._add_if_field_not_empty(payload, "parameters", parameters) + res = self._post_request( "user_lists", payload=payload, additional_headers={"content-type": "application/json"} ) @@ -190,13 +192,13 @@ def update_user_list( ``__ """ - payload = { - "name": name, - "scope_id": scope_id, - "scope_type": scope_type, - "parameters": parameters, - "event_id": event_id, - } + payload: dict[str, Any] = {} + self._add_if_field_not_empty(payload, "name", name) + self._add_if_field_not_empty(payload, "scope_id", scope_id) + self._add_if_field_not_empty(payload, "scope_type", scope_type) + self._add_if_field_not_empty(payload, "parameters", parameters) + self._add_if_field_not_empty(payload, "event_id", event_id) + res = self._put_request( "user_lists", id, diff --git a/parsons/solidarity_tech/st_user_notes.py b/parsons/solidarity_tech/st_user_notes.py index 71ea54a9eb..cf09b1dc00 100644 --- a/parsons/solidarity_tech/st_user_notes.py +++ b/parsons/solidarity_tech/st_user_notes.py @@ -1,7 +1,8 @@ import logging +from typing import Any from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_literals import InteractionType +from parsons.solidarity_tech.solidarity_tech_enums import InteractionType logger = logging.getLogger(__name__) @@ -47,14 +48,15 @@ def create_user_note( ``__ """ - params = { + params: dict[str, Any] = { "user_id": user_id, - "agent_id": agent_id, "content": content, - "created_at": created_at, "restricted": restricted, - "interaction_method": interaction_method, } + self._add_if_field_not_empty(params, "agent_id", agent_id) + self._add_if_field_not_empty(params, "created_at", created_at) + self._add_if_field_not_empty(params, "interaction_method", interaction_method) + res = self._post_request("user_notes", params=params) expected_responses = { @@ -94,7 +96,9 @@ def delete_user_note( ``__ """ - params = {"user_id": user_id, "agent_id": agent_id} + params: dict[str, Any] = {"user_id": user_id} + self._add_if_field_not_empty(params, "agent_id", agent_id) + res = self._del_request("user_notes", id, params=params) expected_responses = { diff --git a/parsons/solidarity_tech/st_user_relationships.py b/parsons/solidarity_tech/st_user_relationships.py index 9a79f87d79..1dbb76303d 100644 --- a/parsons/solidarity_tech/st_user_relationships.py +++ b/parsons/solidarity_tech/st_user_relationships.py @@ -1,6 +1,6 @@ import logging import numbers -from typing import Literal +from typing import Any, Literal from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase @@ -37,7 +37,8 @@ def get_user_relationships( ``__ """ - params = {"user_id": user_id} + params: dict[str, Any] = {"user_id": user_id} + res = self._get_resources( "user_relationships", params=params, @@ -81,11 +82,12 @@ def create_user_relationship( ``__ """ - params = { + params: dict[str, Any] = { "user_id": user_id, "related_user_id": related_user_id, "relationship_type": relationship_type, } + res = self._post_request("user_relationships", params=params) expected_responses = { @@ -120,7 +122,8 @@ def delete_user_relationship( ``__ """ - params = {"user_id": user_id} + params: dict[str, Any] = {"user_id": user_id} + res = self._del_request( "user_relationships", id, diff --git a/parsons/solidarity_tech/st_users.py b/parsons/solidarity_tech/st_users.py index 6bc1a4ecb6..91e2ea18d1 100644 --- a/parsons/solidarity_tech/st_users.py +++ b/parsons/solidarity_tech/st_users.py @@ -62,11 +62,11 @@ def get_users( if isinstance(user_list_ids, list): user_list_ids = ",".join(str(id) for id in user_list_ids) - params = { - "user_list_ids": user_list_ids, - "phone_number": phone_number, - "email": email, - } + params: dict[str, Any] = {} + self._add_if_field_not_empty(params, "user_list_ids", user_list_ids) + self._add_if_field_not_empty(params, "phone_number", phone_number) + self._add_if_field_not_empty(params, "email", email) + res = self._get_resources( "users", limit=limit, @@ -240,32 +240,33 @@ def create_user( if isinstance(timezone, ZoneInfo): timezone = str(timezone.key) - payload = { - "phone_number": phone_number, - "email": email, - "first_name": first_name, - "last_name": last_name, - "alternate_name": alternate_name, - "preferred_language": preferred_language, - "second_language": second_language, - "chapter_id": chapter_id, - "chapter_ids": chapter_ids, - "referred_by_user_id": referred_by_user_id, - "custom_user_properties": custom_user_properties, + payload: dict[str, Any] = { "append_custom_user_properties": append_custom_user_properties, - "add_tags": add_tags, - "remove_tags": remove_tags, - "donation_charge": donation_charge, - "address": address, - "assessment": assessment, - "sms_permission": sms_permission, - "call_permission": call_permission, - "email_permission": email_permission, - "timezone": timezone, "require_contact_info": require_contact_info, "phone_number_textable_validation": phone_number_textable_validation, - "lookup_key": lookup_key, } + self._add_if_field_not_empty(payload, "phone_number", phone_number) + self._add_if_field_not_empty(payload, "email", email) + self._add_if_field_not_empty(payload, "first_name", first_name) + self._add_if_field_not_empty(payload, "last_name", last_name) + self._add_if_field_not_empty(payload, "alternate_name", alternate_name) + self._add_if_field_not_empty(payload, "preferred_language", preferred_language) + self._add_if_field_not_empty(payload, "second_language", second_language) + self._add_if_field_not_empty(payload, "chapter_id", chapter_id) + self._add_if_field_not_empty(payload, "chapter_ids", chapter_ids) + self._add_if_field_not_empty(payload, "referred_by_user_id", referred_by_user_id) + self._add_if_field_not_empty(payload, "custom_user_properties", custom_user_properties) + self._add_if_field_not_empty(payload, "add_tags", add_tags) + self._add_if_field_not_empty(payload, "remove_tags", remove_tags) + self._add_if_field_not_empty(payload, "donation_charge", donation_charge) + self._add_if_field_not_empty(payload, "address", address) + self._add_if_field_not_empty(payload, "assessment", assessment) + self._add_if_field_not_empty(payload, "sms_permission", sms_permission) + self._add_if_field_not_empty(payload, "call_permission", call_permission) + self._add_if_field_not_empty(payload, "email_permission", email_permission) + self._add_if_field_not_empty(payload, "timezone", timezone) + self._add_if_field_not_empty(payload, "lookup_key", lookup_key) + res = self._post_request( "users", payload=payload, additional_headers={"content-type": "application/json"} ) @@ -393,31 +394,32 @@ def update_user( if isinstance(timezone, ZoneInfo): timezone = str(timezone.key) - payload = { - "phone_number": phone_number, - "clear_phone_number": clear_phone_number, - "email": email, - "first_name": first_name, - "last_name": last_name, - "alternate_name": alternate_name, - "preferred_language": preferred_language, - "chapter_id": chapter_id, - "chapter_ids": chapter_ids, - "add_chapter_ids": add_chapter_ids, - "remove_chapter_ids": remove_chapter_ids, - "set_exclusive_chapter": set_exclusive_chapter, - "second_language": second_language, - "referred_by_user_id": referred_by_user_id, - "custom_user_properties": custom_user_properties, + payload: dict[str, Any] = { "append_custom_user_properties": append_custom_user_properties, - "address": address, - "assessment": assessment, - "sms_permission": sms_permission, - "call_permission": call_permission, - "email_permission": email_permission, - "timezone": timezone, - "donation_charge": donation_charge, } + self._add_if_field_not_empty(payload, "phone_number", phone_number) + self._add_if_field_not_empty(payload, "clear_phone_number", clear_phone_number) + self._add_if_field_not_empty(payload, "email", email) + self._add_if_field_not_empty(payload, "first_name", first_name) + self._add_if_field_not_empty(payload, "last_name", last_name) + self._add_if_field_not_empty(payload, "alternate_name", alternate_name) + self._add_if_field_not_empty(payload, "preferred_language", preferred_language) + self._add_if_field_not_empty(payload, "chapter_id", chapter_id) + self._add_if_field_not_empty(payload, "chapter_ids", chapter_ids) + self._add_if_field_not_empty(payload, "add_chapter_ids", add_chapter_ids) + self._add_if_field_not_empty(payload, "remove_chapter_ids", remove_chapter_ids) + self._add_if_field_not_empty(payload, "set_exclusive_chapter", set_exclusive_chapter) + self._add_if_field_not_empty(payload, "second_language", second_language) + self._add_if_field_not_empty(payload, "referred_by_user_id", referred_by_user_id) + self._add_if_field_not_empty(payload, "custom_user_properties", custom_user_properties) + self._add_if_field_not_empty(payload, "address", address) + self._add_if_field_not_empty(payload, "assessment", assessment) + self._add_if_field_not_empty(payload, "sms_permission", sms_permission) + self._add_if_field_not_empty(payload, "call_permission", call_permission) + self._add_if_field_not_empty(payload, "email_permission", email_permission) + self._add_if_field_not_empty(payload, "timezone", timezone) + self._add_if_field_not_empty(payload, "donation_charge", donation_charge) + res = self._put_request( "users", id, @@ -462,10 +464,11 @@ def merge_duplicate_users( if isinstance(user_ids, list): user_ids = ",".join(str(id) for id in user_ids) - payload = { + payload: dict[str, Any] = { "primary_user_id": primary_user_id, "user_ids": user_ids, } + res = self._post_request( "users/merge", payload=payload, From 2cc5a8b1995626095f7aa7960084432d26b66367 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 5 Aug 2026 22:13:05 -0400 Subject: [PATCH 73/94] Update with latest APIConnector + associated type changes --- .../solidarity_tech/solidarity_tech_base.py | 26 ++- parsons/solidarity_tech/st_activities.py | 7 +- .../solidarity_tech/st_agent_assignments.py | 7 +- parsons/solidarity_tech/st_calls.py | 7 +- .../st_chapter_phone_numbers.py | 7 +- .../st_custom_user_properties.py | 13 +- parsons/solidarity_tech/st_emails.py | 7 +- .../solidarity_tech/st_event_attendances.py | 7 +- parsons/solidarity_tech/st_event_rsvps.py | 9 +- parsons/solidarity_tech/st_event_sessions.py | 9 +- parsons/solidarity_tech/st_events.py | 9 +- parsons/solidarity_tech/st_pages.py | 9 +- parsons/solidarity_tech/st_phonebanks.py | 7 +- parsons/solidarity_tech/st_scheduled_calls.py | 7 +- parsons/solidarity_tech/st_scheduled_tasks.py | 7 +- parsons/solidarity_tech/st_task_agents.py | 7 +- .../solidarity_tech/st_task_assignments.py | 7 +- parsons/solidarity_tech/st_text_templates.py | 7 +- parsons/solidarity_tech/st_textbanks.py | 7 +- parsons/solidarity_tech/st_texts.py | 9 +- parsons/solidarity_tech/st_user_actions.py | 7 +- parsons/solidarity_tech/st_user_notes.py | 9 +- .../solidarity_tech/st_user_relationships.py | 11 +- parsons/solidarity_tech/st_users.py | 7 +- parsons/utilities/_api_connector_types.py | 56 ++++++ parsons/utilities/api_connector.py | 186 +++++++++++------- .../utilities/ratelimited_api_connector.py | 38 ---- pyproject.toml | 1 + 28 files changed, 312 insertions(+), 178 deletions(-) create mode 100644 parsons/utilities/_api_connector_types.py delete mode 100644 parsons/utilities/ratelimited_api_connector.py diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index b13c5b5e0b..e60d804e9e 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -5,16 +5,18 @@ from typing import Any, cast import numpy as np +import pyrate_limiter import requests -from pyrate_limiter import Duration, Rate +import requests_ratelimiter +from requests.structures import CaseInsensitiveDict from parsons.solidarity_tech.exceptions import STFailedResponseError, STUnexpectedResponseError from parsons.utilities import check_env -from parsons.utilities.ratelimited_api_connector import RateLimitedAPIConnector +from parsons.utilities.api_connector import APIConnector, _ParamsType logger = logging.getLogger(__name__) -ParamTypes = str | int | np.int64 | float | None +ParamsType = _ParamsType | np.int64 class SolidarityTechBase: @@ -29,10 +31,14 @@ def __init__(self, api_token: str | None = None) -> None: """ self.api_token = cast("str", check_env.check("SOLIDARITY_TECH_BEARER_KEY", api_token)) - self.headers = {"authorization": f"Bearer {self.api_token}"} + self.headers = CaseInsensitiveDict({"authorization": f"Bearer {self.api_token}"}) self.api_url = "https://api.solidarity.tech/v1" - self.api = RateLimitedAPIConnector( - self.api_url, headers=self.headers, ratelimit=Rate(60, Duration.SECOND * 30) + self.api = APIConnector( + self.api_url, + headers=self.headers, + ratelimiter=requests_ratelimiter.Limiter( + pyrate_limiter.Rate(60, pyrate_limiter.Duration.SECOND * 30) + ), ) def _get_resources(self, endpoint: str, **kwargs) -> requests.Response: @@ -73,7 +79,7 @@ def _get_resources(self, endpoint: str, **kwargs) -> requests.Response: "since": "_since", "include_count": "_include_count", } - params: dict[str, ParamTypes] = {} + params: dict[str, ParamsType] = {} for key, value in param_mapping.items(): if key in kwargs: params[value] = kwargs[key] @@ -103,7 +109,7 @@ def _get_single_resource(self, endpoint: str, id: int, **kwargs) -> requests.Res def _post_request( self, endpoint: str, - payload: Mapping[str, ParamTypes] | None = None, + payload: Mapping[str, ParamsType] | None = None, **kwargs, ) -> requests.Response: """Handle POST requests.""" @@ -114,7 +120,7 @@ def _put_request( self, endpoint: str, id: int, - payload: Mapping[str, ParamTypes] | None = None, + payload: Mapping[str, ParamsType] | None = None, **kwargs, ) -> requests.Response: """Handle PUT requests.""" @@ -126,7 +132,7 @@ def _del_request(self, endpoint: str, id: int | str, **kwargs) -> requests.Respo """Handle DEL requests.""" complete_endpoint = f"{endpoint}/{id}" logger.debug("Processing DEL request at endpoint: %s", complete_endpoint) - return self.api.request(url=complete_endpoint, req_type="DEL", **kwargs) + return self.api.request(url=complete_endpoint, req_type="DELETE", **kwargs) def _handle_status_codes( self, res: requests.Response, codes: dict[int, tuple[bool, str]] diff --git a/parsons/solidarity_tech/st_activities.py b/parsons/solidarity_tech/st_activities.py index b6cfa824f6..809b3283c4 100644 --- a/parsons/solidarity_tech/st_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -1,10 +1,13 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) ActionData = dict[str, int | str] @@ -53,7 +56,7 @@ def get_activities( ``__ """ - params: dict[str, Any] = {} + params: ParamsType = {} self._add_if_field_not_empty(params, "user_id", user_id) res = self._get_resources( diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index 4c65ba491f..9ce3d2577e 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -1,12 +1,15 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -46,7 +49,7 @@ def get_agent_assignments( ``__ """ - params: dict[str, Any] = {} + params: ParamsType = {} self._add_if_field_not_empty(params, "user_id", user_id) self._add_if_field_not_empty(params, "agent_user_id", agent_user_id) diff --git a/parsons/solidarity_tech/st_calls.py b/parsons/solidarity_tech/st_calls.py index b263a279ba..7b607fc975 100644 --- a/parsons/solidarity_tech/st_calls.py +++ b/parsons/solidarity_tech/st_calls.py @@ -1,10 +1,13 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) TranscriptData = dict[str, str | int] @@ -45,7 +48,7 @@ def get_calls( ``__ """ - params: dict[str, Any] = {"user_id": user_id} + params: ParamsType = {"user_id": user_id} res = self._get_resources( "calls", diff --git a/parsons/solidarity_tech/st_chapter_phone_numbers.py b/parsons/solidarity_tech/st_chapter_phone_numbers.py index 03100d5638..4e4dd13b6a 100644 --- a/parsons/solidarity_tech/st_chapter_phone_numbers.py +++ b/parsons/solidarity_tech/st_chapter_phone_numbers.py @@ -1,10 +1,13 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -41,7 +44,7 @@ def get_chapter_phone_numbers( ``__ """ - params: dict[str, Any] = {"chapter_id": chapter_id} + params: ParamsType = {"chapter_id": chapter_id} res = self._get_resources( "chapter_phone_numbers", diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index 0d8d8ae3b3..cee09c4ad0 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -1,11 +1,14 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_enums import FieldType, ScopeType +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) UserPropertyData = dict[str, int | str | list[dict[str, str | dict]]] @@ -47,11 +50,9 @@ def get_custom_user_properties( ``__ """ - params: dict[str, Any] = {} - if scope_id is not None: - params["scope_id"] = scope_id - if scope_type is not None: - params["scope_type"] = scope_type.value + params: ParamsType = {} + self._add_if_field_not_empty(params, "scope_id", scope_id) + self._add_if_field_not_empty(params, "description", scope_type) res = self._get_resources( "custom_user_properties", diff --git a/parsons/solidarity_tech/st_emails.py b/parsons/solidarity_tech/st_emails.py index 291de791c5..9d1262e4cf 100644 --- a/parsons/solidarity_tech/st_emails.py +++ b/parsons/solidarity_tech/st_emails.py @@ -1,8 +1,11 @@ import logging -from typing import Any +from typing import TYPE_CHECKING from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -55,7 +58,7 @@ def send_one_off_email( ``__ """ - params: dict[str, Any] = { + params: ParamsType = { "user_id": user_id, "subject": subject, "body_html": body_html, diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index 136f1d2132..f55fc23699 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -1,12 +1,15 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -46,7 +49,7 @@ def get_event_attendances( ``__ """ - params: dict[str, Any] = {} + params: ParamsType = {} self._add_if_field_not_empty(params, "event_id", event_id) self._add_if_field_not_empty(params, "session_id", session_id) diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index dd30c5ecee..f765f44bad 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -1,6 +1,6 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np @@ -8,6 +8,9 @@ from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_enums import AttendanceStatus +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -53,7 +56,7 @@ def get_event_rsvps( ``__ """ - params: dict[str, Any] = {"full_user_payload": full_user_payload} + params: ParamsType = {"full_user_payload": full_user_payload} self._add_if_field_not_empty(params, "event_id", event_id) self._add_if_field_not_empty(params, "session_id", session_id) self._add_if_field_not_empty(params, "user_id", user_id) @@ -96,7 +99,7 @@ def get_event_rsvp( ``__ """ - params: dict[str, Any] = {"full_user_payload": full_user_payload} + params: ParamsType = {"full_user_payload": full_user_payload} res = self._get_single_resource("event_rsvps", id, params=params) diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index 97fb689a7e..7f63e58b54 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -1,6 +1,6 @@ import logging from datetime import datetime -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal import numpy as np @@ -8,6 +8,9 @@ from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_enums import EventType +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -83,7 +86,7 @@ def get_event_sessions( if isinstance(event_tags, list): event_tags = ",".join(str(tag) for tag in event_tags) - params: dict[str, Any] = { + params: ParamsType = { "event_id": event_id, } self._add_if_field_not_empty(params, "upcoming", upcoming) @@ -225,7 +228,7 @@ def get_event_session( ``__ """ - params: dict[str, Any] = {"include_hosts": include_hosts} + params: ParamsType = {"include_hosts": include_hosts} res = self._get_single_resource("event_sessions", id, params=params) expected_responses = { diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index 51a299913c..c2a796de9a 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -1,6 +1,6 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np @@ -8,6 +8,9 @@ from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_enums import EventType, ScopeType +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -56,7 +59,7 @@ def get_events( ``__ """ - params: dict[str, Any] = {} + params: ParamsType = {} self._add_if_field_not_empty(params, "scope_id", scope_id) self._add_if_field_not_empty(params, "scope_type", scope_type) @@ -204,7 +207,7 @@ def get_event( ``__ """ - params: dict[str, Any] = {"include_hosts": include_hosts} + params: ParamsType = {"include_hosts": include_hosts} res = self._get_single_resource("event_sessions", id, params=params) diff --git a/parsons/solidarity_tech/st_pages.py b/parsons/solidarity_tech/st_pages.py index cac4293615..87a7313a69 100644 --- a/parsons/solidarity_tech/st_pages.py +++ b/parsons/solidarity_tech/st_pages.py @@ -1,10 +1,13 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -43,7 +46,7 @@ def get_pages( ``__ """ - params: dict[str, Any] = {"include_action_counts": include_action_counts} + params: ParamsType = {"include_action_counts": include_action_counts} res = self._get_resources( "pages", @@ -89,7 +92,7 @@ def get_page( ``__ """ - params: dict[str, Any] = {"include_action_counts": include_action_counts} + params: ParamsType = {"include_action_counts": include_action_counts} res = self._get_single_resource("pages", id, params=params) diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py index 9d4b738afc..93c958604c 100644 --- a/parsons/solidarity_tech/st_phonebanks.py +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -1,10 +1,13 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -54,7 +57,7 @@ def get_phonebanks( if isinstance(ids, list): ids = ",".join(str(id) for id in ids) - params: dict[str, Any] = {"event_id": event_id, "include_stats": include_stats} + params: ParamsType = {"event_id": event_id, "include_stats": include_stats} self._add_if_field_not_empty(params, "ids", ids) res = self._get_resources( diff --git a/parsons/solidarity_tech/st_scheduled_calls.py b/parsons/solidarity_tech/st_scheduled_calls.py index b6d6059155..8e051bf5a1 100644 --- a/parsons/solidarity_tech/st_scheduled_calls.py +++ b/parsons/solidarity_tech/st_scheduled_calls.py @@ -1,10 +1,13 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -44,7 +47,7 @@ def get_scheduled_calls( ``__ """ - params: dict[str, Any] = {} + params: ParamsType = {} self._add_if_field_not_empty(params, "user_id", user_id) self._add_if_field_not_empty(params, "agent_user_id", agent_user_id) diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py index 07b1497284..801be4d20e 100644 --- a/parsons/solidarity_tech/st_scheduled_tasks.py +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -1,12 +1,15 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -46,7 +49,7 @@ def get_scheduled_tasks( ``__ """ - params: dict[str, Any] = {} + params: ParamsType = {} self._add_if_field_not_empty(params, "user_id", user_id) self._add_if_field_not_empty(params, "agent_user_id", agent_user_id) diff --git a/parsons/solidarity_tech/st_task_agents.py b/parsons/solidarity_tech/st_task_agents.py index 52c5985f44..d48d5a732d 100644 --- a/parsons/solidarity_tech/st_task_agents.py +++ b/parsons/solidarity_tech/st_task_agents.py @@ -1,12 +1,15 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -43,7 +46,7 @@ def get_task_agents( ``__ """ - params: dict[str, Any] = {} + params: ParamsType = {} self._add_if_field_not_empty(params, "task_id", task_id) res = self._get_resources( diff --git a/parsons/solidarity_tech/st_task_assignments.py b/parsons/solidarity_tech/st_task_assignments.py index ffe304d947..e93afaa8d3 100644 --- a/parsons/solidarity_tech/st_task_assignments.py +++ b/parsons/solidarity_tech/st_task_assignments.py @@ -1,12 +1,15 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -46,7 +49,7 @@ def get_task_assignments( ``__ """ - params: dict[str, Any] = {"task_id": task_id, "agent_user_id": agent_user_id} + params: ParamsType = {"task_id": task_id, "agent_user_id": agent_user_id} res = self._get_resources( "task_assignments", diff --git a/parsons/solidarity_tech/st_text_templates.py b/parsons/solidarity_tech/st_text_templates.py index e940b06284..de1a7b452c 100644 --- a/parsons/solidarity_tech/st_text_templates.py +++ b/parsons/solidarity_tech/st_text_templates.py @@ -1,6 +1,6 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np @@ -8,6 +8,9 @@ from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_enums import ScopeType +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -44,7 +47,7 @@ def get_text_templates( ``__ """ - params: dict[str, Any] = {"event_id": event_id} + params: ParamsType = {"event_id": event_id} res = self._get_resources( "text_templates", diff --git a/parsons/solidarity_tech/st_textbanks.py b/parsons/solidarity_tech/st_textbanks.py index e3385e0fab..2b3b82cbd4 100644 --- a/parsons/solidarity_tech/st_textbanks.py +++ b/parsons/solidarity_tech/st_textbanks.py @@ -1,10 +1,13 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -54,7 +57,7 @@ def get_textbanks( if isinstance(ids, list): ids = ",".join(str(id) for id in ids) - params: dict[str, Any] = {"event_id": event_id, "include_stats": include_stats} + params: ParamsType = {"event_id": event_id, "include_stats": include_stats} self._add_if_field_not_empty(params, "ids", ids) res = self._get_resources( diff --git a/parsons/solidarity_tech/st_texts.py b/parsons/solidarity_tech/st_texts.py index 0305ec3a9d..e3c6ba4231 100644 --- a/parsons/solidarity_tech/st_texts.py +++ b/parsons/solidarity_tech/st_texts.py @@ -1,10 +1,13 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -41,7 +44,7 @@ def get_texts( ``__ """ - params: dict[str, Any] = {} + params: ParamsType = {} self._add_if_field_not_empty(params, "user_id", user_id) res = self._get_resources( @@ -93,7 +96,7 @@ def send_text( ``__ """ - params: dict[str, Any] = { + params: ParamsType = { "user_id": user_id, "body": body, } diff --git a/parsons/solidarity_tech/st_user_actions.py b/parsons/solidarity_tech/st_user_actions.py index 872f4801a1..314de4cc5d 100644 --- a/parsons/solidarity_tech/st_user_actions.py +++ b/parsons/solidarity_tech/st_user_actions.py @@ -1,12 +1,15 @@ import logging from datetime import datetime -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal import numpy as np from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -64,7 +67,7 @@ def get_user_actions( ``__ """ - params: dict[str, Any] = {} + params: ParamsType = {} self._add_if_field_not_empty(params, "user_id", user_id) self._add_if_field_not_empty(params, "page_id", page_id) self._add_if_field_not_empty(params, "group_by", group_by) diff --git a/parsons/solidarity_tech/st_user_notes.py b/parsons/solidarity_tech/st_user_notes.py index cf09b1dc00..6c56504b4f 100644 --- a/parsons/solidarity_tech/st_user_notes.py +++ b/parsons/solidarity_tech/st_user_notes.py @@ -1,9 +1,12 @@ import logging -from typing import Any +from typing import TYPE_CHECKING from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase from parsons.solidarity_tech.solidarity_tech_enums import InteractionType +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) @@ -48,7 +51,7 @@ def create_user_note( ``__ """ - params: dict[str, Any] = { + params: ParamsType = { "user_id": user_id, "content": content, "restricted": restricted, @@ -96,7 +99,7 @@ def delete_user_note( ``__ """ - params: dict[str, Any] = {"user_id": user_id} + params: ParamsType = {"user_id": user_id} self._add_if_field_not_empty(params, "agent_id", agent_id) res = self._del_request("user_notes", id, params=params) diff --git a/parsons/solidarity_tech/st_user_relationships.py b/parsons/solidarity_tech/st_user_relationships.py index 1dbb76303d..5bee6ca465 100644 --- a/parsons/solidarity_tech/st_user_relationships.py +++ b/parsons/solidarity_tech/st_user_relationships.py @@ -1,10 +1,13 @@ import logging import numbers -from typing import Any, Literal +from typing import TYPE_CHECKING, Literal from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) CompareValueType = str | numbers.Rational | bool @@ -37,7 +40,7 @@ def get_user_relationships( ``__ """ - params: dict[str, Any] = {"user_id": user_id} + params: ParamsType = {"user_id": user_id} res = self._get_resources( "user_relationships", @@ -82,7 +85,7 @@ def create_user_relationship( ``__ """ - params: dict[str, Any] = { + params: ParamsType = { "user_id": user_id, "related_user_id": related_user_id, "relationship_type": relationship_type, @@ -122,7 +125,7 @@ def delete_user_relationship( ``__ """ - params: dict[str, Any] = {"user_id": user_id} + params: ParamsType = {"user_id": user_id} res = self._del_request( "user_relationships", diff --git a/parsons/solidarity_tech/st_users.py b/parsons/solidarity_tech/st_users.py index 91e2ea18d1..5988aa3657 100644 --- a/parsons/solidarity_tech/st_users.py +++ b/parsons/solidarity_tech/st_users.py @@ -1,12 +1,15 @@ import logging import numbers from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any from zoneinfo import ZoneInfo from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + logger = logging.getLogger(__name__) CompareValueType = str | numbers.Rational | bool @@ -62,7 +65,7 @@ def get_users( if isinstance(user_list_ids, list): user_list_ids = ",".join(str(id) for id in user_list_ids) - params: dict[str, Any] = {} + params: ParamsType = {} self._add_if_field_not_empty(params, "user_list_ids", user_list_ids) self._add_if_field_not_empty(params, "phone_number", phone_number) self._add_if_field_not_empty(params, "email", email) diff --git a/parsons/utilities/_api_connector_types.py b/parsons/utilities/_api_connector_types.py new file mode 100644 index 0000000000..0d88fe1e65 --- /dev/null +++ b/parsons/utilities/_api_connector_types.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import logging +from collections.abc import Callable, Iterable, Mapping, Sequence +from typing import TYPE_CHECKING, Any, Protocol, TypeVar, runtime_checkable + +from requests.auth import AuthBase +from requests.models import PreparedRequest +from typing_extensions import ( + Buffer, # TODO: move to collections.abc when Python >= 3.12 +) + +if TYPE_CHECKING: + from typing import TypeAlias + +logger = logging.getLogger(__name__) + + +_T_co = TypeVar("_T_co", covariant=True) +_KT_co = TypeVar("_KT_co", covariant=True) +_VT_co = TypeVar("_VT_co", covariant=True) + + +@runtime_checkable +class SupportsItems(Protocol[_KT_co, _VT_co]): + def items(self) -> Iterable[tuple[_KT_co, _VT_co]]: ... + + +@runtime_checkable +class SupportsRead(Protocol[_T_co]): + def read(self, length: int = ..., /) -> _T_co: ... + + +_AuthType: TypeAlias = ( + tuple[str, str] | AuthBase | Callable[[PreparedRequest], PreparedRequest] | None +) +_HeadersType: TypeAlias = Mapping[str, str | bytes] | None +_JsonType: TypeAlias = ( + None | bool | int | float | str | Sequence["_JsonType"] | Mapping[str, "_JsonType"] +) +_ParamsMappingKeyType: TypeAlias = str | bytes | int | float +_ParamsMappingValueType: TypeAlias = ( + str | bytes | int | float | Iterable[str | bytes | int | float] | None +) +_ParamsType: TypeAlias = ( + SupportsItems[_ParamsMappingKeyType, _ParamsMappingValueType] + | tuple[tuple[_ParamsMappingKeyType, _ParamsMappingValueType], ...] + | Iterable[tuple[_ParamsMappingKeyType, _ParamsMappingValueType]] + | str + | bytes + | None +) +_KVDataType: TypeAlias = Iterable[tuple[Any, Any]] | SupportsItems[Any, Any] +_DataType: TypeAlias = ( + _KVDataType | Iterable[bytes | str] | str | bytes | Buffer | SupportsRead[str | bytes] | None +) diff --git a/parsons/utilities/api_connector.py b/parsons/utilities/api_connector.py index e547f93f44..0cf181f6c1 100644 --- a/parsons/utilities/api_connector.py +++ b/parsons/utilities/api_connector.py @@ -1,38 +1,38 @@ +from __future__ import annotations + import logging import urllib.parse -from collections.abc import Callable, Iterable, Mapping -from typing import Any, Literal, overload +from typing import TYPE_CHECKING, Any, overload import requests -from requests.auth import AuthBase +import requests_ratelimiter from requests.exceptions import HTTPError -from requests.models import PreparedRequest from simplejson.errors import JSONDecodeError from parsons import Table -logger = logging.getLogger(__name__) - -_Auth = tuple[str, str] | AuthBase | Callable[[PreparedRequest], PreparedRequest] -_Headers = Mapping[str, str | bytes | None] -_Data = ( - Iterable[bytes] - | str - | bytes - | list[tuple[Any, Any]] - | tuple[tuple[Any, Any], ...] - | Mapping[Any, Any] -) -_ParamsMappingKeyType = str | bytes | int | float -_ParamsMappingValueType = str | bytes | int | float | Iterable[str | bytes | int | float] | None -_Params = ( - Mapping[_ParamsMappingKeyType, _ParamsMappingValueType] - | tuple[_ParamsMappingKeyType, _ParamsMappingValueType] - | Iterable[tuple[_ParamsMappingKeyType, _ParamsMappingValueType]] - | str - | bytes +from ._api_connector_types import ( + _AuthType, + _DataType, + _HeadersType, + _JsonType, + _ParamsType, ) +# There are here for backwards compatibility +_Auth = _AuthType +_Headers = _HeadersType +_Data = _DataType +_Params = _ParamsType + + +if TYPE_CHECKING: + from typing import Literal + + import pyrate_limiter + +logger = logging.getLogger(__name__) + class APIConnector: """ @@ -48,10 +48,13 @@ class APIConnector: def __init__( self, uri: str, - headers: _Headers | None = None, - auth: _Auth | None = None, + headers: _HeadersType | None = None, + auth: _AuthType | None = None, pagination_key: str | None = None, data_key: str | None = None, + *, + ratelimiter: pyrate_limiter.Limiter | None = None, + session: requests.Session | None = None, ) -> None: """ Initialize the APIConnector. @@ -71,28 +74,75 @@ def __init__( The name of the key in the response json where the data is contained. Required if the data is nested in the response json. + ratelimiter: + A :class:`~pyrate_limiter.limiter.Limiter` instance to use. + If not provided, no rate limiting will be applied. + session: + A preconfigured :class`~requests.Session` for advanced users. + If using `session`, `ratelimiter` must be None. + + Raises: + ValueError: + If both `session` and `ratelimiter` are provided. """ - # Add a trailing slash if its missing + # Add a trailing slash if it's missing if not uri.endswith("/"): uri = uri + "/" self.uri = uri - self.headers = headers - self.auth = auth self.pagination_key = pagination_key self.data_key = data_key + if session and ratelimiter: + raise ValueError("session and ratelimiter cannot both be provided") + + if session: + self.session = session + elif ratelimiter: + self.session = requests_ratelimiter.LimiterSession(limiter=ratelimiter) + else: + self.session = requests.Session() + + if auth: + self.session.auth = auth + + if headers: + self.session.headers = headers # ignore: type[invalid-assignment] + + @property + def auth(self) -> _AuthType: + return self.session.auth + + @auth.setter + def auth(self, inp: _AuthType) -> None: + self.session.auth = inp + + @auth.deleter + def auth(self) -> None: + del self.session.auth + + @property + def headers(self) -> _HeadersType: + return self.session.headers + + @headers.setter + def headers(self, inp: _HeadersType) -> None: + self.session.headers = inp # ignore: type[invalid-assignment] + + @headers.deleter + def headers(self) -> None: + del self.session.headers + def request( self, url: str, req_type: Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], *, json: Any | None = None, - data: _Data | None = None, - params: _Params | None = None, + data: _DataType | None = None, + params: _ParamsType | None = None, raise_on_error: bool = True, - additional_headers: _Headers | None = None, **kwargs, ) -> requests.Response: """ @@ -103,7 +153,8 @@ def request( The url request string. If ``url`` is a relative URL, it will be joined with the ``uri`` of the ``APIConnector`. - If ``url`` is an absolute URL, it will be used as is. + If ``url`` is an absolute URL, + it will be used as is. req_type: The request type. json: The payload of the request object. @@ -116,35 +167,26 @@ def request( E.g. ``http://myapi.com/things?id=1`` raise_on_error: If the request yields an error status code (anything above 400), - raise an :class:`HTTPError`. In most cases, this should be ``True``, + raise an error. In most cases, this should be ``True``, however in some cases, if you are looping through data, you might want to ignore individual failures. - additional_headers: - Additional headers to include in this specific request. - If a header key exists in both ``self.headers`` and - ``additional_headers``, the value from ``additional_headers`` - takes precedence. This does not mutate ``self.headers``. `**kwargs`: - Additional keyword arguments to pass to :func:`requests.request`. + Additional keyword arguments to add to the :class:`~requests.Request`. """ full_url = urllib.parse.urljoin(self.uri, url) - complete_headers: _Headers = {} - if self.headers: - complete_headers.update(self.headers) - if additional_headers: - complete_headers.update(additional_headers) - - resp = requests.request( - req_type, - full_url, - headers=complete_headers, # type: ignore[arg-type] - auth=self.auth, + + req = requests.Request( + method=req_type, + url=full_url, json=json, data=data, - params=params, # type: ignore[arg-type] **kwargs, ) + if params: + req.params = params + + resp = self.session.send(req.prepare()) if raise_on_error: self.validate_response(resp) @@ -160,7 +202,7 @@ def get_request( return_format: Literal["json"] = "json", raise_on_error: ... = ..., **kwargs, - ) -> dict[str, Any]: ... + ) -> _JsonType: ... @overload def get_request( @@ -177,11 +219,11 @@ def get_request( self, url: str, *, - params: _Params | None = None, + params: _ParamsType | None = None, return_format: Literal["json", "content"] = "json", raise_on_error: bool = True, **kwargs, - ) -> dict[str, Any] | bytes: + ) -> _JsonType | bytes: """ Make a GET request. @@ -194,14 +236,14 @@ def get_request( however in some cases, if you are looping through data, you might want to ignore individual failures. `**kwargs`: - Additional keyword arguments to pass to :func:`requests.request`. + Additional keyword arguments to pass to the :class:`~requests.Request`. Returns: The :meth:`requests.Response.json` from the response if `return_format` is ``json``, or :attr:`requests.Response.content` from the response if `return_format` is ``content``. Raises: - RuntimeError: If ``return_format`` is not ``json`` or ``content``. + RuntimeError: If return_format is not ``json`` or ``content``. """ r = self.request(url, "GET", params=params, raise_on_error=raise_on_error, **kwargs) @@ -219,13 +261,13 @@ def post_request( self, url: str, *, - params: _Params | None = None, - data: _Data | None = None, + params: _ParamsType | None = None, + data: _DataType | None = None, json: Any | None = None, success_codes: list[int] | None = None, raise_on_error: bool = True, **kwargs, - ) -> dict[str, Any] | int | None: + ) -> _JsonType: """ Make a POST request. @@ -243,7 +285,7 @@ def post_request( however in some cases, if you are looping through data, you might want to ignore individual failures. `**kwargs`: - Additional keyword arguments to pass to :func:`requests.request`. + Additional keyword arguments to pass to :class:`~requests.Request`. Returns: If successful, json date from :meth:`requests.Response.json` @@ -277,11 +319,11 @@ def delete_request( self, url: str, *, - params: _Params | None = None, + params: _ParamsType | None = None, success_codes: list[int] | None = None, raise_on_error: bool = True, **kwargs, - ) -> dict[str, Any] | int | None: + ) -> _JsonType: """ Make a DELETE request. @@ -297,7 +339,7 @@ def delete_request( however in some cases, if you are looping through data, you might want to ignore individual failures. `**kwargs`: - Additional keyword arguments to pass to :func:`requests.request`. + Additional keyword arguments to pass to :class:`~requests.Request`. Returns: If successful, json date from :meth:`requests.Response.json` @@ -323,13 +365,13 @@ def put_request( self, url: str, *, - data: _Data | None = None, + data: _DataType | None = None, json: Any | None = None, - params: _Params | None = None, + params: _ParamsType | None = None, success_codes: list[int] | None = None, raise_on_error: bool = True, **kwargs, - ) -> dict[str, Any] | int | None: + ) -> _JsonType: """ Make a PUT request. @@ -347,7 +389,7 @@ def put_request( however in some cases, if you are looping through data, you might want to ignore individual failures. `**kwargs`: - Additional keyword arguments to pass to :func:`requests.request`. + Additional keyword arguments to pass to :class:`~requests.Request`. Returns: If successful, json date from :meth:`requests.Response.json` @@ -375,13 +417,13 @@ def patch_request( self, url: str, *, - params: _Params | None = None, - data: _Data | None = None, + params: _ParamsType | None = None, + data: _DataType | None = None, json: Any | None = None, success_codes: list[int] | None = None, raise_on_error: bool = True, **kwargs, - ) -> dict[str, Any] | int | None: + ) -> _JsonType: """ Make a PATCH request. @@ -399,7 +441,7 @@ def patch_request( however in some cases, if you are looping through data, you might want to ignore individual failures. `**kwargs`: - Additional keyword arguments to pass to :func:`requests.request`. + Additional keyword arguments to pass to :class:`~requests.Request`. Returns: If successful, json date from :meth:`requests.Response.json` diff --git a/parsons/utilities/ratelimited_api_connector.py b/parsons/utilities/ratelimited_api_connector.py deleted file mode 100644 index 63ffcded1f..0000000000 --- a/parsons/utilities/ratelimited_api_connector.py +++ /dev/null @@ -1,38 +0,0 @@ -import requests -from pyrate_limiter import Limiter, Rate - -from parsons.utilities.api_connector import APIConnector - - -class RateLimitedAPIConnector(APIConnector): - """A wrapper around :class:`APIConnector` that adds rate limiting.""" - - def __init__(self, *args, ratelimit: Rate, **kwargs) -> None: - """ - Initialize the RateLimitedAPIConnector. - - Accepts and passes through all the args and kwargs of :class:`APIConnector`. - - Args: - ratelimit: - The rate limit to apply to API calls, - as a pyrate-limiter :class:`pyrate_limiter.abstracts.Rate` object. - - """ - self.limiter = Limiter(ratelimit) - super().__init__(*args, **kwargs) - - def request( - self, - *args, - **kwargs, - ) -> requests.Response: - """ - Make a rate limited request. - - If the rate limit has been exceeded, the request will be held for the next available opportunity. - All args will be passed through to :class:`APIConnector`. - - """ - self.limiter.try_acquire("api_call") - return super().request(*args, **kwargs) diff --git a/pyproject.toml b/pyproject.toml index acf2a39856..ac05ab90fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "pyrate-limiter >= 4.0", "python-dateutil >= 2.0", "requests >= 2.0", + "requests-ratelimiter >= 0.10", "requests-oauthlib >= 1.0", "simplejson >= 3.18", ] From 3c7f83aee0175ffb4cb63e257383d5bdf97caf16 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 5 Aug 2026 22:16:34 -0400 Subject: [PATCH 74/94] Import from future annotations and utilize TYPE_CHECKING block more --- parsons/solidarity_tech/solidarity_tech.py | 2 ++ parsons/solidarity_tech/solidarity_tech_base.py | 11 ++++++++--- parsons/solidarity_tech/solidarity_tech_enums.py | 2 ++ parsons/solidarity_tech/st_activities.py | 5 ++++- parsons/solidarity_tech/st_agent_assignments.py | 9 ++++++--- .../solidarity_tech/st_automation_enrollments.py | 2 ++ parsons/solidarity_tech/st_calls.py | 5 ++++- .../solidarity_tech/st_chapter_phone_numbers.py | 5 ++++- parsons/solidarity_tech/st_chapters.py | 7 ++++++- .../solidarity_tech/st_custom_user_properties.py | 7 +++++-- parsons/solidarity_tech/st_donation_charges.py | 8 ++++++-- parsons/solidarity_tech/st_email_blasts.py | 7 ++++++- parsons/solidarity_tech/st_email_senders.py | 2 ++ parsons/solidarity_tech/st_emails.py | 2 ++ parsons/solidarity_tech/st_event_attendances.py | 9 ++++++--- parsons/solidarity_tech/st_event_rsvps.py | 11 +++++++---- parsons/solidarity_tech/st_event_sessions.py | 11 +++++++---- parsons/solidarity_tech/st_events.py | 11 +++++++---- parsons/solidarity_tech/st_field_survey_urls.py | 2 ++ parsons/solidarity_tech/st_organizations.py | 7 ++++++- parsons/solidarity_tech/st_pages.py | 5 ++++- parsons/solidarity_tech/st_phonebanks.py | 5 ++++- parsons/solidarity_tech/st_scheduled_calls.py | 5 ++++- parsons/solidarity_tech/st_scheduled_tasks.py | 6 ++++-- parsons/solidarity_tech/st_task_agents.py | 9 ++++++--- parsons/solidarity_tech/st_task_assignments.py | 9 ++++++--- parsons/solidarity_tech/st_team_members.py | 11 ++++++++--- parsons/solidarity_tech/st_text_blasts.py | 7 ++++++- parsons/solidarity_tech/st_text_templates.py | 11 +++++++---- parsons/solidarity_tech/st_textbanks.py | 5 ++++- parsons/solidarity_tech/st_texts.py | 5 ++++- parsons/solidarity_tech/st_user_actions.py | 9 ++++++--- parsons/solidarity_tech/st_user_lists.py | 15 ++++++++++----- parsons/solidarity_tech/st_user_notes.py | 4 +++- parsons/solidarity_tech/st_user_relationships.py | 2 ++ parsons/solidarity_tech/st_users.py | 5 ++++- 36 files changed, 176 insertions(+), 62 deletions(-) diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index b2fbf07370..2d79915dcb 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from parsons.solidarity_tech.st_activities import SolidarityTechActivities diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index e60d804e9e..3f8b730f34 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -1,12 +1,12 @@ +from __future__ import annotations + import logging -from collections.abc import Mapping from datetime import datetime from enum import Enum -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import numpy as np import pyrate_limiter -import requests import requests_ratelimiter from requests.structures import CaseInsensitiveDict @@ -14,6 +14,11 @@ from parsons.utilities import check_env from parsons.utilities.api_connector import APIConnector, _ParamsType +if TYPE_CHECKING: + from collections.abc import Mapping + + import requests + logger = logging.getLogger(__name__) ParamsType = _ParamsType | np.int64 diff --git a/parsons/solidarity_tech/solidarity_tech_enums.py b/parsons/solidarity_tech/solidarity_tech_enums.py index 1e318944d4..511e03ee67 100644 --- a/parsons/solidarity_tech/solidarity_tech_enums.py +++ b/parsons/solidarity_tech/solidarity_tech_enums.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from enum import Enum diff --git a/parsons/solidarity_tech/st_activities.py b/parsons/solidarity_tech/st_activities.py index 809b3283c4..8a9e43bfaa 100644 --- a/parsons/solidarity_tech/st_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index 9ce3d2577e..4f09951889 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -1,13 +1,16 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING, Any -import numpy as np - from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + + import numpy as np + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_automation_enrollments.py b/parsons/solidarity_tech/st_automation_enrollments.py index a9123d15c6..81f501190c 100644 --- a/parsons/solidarity_tech/st_automation_enrollments.py +++ b/parsons/solidarity_tech/st_automation_enrollments.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from typing import Any diff --git a/parsons/solidarity_tech/st_calls.py b/parsons/solidarity_tech/st_calls.py index 7b607fc975..b6ba0995f3 100644 --- a/parsons/solidarity_tech/st_calls.py +++ b/parsons/solidarity_tech/st_calls.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_chapter_phone_numbers.py b/parsons/solidarity_tech/st_chapter_phone_numbers.py index 4e4dd13b6a..ddf1213f71 100644 --- a/parsons/solidarity_tech/st_chapter_phone_numbers.py +++ b/parsons/solidarity_tech/st_chapter_phone_numbers.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_chapters.py b/parsons/solidarity_tech/st_chapters.py index 686c323f4a..fb5bee7899 100644 --- a/parsons/solidarity_tech/st_chapters.py +++ b/parsons/solidarity_tech/st_chapters.py @@ -1,9 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from datetime import datetime + logger = logging.getLogger(__name__) ChapterData = dict[str, int | str] diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index cee09c4ad0..5be2cbb6e5 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -1,13 +1,16 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING, Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_enums import FieldType, ScopeType if TYPE_CHECKING: + from datetime import datetime + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.solidarity_tech_enums import FieldType, ScopeType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_donation_charges.py b/parsons/solidarity_tech/st_donation_charges.py index ca1fac2484..04aa970cad 100644 --- a/parsons/solidarity_tech/st_donation_charges.py +++ b/parsons/solidarity_tech/st_donation_charges.py @@ -1,10 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from datetime import datetime + logger = logging.getLogger(__name__) DonationChargeData = dict[str, int | str | bool | dict | dict[str, Any]] diff --git a/parsons/solidarity_tech/st_email_blasts.py b/parsons/solidarity_tech/st_email_blasts.py index 193cf8c52f..57e5f730b3 100644 --- a/parsons/solidarity_tech/st_email_blasts.py +++ b/parsons/solidarity_tech/st_email_blasts.py @@ -1,9 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from datetime import datetime + logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_email_senders.py b/parsons/solidarity_tech/st_email_senders.py index f4eab2d047..9b4f9c57fd 100644 --- a/parsons/solidarity_tech/st_email_senders.py +++ b/parsons/solidarity_tech/st_email_senders.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from parsons import Table diff --git a/parsons/solidarity_tech/st_emails.py b/parsons/solidarity_tech/st_emails.py index 9d1262e4cf..9332497970 100644 --- a/parsons/solidarity_tech/st_emails.py +++ b/parsons/solidarity_tech/st_emails.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from typing import TYPE_CHECKING diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index f55fc23699..2aa9e4d8ac 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -1,13 +1,16 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING, Any -import numpy as np - from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + + import numpy as np + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index f765f44bad..17393eb657 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -1,15 +1,18 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING, Any -import numpy as np - from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_enums import AttendanceStatus if TYPE_CHECKING: + from datetime import datetime + + import numpy as np + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.solidarity_tech_enums import AttendanceStatus logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index 7f63e58b54..600b01ff3e 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -1,15 +1,18 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING, Any, Literal -import numpy as np - from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_enums import EventType if TYPE_CHECKING: + from datetime import datetime + + import numpy as np + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.solidarity_tech_enums import EventType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index c2a796de9a..34cebdf3a7 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -1,15 +1,18 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING, Any -import numpy as np - from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_enums import EventType, ScopeType if TYPE_CHECKING: + from datetime import datetime + + import numpy as np + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.solidarity_tech_enums import EventType, ScopeType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_field_survey_urls.py b/parsons/solidarity_tech/st_field_survey_urls.py index 2c1d26c9fa..9ea4c5f80f 100644 --- a/parsons/solidarity_tech/st_field_survey_urls.py +++ b/parsons/solidarity_tech/st_field_survey_urls.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from typing import Any, Literal diff --git a/parsons/solidarity_tech/st_organizations.py b/parsons/solidarity_tech/st_organizations.py index bc838d1ab3..3fb45e713a 100644 --- a/parsons/solidarity_tech/st_organizations.py +++ b/parsons/solidarity_tech/st_organizations.py @@ -1,9 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from datetime import datetime + logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_pages.py b/parsons/solidarity_tech/st_pages.py index 87a7313a69..bf0ab02070 100644 --- a/parsons/solidarity_tech/st_pages.py +++ b/parsons/solidarity_tech/st_pages.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py index 93c958604c..18b41afdf5 100644 --- a/parsons/solidarity_tech/st_phonebanks.py +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_scheduled_calls.py b/parsons/solidarity_tech/st_scheduled_calls.py index 8e051bf5a1..4b71827dd0 100644 --- a/parsons/solidarity_tech/st_scheduled_calls.py +++ b/parsons/solidarity_tech/st_scheduled_calls.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py index 801be4d20e..625d659abb 100644 --- a/parsons/solidarity_tech/st_scheduled_tasks.py +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -1,13 +1,15 @@ +from __future__ import annotations + import logging from datetime import datetime from typing import TYPE_CHECKING, Any -import numpy as np - from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + import numpy as np + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_task_agents.py b/parsons/solidarity_tech/st_task_agents.py index d48d5a732d..b86dc1f4f0 100644 --- a/parsons/solidarity_tech/st_task_agents.py +++ b/parsons/solidarity_tech/st_task_agents.py @@ -1,13 +1,16 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING, Any -import numpy as np - from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + + import numpy as np + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_task_assignments.py b/parsons/solidarity_tech/st_task_assignments.py index e93afaa8d3..9b179a40db 100644 --- a/parsons/solidarity_tech/st_task_assignments.py +++ b/parsons/solidarity_tech/st_task_assignments.py @@ -1,13 +1,16 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING, Any -import numpy as np - from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + + import numpy as np + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py index 1e5e2d7ede..0ec888b400 100644 --- a/parsons/solidarity_tech/st_team_members.py +++ b/parsons/solidarity_tech/st_team_members.py @@ -1,10 +1,15 @@ +from __future__ import annotations + import logging -from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_enums import InviteType, ScopeType + +if TYPE_CHECKING: + from datetime import datetime + + from parsons.solidarity_tech.solidarity_tech_enums import InviteType, ScopeType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_text_blasts.py b/parsons/solidarity_tech/st_text_blasts.py index 0a820955d6..0d834997cb 100644 --- a/parsons/solidarity_tech/st_text_blasts.py +++ b/parsons/solidarity_tech/st_text_blasts.py @@ -1,9 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime +from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +if TYPE_CHECKING: + from datetime import datetime + logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_text_templates.py b/parsons/solidarity_tech/st_text_templates.py index de1a7b452c..e3c1f67431 100644 --- a/parsons/solidarity_tech/st_text_templates.py +++ b/parsons/solidarity_tech/st_text_templates.py @@ -1,15 +1,18 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING, Any -import numpy as np - from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_enums import ScopeType if TYPE_CHECKING: + from datetime import datetime + + import numpy as np + from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.solidarity_tech_enums import ScopeType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_textbanks.py b/parsons/solidarity_tech/st_textbanks.py index 2b3b82cbd4..c65ff88000 100644 --- a/parsons/solidarity_tech/st_textbanks.py +++ b/parsons/solidarity_tech/st_textbanks.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_texts.py b/parsons/solidarity_tech/st_texts.py index e3c6ba4231..5c786a1c27 100644 --- a/parsons/solidarity_tech/st_texts.py +++ b/parsons/solidarity_tech/st_texts.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_user_actions.py b/parsons/solidarity_tech/st_user_actions.py index 314de4cc5d..39ef17b57a 100644 --- a/parsons/solidarity_tech/st_user_actions.py +++ b/parsons/solidarity_tech/st_user_actions.py @@ -1,13 +1,16 @@ +from __future__ import annotations + import logging -from datetime import datetime from typing import TYPE_CHECKING, Any, Literal -import numpy as np - from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + + import numpy as np + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_user_lists.py b/parsons/solidarity_tech/st_user_lists.py index 9547338370..2927a5b19b 100644 --- a/parsons/solidarity_tech/st_user_lists.py +++ b/parsons/solidarity_tech/st_user_lists.py @@ -1,13 +1,18 @@ +from __future__ import annotations + import logging import numbers -from datetime import datetime -from typing import Any - -import numpy as np +from typing import TYPE_CHECKING, Any from parsons import Table from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_enums import ScopeType + +if TYPE_CHECKING: + from datetime import datetime + + import numpy as np + + from parsons.solidarity_tech.solidarity_tech_enums import ScopeType CompareValueType = str | numbers.Rational | bool QueryParamType = dict[ diff --git a/parsons/solidarity_tech/st_user_notes.py b/parsons/solidarity_tech/st_user_notes.py index 6c56504b4f..6b3b25d2e4 100644 --- a/parsons/solidarity_tech/st_user_notes.py +++ b/parsons/solidarity_tech/st_user_notes.py @@ -1,11 +1,13 @@ +from __future__ import annotations + import logging from typing import TYPE_CHECKING from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase -from parsons.solidarity_tech.solidarity_tech_enums import InteractionType if TYPE_CHECKING: from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.solidarity_tech_enums import InteractionType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_user_relationships.py b/parsons/solidarity_tech/st_user_relationships.py index 5bee6ca465..8fc93bec5e 100644 --- a/parsons/solidarity_tech/st_user_relationships.py +++ b/parsons/solidarity_tech/st_user_relationships.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import numbers from typing import TYPE_CHECKING, Literal diff --git a/parsons/solidarity_tech/st_users.py b/parsons/solidarity_tech/st_users.py index 5988aa3657..2a851eeb14 100644 --- a/parsons/solidarity_tech/st_users.py +++ b/parsons/solidarity_tech/st_users.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import logging import numbers -from datetime import datetime from typing import TYPE_CHECKING, Any from zoneinfo import ZoneInfo @@ -8,6 +9,8 @@ from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase if TYPE_CHECKING: + from datetime import datetime + from parsons.solidarity_tech.solidarity_tech_base import ParamsType logger = logging.getLogger(__name__) From 567c529b093e367bac70256cb823c07b35fe2db2 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 5 Aug 2026 22:17:21 -0400 Subject: [PATCH 75/94] Remove APIConnector tests (out of scope here) --- test/test_utilities/test_api_connector.py | 55 -------------- .../test_ratelimited_api_connector.py | 72 ------------------- 2 files changed, 127 deletions(-) delete mode 100644 test/test_utilities/test_api_connector.py delete mode 100644 test/test_utilities/test_ratelimited_api_connector.py diff --git a/test/test_utilities/test_api_connector.py b/test/test_utilities/test_api_connector.py deleted file mode 100644 index 161b040429..0000000000 --- a/test/test_utilities/test_api_connector.py +++ /dev/null @@ -1,55 +0,0 @@ -import pytest -from requests_mock import Mocker - -from parsons.utilities.api_connector import APIConnector - - -@pytest.fixture -def connector() -> APIConnector: - return APIConnector( - uri="https://api.example.com/v1", headers={"content-type": "application/json"} - ) - - -def test_init_adds_headers(connector: APIConnector, requests_mock: Mocker) -> None: - requests_mock.get( - "https://api.example.com/v1/data", json={"status": "authorized"}, status_code=200 - ) - - connector.request("data", "GET") - - req = requests_mock.last_request - assert req.headers["content-type"] == "application/json" - - -def test_request_with_additional_headers(connector: APIConnector, requests_mock) -> None: - requests_mock.get( - "https://api.example.com/v1/data", json={"status": "authorized"}, status_code=200 - ) - - connector.request( - "data", - "GET", - additional_headers={"Authorization": "Bearer token123", "X-Custom-Header": "value"}, - ) - - req = requests_mock.last_request - assert req.headers["Authorization"] == "Bearer token123" - assert req.headers["X-Custom-Header"] == "value" - - -def test_request_merges_base_and_additional_headers( - connector: APIConnector, requests_mock: Mocker -) -> None: - requests_mock.get("https://api.example.com/v1/data", json={}, status_code=200) - - connector.request( - "data", - "GET", - additional_headers={"Authorization": "Bearer token123", "X-Custom-Header": "value"}, - ) - - req = requests_mock.last_request - assert req.headers["content-type"] == "application/json" - assert req.headers["Authorization"] == "Bearer token123" - assert req.headers["X-Custom-Header"] == "value" diff --git a/test/test_utilities/test_ratelimited_api_connector.py b/test/test_utilities/test_ratelimited_api_connector.py deleted file mode 100644 index 1425560fe2..0000000000 --- a/test/test_utilities/test_ratelimited_api_connector.py +++ /dev/null @@ -1,72 +0,0 @@ -import pytest -from pyrate_limiter import Duration, Rate -from requests_mock import DELETE, GET, PATCH, POST, PUT, Mocker - -from parsons.utilities.ratelimited_api_connector import RateLimitedAPIConnector - -EXAMPLE_URL = "https://api.example.com" -EXAMPLE_ENDPOINT = f"{EXAMPLE_URL}/test-endpoint" - - -@pytest.fixture -def connector() -> RateLimitedAPIConnector: - rate = Rate(1, Duration.MINUTE) - return RateLimitedAPIConnector(EXAMPLE_URL, ratelimit=rate) - - -@pytest.fixture -def limiter_spy(connector: RateLimitedAPIConnector) -> list[tuple[str, bool]]: - """Spies on connector.limiter.try_acquire and records call results.""" - calls = [] - original_try_acquire = connector.limiter.try_acquire - - def spy_try_acquire(name: str = "pyrate", *, blocking: bool = False): - is_blocking = not original_try_acquire(name, blocking=blocking) - calls.append((name, is_blocking)) - return is_blocking - - connector.limiter.try_acquire = spy_try_acquire # type: ignore - return calls - - -@pytest.mark.parametrize( - ("request_type", "method_name"), - [ - (GET, "get_request"), - (POST, "post_request"), - (PUT, "put_request"), - (PATCH, "patch_request"), - (DELETE, "delete_request"), - ], -) -def test_methods_triggers_limiter( - connector: RateLimitedAPIConnector, - requests_mock: Mocker, - limiter_spy: list[tuple[str, bool]], - request_type: str, - method_name: str, -): - requests_mock.register_uri( - method=request_type, - url=EXAMPLE_ENDPOINT, - json={"status": "ok"}, - status_code=200, - ) - - method = getattr(connector, method_name) - method(EXAMPLE_ENDPOINT) - - assert limiter_spy == [("api_call", False)] - - -def test_rate_limiter_blocks_exceeding_calls( - connector: RateLimitedAPIConnector, - requests_mock: Mocker, - limiter_spy: list[tuple[str, bool]], -): - requests_mock.get(url=EXAMPLE_ENDPOINT, json={"data": "ok"}) - - connector.request(EXAMPLE_ENDPOINT, req_type=GET) - connector.request(EXAMPLE_ENDPOINT, req_type=GET) - - assert limiter_spy == [("api_call", False), ("api_call", True)] From f6f708cb26627696322d559f8d3ab76f3f1197f7 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 5 Aug 2026 22:20:28 -0400 Subject: [PATCH 76/94] Remove obsolete ratelimited API connector documentation --- docs/framework/utilities.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/framework/utilities.rst b/docs/framework/utilities.rst index 0916d3d499..8b99515c22 100644 --- a/docs/framework/utilities.rst +++ b/docs/framework/utilities.rst @@ -132,13 +132,6 @@ OAuth API Connector :inherited-members: :members: -Rate-Limited API Connector -========================== - -.. automodule:: parsons.utilities.ratelimited_api_connector - :inherited-members: - :members: - SQL Helpers =========== From 1475754ae3722ac9b8c582680034e511970cf8a7 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 6 Aug 2026 12:14:06 -0400 Subject: [PATCH 77/94] Improve SolidarityTech connector documentation --- docs/solidarity_tech.rst | 4 +++- parsons/solidarity_tech/solidarity_tech.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/solidarity_tech.rst b/docs/solidarity_tech.rst index 8df14b83ec..b0f01c9cc9 100644 --- a/docs/solidarity_tech.rst +++ b/docs/solidarity_tech.rst @@ -5,7 +5,7 @@ Solidarity Tech Overview ======== -Solidarity Tech's all-in-one nonprofit CRM includes digital tools like +`Solidarity Tech`_'s all-in-one nonprofit CRM includes digital tools like texting, calling, email & websites for advocacy groups, unions & grassroots organizers. As of September 2026, the :class:`~parsons.solidarity_tech.solidarity_tech.SolidarityTech` connector supports @@ -52,3 +52,5 @@ API .. autoclass:: parsons.solidarity_tech.solidarity_tech.SolidarityTech :inherited-members: :members: + +.. _Solidarity Tech: https://domain.invalid/ diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index 2d79915dcb..a0e4838928 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -74,4 +74,23 @@ class SolidarityTech( SolidarityTechUserRelationships, SolidarityTechUsers, ): + """ + Parsons connector for interacting with `Solidarity Tech`_ endpoints. + + The SolidarityTech connector provides a complete + interface for interacting with the Solidarity Tech API. + It inherits from multiple endpoint-specific classes, + all of which ultimately inherit shared methods from + :class:`~parsons.solidarity_tech.solidarity_tech.SolidarityTechBase`. + + If you only need limited functionality rather than the full connector, + you can import an individual component class directly. + + .. code-block:: python + :caption: Importing a component class directly. + + from parsons.solidarity_tech import SolidarityTechEvents + + """ + pass From 9f6ae984bb7ec761f9eeaab3c4fc2758c3c79a60 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 6 Aug 2026 12:15:19 -0400 Subject: [PATCH 78/94] Allow passing a requests.Session through SolidarityTech for advanced configuration. --- parsons/solidarity_tech/solidarity_tech_base.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/solidarity_tech_base.py index 3f8b730f34..6e5063ab6d 100644 --- a/parsons/solidarity_tech/solidarity_tech_base.py +++ b/parsons/solidarity_tech/solidarity_tech_base.py @@ -25,7 +25,9 @@ class SolidarityTechBase: - def __init__(self, api_token: str | None = None) -> None: + def __init__( + self, api_token: str | None = None, *, session: requests.Session | None = None + ) -> None: """ Instantiate the SolidarityTech class. @@ -33,6 +35,10 @@ def __init__(self, api_token: str | None = None) -> None: api_token: A valid Bearer token for authorization. Not required if the `SOLIDARITY_TECH_BEARER_KEY` env variable is set. + session: + A custom :class:`requests.Session` instance for advanced configuration. + Providing your own :class:`~requests.Session` will bypass + built-in rate limiting, so you will need to provide your own solution. """ self.api_token = cast("str", check_env.check("SOLIDARITY_TECH_BEARER_KEY", api_token)) @@ -43,7 +49,10 @@ def __init__(self, api_token: str | None = None) -> None: headers=self.headers, ratelimiter=requests_ratelimiter.Limiter( pyrate_limiter.Rate(60, pyrate_limiter.Duration.SECOND * 30) - ), + ) + if not session + else None, + session=session, ) def _get_resources(self, endpoint: str, **kwargs) -> requests.Response: From cfef61610ea0fecac2518d73b4d4d9ee9fbdcf02 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 6 Aug 2026 12:21:06 -0400 Subject: [PATCH 79/94] Add additional external links in documentation --- docs/solidarity_tech.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/solidarity_tech.rst b/docs/solidarity_tech.rst index b0f01c9cc9..5977590d22 100644 --- a/docs/solidarity_tech.rst +++ b/docs/solidarity_tech.rst @@ -6,10 +6,18 @@ Overview ======== `Solidarity Tech`_'s all-in-one nonprofit CRM includes digital tools like -texting, calling, email & websites for advocacy groups, unions & grassroots organizers. +`texting `__, +`calling `__, +`email `__ & +`websites `__ for +`advocacy groups `__, +`unions `__ & +grassroots organizers. As of September 2026, the :class:`~parsons.solidarity_tech.solidarity_tech.SolidarityTech` connector supports all endpoints described in the `Solidarity Tech API Documentation `_. +The documented `rate limits `__ +are applied automatically, although advanced configuration is available. Quickstart ========== From 410a6c5185280f7e63c3f256b4b56e92803e9a67 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 6 Aug 2026 13:14:03 -0400 Subject: [PATCH 80/94] Add exceptions to documentation --- docs/solidarity_tech.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/solidarity_tech.rst b/docs/solidarity_tech.rst index 5977590d22..5dfead1cee 100644 --- a/docs/solidarity_tech.rst +++ b/docs/solidarity_tech.rst @@ -5,6 +5,9 @@ Solidarity Tech Overview ======== +What is Solidarity Tech? +------------------------ + `Solidarity Tech`_'s all-in-one nonprofit CRM includes digital tools like `texting `__, `calling `__, @@ -14,7 +17,10 @@ Overview `unions `__ & grassroots organizers. -As of September 2026, the :class:`~parsons.solidarity_tech.solidarity_tech.SolidarityTech` connector supports +The SolidarityTech Connector +---------------------------- + +As of September 2026, parsons' :class:`~parsons.solidarity_tech.solidarity_tech.SolidarityTech` connector supports all endpoints described in the `Solidarity Tech API Documentation `_. The documented `rate limits `__ are applied automatically, although advanced configuration is available. @@ -61,4 +67,8 @@ API :inherited-members: :members: -.. _Solidarity Tech: https://domain.invalid/ +.. automodule:: parsons.solidarity_tech.exceptions + :inherited-members: + :members: + +.. _Solidarity Tech: https://www.solidarity.tech/ From 797a95bec7358e01c289a67c17b0fe989f6a9286 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 6 Aug 2026 13:16:40 -0400 Subject: [PATCH 81/94] Shorter names for enum and base files --- .../solidarity_tech/{solidarity_tech_base.py => base.py} | 0 .../solidarity_tech/{solidarity_tech_enums.py => enums.py} | 0 parsons/solidarity_tech/solidarity_tech.py | 2 +- parsons/solidarity_tech/st_activities.py | 4 ++-- parsons/solidarity_tech/st_agent_assignments.py | 4 ++-- parsons/solidarity_tech/st_automation_enrollments.py | 2 +- parsons/solidarity_tech/st_calls.py | 4 ++-- parsons/solidarity_tech/st_chapter_phone_numbers.py | 4 ++-- parsons/solidarity_tech/st_chapters.py | 2 +- parsons/solidarity_tech/st_custom_user_properties.py | 6 +++--- parsons/solidarity_tech/st_donation_charges.py | 2 +- parsons/solidarity_tech/st_email_blasts.py | 2 +- parsons/solidarity_tech/st_email_senders.py | 2 +- parsons/solidarity_tech/st_emails.py | 4 ++-- parsons/solidarity_tech/st_event_attendances.py | 4 ++-- parsons/solidarity_tech/st_event_rsvps.py | 6 +++--- parsons/solidarity_tech/st_event_sessions.py | 6 +++--- parsons/solidarity_tech/st_events.py | 6 +++--- parsons/solidarity_tech/st_field_survey_urls.py | 2 +- parsons/solidarity_tech/st_organizations.py | 2 +- parsons/solidarity_tech/st_pages.py | 4 ++-- parsons/solidarity_tech/st_phonebanks.py | 4 ++-- parsons/solidarity_tech/st_scheduled_calls.py | 4 ++-- parsons/solidarity_tech/st_scheduled_tasks.py | 4 ++-- parsons/solidarity_tech/st_task_agents.py | 4 ++-- parsons/solidarity_tech/st_task_assignments.py | 4 ++-- parsons/solidarity_tech/st_team_members.py | 4 ++-- parsons/solidarity_tech/st_text_blasts.py | 2 +- parsons/solidarity_tech/st_text_templates.py | 6 +++--- parsons/solidarity_tech/st_textbanks.py | 4 ++-- parsons/solidarity_tech/st_texts.py | 4 ++-- parsons/solidarity_tech/st_user_actions.py | 4 ++-- parsons/solidarity_tech/st_user_lists.py | 4 ++-- parsons/solidarity_tech/st_user_notes.py | 6 +++--- parsons/solidarity_tech/st_user_relationships.py | 4 ++-- parsons/solidarity_tech/st_users.py | 4 ++-- 36 files changed, 65 insertions(+), 65 deletions(-) rename parsons/solidarity_tech/{solidarity_tech_base.py => base.py} (100%) rename parsons/solidarity_tech/{solidarity_tech_enums.py => enums.py} (100%) diff --git a/parsons/solidarity_tech/solidarity_tech_base.py b/parsons/solidarity_tech/base.py similarity index 100% rename from parsons/solidarity_tech/solidarity_tech_base.py rename to parsons/solidarity_tech/base.py diff --git a/parsons/solidarity_tech/solidarity_tech_enums.py b/parsons/solidarity_tech/enums.py similarity index 100% rename from parsons/solidarity_tech/solidarity_tech_enums.py rename to parsons/solidarity_tech/enums.py diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index a0e4838928..d8bbc44736 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -81,7 +81,7 @@ class SolidarityTech( interface for interacting with the Solidarity Tech API. It inherits from multiple endpoint-specific classes, all of which ultimately inherit shared methods from - :class:`~parsons.solidarity_tech.solidarity_tech.SolidarityTechBase`. + :class:`~parsons.solidarity_tech.base.SolidarityTechBase`. If you only need limited functionality rather than the full connector, you can import an individual component class directly. diff --git a/parsons/solidarity_tech/st_activities.py b/parsons/solidarity_tech/st_activities.py index 8a9e43bfaa..aa41137fe7 100644 --- a/parsons/solidarity_tech/st_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index 4f09951889..8213653030 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -4,14 +4,14 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime import numpy as np - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_automation_enrollments.py b/parsons/solidarity_tech/st_automation_enrollments.py index 81f501190c..47ef35e3ad 100644 --- a/parsons/solidarity_tech/st_automation_enrollments.py +++ b/parsons/solidarity_tech/st_automation_enrollments.py @@ -3,7 +3,7 @@ import logging from typing import Any -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_calls.py b/parsons/solidarity_tech/st_calls.py index b6ba0995f3..fd85ab03ff 100644 --- a/parsons/solidarity_tech/st_calls.py +++ b/parsons/solidarity_tech/st_calls.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_chapter_phone_numbers.py b/parsons/solidarity_tech/st_chapter_phone_numbers.py index ddf1213f71..99ad6d54a9 100644 --- a/parsons/solidarity_tech/st_chapter_phone_numbers.py +++ b/parsons/solidarity_tech/st_chapter_phone_numbers.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_chapters.py b/parsons/solidarity_tech/st_chapters.py index fb5bee7899..0dadc83d95 100644 --- a/parsons/solidarity_tech/st_chapters.py +++ b/parsons/solidarity_tech/st_chapters.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index 5be2cbb6e5..6952984830 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -4,13 +4,13 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime - from parsons.solidarity_tech.solidarity_tech_base import ParamsType - from parsons.solidarity_tech.solidarity_tech_enums import FieldType, ScopeType + from parsons.solidarity_tech.base import ParamsType + from parsons.solidarity_tech.enums import FieldType, ScopeType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_donation_charges.py b/parsons/solidarity_tech/st_donation_charges.py index 04aa970cad..0dd577cd62 100644 --- a/parsons/solidarity_tech/st_donation_charges.py +++ b/parsons/solidarity_tech/st_donation_charges.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime diff --git a/parsons/solidarity_tech/st_email_blasts.py b/parsons/solidarity_tech/st_email_blasts.py index 57e5f730b3..bc8e97e194 100644 --- a/parsons/solidarity_tech/st_email_blasts.py +++ b/parsons/solidarity_tech/st_email_blasts.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime diff --git a/parsons/solidarity_tech/st_email_senders.py b/parsons/solidarity_tech/st_email_senders.py index 9b4f9c57fd..9fb03f6060 100644 --- a/parsons/solidarity_tech/st_email_senders.py +++ b/parsons/solidarity_tech/st_email_senders.py @@ -3,7 +3,7 @@ import logging from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_emails.py b/parsons/solidarity_tech/st_emails.py index 9332497970..71de46bd1b 100644 --- a/parsons/solidarity_tech/st_emails.py +++ b/parsons/solidarity_tech/st_emails.py @@ -3,10 +3,10 @@ import logging from typing import TYPE_CHECKING -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index 2aa9e4d8ac..549b9ce539 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -4,14 +4,14 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime import numpy as np - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index 17393eb657..a5ec5a0b77 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -4,15 +4,15 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime import numpy as np - from parsons.solidarity_tech.solidarity_tech_base import ParamsType - from parsons.solidarity_tech.solidarity_tech_enums import AttendanceStatus + from parsons.solidarity_tech.base import ParamsType + from parsons.solidarity_tech.enums import AttendanceStatus logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index 600b01ff3e..1972ebac38 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -4,15 +4,15 @@ from typing import TYPE_CHECKING, Any, Literal from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime import numpy as np - from parsons.solidarity_tech.solidarity_tech_base import ParamsType - from parsons.solidarity_tech.solidarity_tech_enums import EventType + from parsons.solidarity_tech.base import ParamsType + from parsons.solidarity_tech.enums import EventType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index 34cebdf3a7..b325953cc3 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -4,15 +4,15 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime import numpy as np - from parsons.solidarity_tech.solidarity_tech_base import ParamsType - from parsons.solidarity_tech.solidarity_tech_enums import EventType, ScopeType + from parsons.solidarity_tech.base import ParamsType + from parsons.solidarity_tech.enums import EventType, ScopeType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_field_survey_urls.py b/parsons/solidarity_tech/st_field_survey_urls.py index 9ea4c5f80f..239a0a0a9e 100644 --- a/parsons/solidarity_tech/st_field_survey_urls.py +++ b/parsons/solidarity_tech/st_field_survey_urls.py @@ -3,7 +3,7 @@ import logging from typing import Any, Literal -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_organizations.py b/parsons/solidarity_tech/st_organizations.py index 3fb45e713a..707b577a31 100644 --- a/parsons/solidarity_tech/st_organizations.py +++ b/parsons/solidarity_tech/st_organizations.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime diff --git a/parsons/solidarity_tech/st_pages.py b/parsons/solidarity_tech/st_pages.py index bf0ab02070..6e0d925b9c 100644 --- a/parsons/solidarity_tech/st_pages.py +++ b/parsons/solidarity_tech/st_pages.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py index 18b41afdf5..3ec985dd72 100644 --- a/parsons/solidarity_tech/st_phonebanks.py +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_scheduled_calls.py b/parsons/solidarity_tech/st_scheduled_calls.py index 4b71827dd0..1cc872322f 100644 --- a/parsons/solidarity_tech/st_scheduled_calls.py +++ b/parsons/solidarity_tech/st_scheduled_calls.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py index 625d659abb..e9406ea09d 100644 --- a/parsons/solidarity_tech/st_scheduled_tasks.py +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -5,12 +5,12 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: import numpy as np - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_task_agents.py b/parsons/solidarity_tech/st_task_agents.py index b86dc1f4f0..f76a54b933 100644 --- a/parsons/solidarity_tech/st_task_agents.py +++ b/parsons/solidarity_tech/st_task_agents.py @@ -4,14 +4,14 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime import numpy as np - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_task_assignments.py b/parsons/solidarity_tech/st_task_assignments.py index 9b179a40db..48656d5e79 100644 --- a/parsons/solidarity_tech/st_task_assignments.py +++ b/parsons/solidarity_tech/st_task_assignments.py @@ -4,14 +4,14 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime import numpy as np - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py index 0ec888b400..4534604487 100644 --- a/parsons/solidarity_tech/st_team_members.py +++ b/parsons/solidarity_tech/st_team_members.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime - from parsons.solidarity_tech.solidarity_tech_enums import InviteType, ScopeType + from parsons.solidarity_tech.enums import InviteType, ScopeType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_text_blasts.py b/parsons/solidarity_tech/st_text_blasts.py index 0d834997cb..d65cef1766 100644 --- a/parsons/solidarity_tech/st_text_blasts.py +++ b/parsons/solidarity_tech/st_text_blasts.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime diff --git a/parsons/solidarity_tech/st_text_templates.py b/parsons/solidarity_tech/st_text_templates.py index e3c1f67431..1a6f09713f 100644 --- a/parsons/solidarity_tech/st_text_templates.py +++ b/parsons/solidarity_tech/st_text_templates.py @@ -4,15 +4,15 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime import numpy as np - from parsons.solidarity_tech.solidarity_tech_base import ParamsType - from parsons.solidarity_tech.solidarity_tech_enums import ScopeType + from parsons.solidarity_tech.base import ParamsType + from parsons.solidarity_tech.enums import ScopeType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_textbanks.py b/parsons/solidarity_tech/st_textbanks.py index c65ff88000..a1e23a5ace 100644 --- a/parsons/solidarity_tech/st_textbanks.py +++ b/parsons/solidarity_tech/st_textbanks.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_texts.py b/parsons/solidarity_tech/st_texts.py index 5c786a1c27..d7224c5d58 100644 --- a/parsons/solidarity_tech/st_texts.py +++ b/parsons/solidarity_tech/st_texts.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_user_actions.py b/parsons/solidarity_tech/st_user_actions.py index 39ef17b57a..ba5faf7391 100644 --- a/parsons/solidarity_tech/st_user_actions.py +++ b/parsons/solidarity_tech/st_user_actions.py @@ -4,14 +4,14 @@ from typing import TYPE_CHECKING, Any, Literal from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime import numpy as np - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_user_lists.py b/parsons/solidarity_tech/st_user_lists.py index 2927a5b19b..76f20fcab9 100644 --- a/parsons/solidarity_tech/st_user_lists.py +++ b/parsons/solidarity_tech/st_user_lists.py @@ -5,14 +5,14 @@ from typing import TYPE_CHECKING, Any from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime import numpy as np - from parsons.solidarity_tech.solidarity_tech_enums import ScopeType + from parsons.solidarity_tech.enums import ScopeType CompareValueType = str | numbers.Rational | bool QueryParamType = dict[ diff --git a/parsons/solidarity_tech/st_user_notes.py b/parsons/solidarity_tech/st_user_notes.py index 6b3b25d2e4..3b99a8e695 100644 --- a/parsons/solidarity_tech/st_user_notes.py +++ b/parsons/solidarity_tech/st_user_notes.py @@ -3,11 +3,11 @@ import logging from typing import TYPE_CHECKING -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: - from parsons.solidarity_tech.solidarity_tech_base import ParamsType - from parsons.solidarity_tech.solidarity_tech_enums import InteractionType + from parsons.solidarity_tech.base import ParamsType + from parsons.solidarity_tech.enums import InteractionType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_user_relationships.py b/parsons/solidarity_tech/st_user_relationships.py index 8fc93bec5e..181a7bb448 100644 --- a/parsons/solidarity_tech/st_user_relationships.py +++ b/parsons/solidarity_tech/st_user_relationships.py @@ -5,10 +5,10 @@ from typing import TYPE_CHECKING, Literal from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) diff --git a/parsons/solidarity_tech/st_users.py b/parsons/solidarity_tech/st_users.py index 2a851eeb14..cca58fa96d 100644 --- a/parsons/solidarity_tech/st_users.py +++ b/parsons/solidarity_tech/st_users.py @@ -6,12 +6,12 @@ from zoneinfo import ZoneInfo from parsons import Table -from parsons.solidarity_tech.solidarity_tech_base import SolidarityTechBase +from parsons.solidarity_tech.base import SolidarityTechBase if TYPE_CHECKING: from datetime import datetime - from parsons.solidarity_tech.solidarity_tech_base import ParamsType + from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) From 64ff3bbaf5f7b14e91d93e23844c112c74106f49 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Thu, 6 Aug 2026 13:17:56 -0400 Subject: [PATCH 82/94] add enums to documentation --- docs/solidarity_tech.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/solidarity_tech.rst b/docs/solidarity_tech.rst index 5dfead1cee..23b89b0208 100644 --- a/docs/solidarity_tech.rst +++ b/docs/solidarity_tech.rst @@ -71,4 +71,8 @@ API :inherited-members: :members: +.. automodule:: parsons.solidarity_tech.enums + :inherited-members: + :members: + .. _Solidarity Tech: https://www.solidarity.tech/ From 76032fc8c8bbfcb08df3bab842c071bab09da9cd Mon Sep 17 00:00:00 2001 From: Ramona T Date: Fri, 7 Aug 2026 10:33:02 -0400 Subject: [PATCH 83/94] Type fixes in SolidarityTechBase --- parsons/solidarity_tech/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/parsons/solidarity_tech/base.py b/parsons/solidarity_tech/base.py index 6e5063ab6d..36b938cd72 100644 --- a/parsons/solidarity_tech/base.py +++ b/parsons/solidarity_tech/base.py @@ -12,7 +12,7 @@ from parsons.solidarity_tech.exceptions import STFailedResponseError, STUnexpectedResponseError from parsons.utilities import check_env -from parsons.utilities.api_connector import APIConnector, _ParamsType +from parsons.utilities.api_connector import APIConnector, _JsonType if TYPE_CHECKING: from collections.abc import Mapping @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) -ParamsType = _ParamsType | np.int64 +ParamsType = _JsonType | np.int64 class SolidarityTechBase: @@ -123,7 +123,7 @@ def _get_single_resource(self, endpoint: str, id: int, **kwargs) -> requests.Res def _post_request( self, endpoint: str, - payload: Mapping[str, ParamsType] | None = None, + payload: Mapping[str, _JsonType] | None = None, **kwargs, ) -> requests.Response: """Handle POST requests.""" @@ -134,7 +134,7 @@ def _put_request( self, endpoint: str, id: int, - payload: Mapping[str, ParamsType] | None = None, + payload: Mapping[str, _JsonType] | None = None, **kwargs, ) -> requests.Response: """Handle PUT requests.""" From 61452dd8b6bd3e4dbbf3e03b99631a338ee97335 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Fri, 7 Aug 2026 10:33:29 -0400 Subject: [PATCH 84/94] Document and improve SolidarityTech tests --- test/test_solidarity-tech/conftest.py | 2 + test/test_solidarity-tech/test_st_base.py | 89 ++++++++++++++++++++--- test/test_solidarity-tech/test_st_init.py | 12 ++- 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/test/test_solidarity-tech/conftest.py b/test/test_solidarity-tech/conftest.py index 15a743b35a..822bc82b51 100644 --- a/test/test_solidarity-tech/conftest.py +++ b/test/test_solidarity-tech/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from parsons.solidarity_tech import SolidarityTech diff --git a/test/test_solidarity-tech/test_st_base.py b/test/test_solidarity-tech/test_st_base.py index 9bc0f4ace3..dd14185a4c 100644 --- a/test/test_solidarity-tech/test_st_base.py +++ b/test/test_solidarity-tech/test_st_base.py @@ -1,17 +1,25 @@ +from __future__ import annotations + import re from datetime import datetime, timezone +from typing import TYPE_CHECKING import pytest import requests -from pytest_mock import MockerFixture from requests_mock import GET, POST, Mocker -from parsons.solidarity_tech import SolidarityTech from parsons.solidarity_tech.exceptions import STFailedResponseError, STUnexpectedResponseError +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + from parsons.solidarity_tech import SolidarityTech + from parsons.utilities.api_connector import _JsonType, _ParamsType + @pytest.fixture def known_status_codes() -> dict[int, tuple[bool, str]]: + """Known status codes and their expected outcomes.""" return { 200: (True, "OK"), 201: (True, "updated resource"), @@ -21,51 +29,86 @@ def known_status_codes() -> dict[int, tuple[bool, str]]: class Test_Post_Request: + """Tests for the _post_request method.""" + + @pytest.mark.parametrize( + "endpoint", ["custom_user_properties", "event_sessions/295876/hosts", "field_survey_urls"] + ) + def test_get_single_resource_handles_varied_endpoints( + self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture, endpoint: str + ) -> None: + """Make a POST request to varied endpoints.""" + endpoint_url = f"{st.api_url}/{endpoint}" + + requests_mock.post(endpoint_url) + spy = mocker.spy(st.api, "request") + + st._post_request(endpoint) + + spy.assert_called_once_with(url=endpoint_url, req_type=POST) + def test_get_single_resource_makes_request_with_payload( self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture ) -> None: - payload = {"user_id": 654123} + """Makes a POST request with payload.""" + payload: _JsonType = {"user_id": 654123} requests_mock.post(st.api_url) spy = mocker.spy(st.api, "request") st._post_request(st.api_url, payload=payload) + spy.assert_called_once_with(url=st.api_url, req_type=POST, json=payload) def test_get_single_resource_makes_request_with_params( self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture ) -> None: - params = {"automation_id": 35876} + """Make a POST request with params.""" + params: _ParamsType = {"automation_id": 35876} requests_mock.post(st.api_url) spy = mocker.spy(st.api, "request") st._post_request(st.api_url, params=params) + spy.assert_called_once_with(url=st.api_url, req_type=POST, json=None, params=params) class Test_Get_Single_Resource: + """Tests for the _get_single_resource method.""" + def test_get_single_resource_makes_request_with_id( self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture ) -> None: + """Make a GET request with an ID.""" id = 42 - endpoint = f"{st.api_url}/{id}" + endpoint = "users" + endpoint_url = f"{st.api_url}/{endpoint}/{id}" - requests_mock.get(endpoint) + requests_mock.get(endpoint_url) spy = mocker.spy(st.api, "request") - st._get_single_resource(st.api_url, id) - spy.assert_called_once_with(url=endpoint, req_type=GET) + st._get_single_resource(endpoint, id) + + spy.assert_called_once_with(url=endpoint_url, req_type=GET) class Test_Get_Resources: + """Tests for the _get_resources method.""" + + @pytest.mark.parametrize("endpoint", ["activities", "agent_assignments", "users/124876"]) def test_get_resources_makes_request( - self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture + self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture, endpoint: str ) -> None: - requests_mock.get(st.api_url) + """Make a GET request to varied endpoints.""" + endpoint_url = f"{st.api_url}/{endpoint}" + + requests_mock.get(endpoint_url) spy = mocker.spy(st.api, "request") - st._get_resources(st.api_url) - spy.assert_called_once_with(url=st.api_url, req_type=GET) + + st._get_resources(endpoint) + + spy.assert_called_once_with(url=endpoint_url, req_type=GET) def test_get_resources_datetime( self, @@ -73,6 +116,7 @@ def test_get_resources_datetime( requests_mock: Mocker, mocker: MockerFixture, ) -> None: + """Convert datetime-typed ``since``.""" now_datetime = datetime.now(tz=timezone.utc) now_timestamp = int(now_datetime.timestamp()) @@ -95,8 +139,10 @@ def test_get_resources_remaps_special_query_strings( requests_mock: Mocker, mocker: MockerFixture, ) -> None: + """Integrate special query names provided as keyword arguments.""" requests_mock.get(st.api_url) spy = mocker.spy(st.api, "request") + st._get_resources( st.api_url, limit=123456, @@ -105,6 +151,7 @@ def test_get_resources_remaps_special_query_strings( since=456321, include_count=123654, ) + spy.assert_called_once_with( url=st.api_url, req_type=GET, @@ -122,12 +169,16 @@ def test_get_resources_param_collision_error( st: SolidarityTech, requests_mock: Mocker, ) -> None: + """Raise a :class:`KeyError` when a query passed in keyword arguments collides with one passed in params.""" requests_mock.get(st.api_url) + with pytest.raises(KeyError, match="Request param '_limit' already exists"): st._get_resources(st.api_url, limit=15, params={"_limit": 30}) class Test_Add_If_Field_Not_Empty: + """Test the ``_add_if_field_not_empty`` method.""" + @pytest.mark.parametrize( ("key", "value", "expected"), [ @@ -143,11 +194,13 @@ def test_add_if_field_not_empty( value: str | int | None, expected: dict[str, str | int], ) -> None: + """Add only items with a value.""" init_dict = {} result = st._add_if_field_not_empty(init_dict, key, value) assert result == expected def test_add_if_field_not_empty_overwrite(self, st: SolidarityTech) -> None: + """Overwrite existing keys when ``overwrite`` is ``True``.""" init_dict = {"test_key": "original_value"} result = st._add_if_field_not_empty( init_dict, "test_key", "overwrite_value", overwrite=True @@ -155,17 +208,21 @@ def test_add_if_field_not_empty_overwrite(self, st: SolidarityTech) -> None: assert result["test_key"] == "overwrite_value" def test_add_if_field_not_empty_no_overwrite_default(self, st: SolidarityTech) -> None: + """Don't overwrite existing keys when ``overwrite`` is not provided.""" init_dict = {"test_key": "original_value"} with pytest.raises(KeyError, match="'test_key' already exists"): st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value") def test_add_if_field_not_empty_no_overwrite(self, st: SolidarityTech) -> None: + """Raise a :class`KeyError` when ``overwrite`` is ``False`` and the key already exists.""" init_dict = {"test_key": "original_value"} with pytest.raises(KeyError, match="'test_key' already exists"): st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value", overwrite=False) class Test_Handle_Status_Codes: + """Test the ``_handle_status_codes`` method.""" + @pytest.mark.parametrize( "status_code", [200, 201, 404, 422], @@ -177,6 +234,13 @@ def test_handle_status_codes( known_status_codes: dict[int, tuple[bool, str]], status_code: int, ) -> None: + """ + Handle known status codes. + + Raise a :class:`STFailedResponseError` if parsing a known failure status code, + return ``True`` if parsing a known success status code. + + """ requests_mock.get("https://api.example.com", status_code=status_code) res = requests.get("https://api.example.com") @@ -197,6 +261,7 @@ def test_handle_status_codes_unrecognized( requests_mock: Mocker, known_status_codes: dict[int, tuple[bool, str]], ) -> None: + """Raise a :class:`STUnexpectedResponseError` if parsing an unrecognized status code.""" status_code = 500 requests_mock.get("https://api.example.com", status_code=status_code) diff --git a/test/test_solidarity-tech/test_st_init.py b/test/test_solidarity-tech/test_st_init.py index 75ee14f2c7..bb1e58f832 100644 --- a/test/test_solidarity-tech/test_st_init.py +++ b/test/test_solidarity-tech/test_st_init.py @@ -1,21 +1,29 @@ +from __future__ import annotations + import os +from typing import TYPE_CHECKING import pytest -from pytest_mock import MockerFixture from parsons.solidarity_tech import SolidarityTech +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + TOKEN_ENV_NAME = "SOLIDARITY_TECH_BEARER_KEY" TOKEN_PLACEHOLDER = "SOME_BEARER_KEY" def test_init_with_arg() -> None: + """Set api_token property and header when initialized via an argument.""" st = SolidarityTech(api_token=TOKEN_PLACEHOLDER) assert st.api_token == TOKEN_PLACEHOLDER assert st.headers.get("authorization") == f"Bearer {TOKEN_PLACEHOLDER}" def test_init_with_env(mocker: MockerFixture) -> None: + """Set api_token property and header when initialized via environment variable.""" mocker.patch.dict(os.environ, {TOKEN_ENV_NAME: TOKEN_PLACEHOLDER}) st = SolidarityTech() assert st.api_token == TOKEN_PLACEHOLDER @@ -23,9 +31,11 @@ def test_init_with_env(mocker: MockerFixture) -> None: def test_init_with_no_api_token() -> None: + """Raise :class:`KeyError` when no API token is provided and the environment variable is not set.""" with pytest.raises(KeyError, match=f"No '{TOKEN_ENV_NAME}' found."): SolidarityTech() def test_init_api_url(st: SolidarityTech) -> None: + """Set api_url property.""" assert st.api_url == "https://api.solidarity.tech/v1" From 5c40049fc00a6b24c708cc2e656d73f841b95173 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Fri, 7 Aug 2026 10:33:48 -0400 Subject: [PATCH 85/94] Updated APIConnector typing --- parsons/utilities/api_connector.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/parsons/utilities/api_connector.py b/parsons/utilities/api_connector.py index 0cf181f6c1..e1e5ced33d 100644 --- a/parsons/utilities/api_connector.py +++ b/parsons/utilities/api_connector.py @@ -108,7 +108,7 @@ def __init__( self.session.auth = auth if headers: - self.session.headers = headers # ignore: type[invalid-assignment] + self.session.headers = headers # ignore: type[ty:invalid-assignment] # pyright: ignore [reportAttributeAccessIssue] @property def auth(self) -> _AuthType: @@ -128,7 +128,7 @@ def headers(self) -> _HeadersType: @headers.setter def headers(self, inp: _HeadersType) -> None: - self.session.headers = inp # ignore: type[invalid-assignment] + self.session.headers = inp # ignore: type[ty:invalid-assignment] # pyright: ignore [reportAttributeAccessIssue] @headers.deleter def headers(self) -> None: @@ -139,7 +139,7 @@ def request( url: str, req_type: Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], *, - json: Any | None = None, + json: _JsonType | None = None, data: _DataType | None = None, params: _ParamsType | None = None, raise_on_error: bool = True, @@ -263,7 +263,7 @@ def post_request( *, params: _ParamsType | None = None, data: _DataType | None = None, - json: Any | None = None, + json: _JsonType | None = None, success_codes: list[int] | None = None, raise_on_error: bool = True, **kwargs, @@ -366,7 +366,7 @@ def put_request( url: str, *, data: _DataType | None = None, - json: Any | None = None, + json: _JsonType | None = None, params: _ParamsType | None = None, success_codes: list[int] | None = None, raise_on_error: bool = True, @@ -419,7 +419,7 @@ def patch_request( *, params: _ParamsType | None = None, data: _DataType | None = None, - json: Any | None = None, + json: _JsonType | None = None, success_codes: list[int] | None = None, raise_on_error: bool = True, **kwargs, From cfb9931adad583584885ebf8e6eac37291bc11e5 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Mon, 24 Aug 2026 20:05:34 -0400 Subject: [PATCH 86/94] cleanup base --- parsons/solidarity_tech/base.py | 21 +++++++++++---------- test/test_solidarity-tech/test_st_init.py | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/parsons/solidarity_tech/base.py b/parsons/solidarity_tech/base.py index 36b938cd72..d3959070f3 100644 --- a/parsons/solidarity_tech/base.py +++ b/parsons/solidarity_tech/base.py @@ -25,6 +25,8 @@ class SolidarityTechBase: + """Base class for interacting with the SolidarityTech API.""" + def __init__( self, api_token: str | None = None, *, session: requests.Session | None = None ) -> None: @@ -43,9 +45,9 @@ def __init__( """ self.api_token = cast("str", check_env.check("SOLIDARITY_TECH_BEARER_KEY", api_token)) self.headers = CaseInsensitiveDict({"authorization": f"Bearer {self.api_token}"}) - self.api_url = "https://api.solidarity.tech/v1" + self.api_url = "https://api.solidarity.tech/v1/" self.api = APIConnector( - self.api_url, + uri=self.api_url, headers=self.headers, ratelimiter=requests_ratelimiter.Limiter( pyrate_limiter.Rate(60, pyrate_limiter.Duration.SECOND * 30) @@ -114,9 +116,9 @@ def _get_resources(self, endpoint: str, **kwargs) -> requests.Response: logger.debug("Processing GET request at endpoint: %s", endpoint, extra=params) return self.api.request(url=endpoint, req_type="GET", **kwargs) - def _get_single_resource(self, endpoint: str, id: int, **kwargs) -> requests.Response: + def _get_single_resource(self, endpoint: str, resource_id: int, **kwargs) -> requests.Response: """Handle GET requests for single resources.""" - complete_endpoint = f"{endpoint}/{id}" + complete_endpoint = f"{endpoint}/{resource_id}" logger.debug("Processing GET request at endpoint: %s", complete_endpoint) return self.api.request(url=complete_endpoint, req_type="GET", **kwargs) @@ -133,18 +135,18 @@ def _post_request( def _put_request( self, endpoint: str, - id: int, + resource_id: int, payload: Mapping[str, _JsonType] | None = None, **kwargs, ) -> requests.Response: """Handle PUT requests.""" - complete_endpoint = f"{endpoint}/{id}" + complete_endpoint = f"{endpoint}/{resource_id}" logger.debug("Processing PUT request at endpoint: %s", complete_endpoint, extra=payload) return self.api.request(url=complete_endpoint, req_type="PUT", json=payload, **kwargs) - def _del_request(self, endpoint: str, id: int | str, **kwargs) -> requests.Response: + def _del_request(self, endpoint: str, resource_id: int | str, **kwargs) -> requests.Response: """Handle DEL requests.""" - complete_endpoint = f"{endpoint}/{id}" + complete_endpoint = f"{endpoint}/{resource_id}" logger.debug("Processing DEL request at endpoint: %s", complete_endpoint) return self.api.request(url=complete_endpoint, req_type="DELETE", **kwargs) @@ -177,7 +179,7 @@ def _handle_status_codes( raise STUnexpectedResponseError(response=res) def _add_if_field_not_empty( - self, receiving_dict: dict, key: str, value: Any | None, overwrite: bool = False + self, receiving_dict: dict, key: str, value: Any | None, *, overwrite: bool = False ) -> dict: """ Add a key/value pair to a dictionary if the value is not None. @@ -211,6 +213,5 @@ def _add_if_field_not_empty( logger.debug( "Skipping adding '%s' to payload or parameters dictionary as value is None", key, - value, ) return receiving_dict diff --git a/test/test_solidarity-tech/test_st_init.py b/test/test_solidarity-tech/test_st_init.py index bb1e58f832..5a845d901a 100644 --- a/test/test_solidarity-tech/test_st_init.py +++ b/test/test_solidarity-tech/test_st_init.py @@ -38,4 +38,4 @@ def test_init_with_no_api_token() -> None: def test_init_api_url(st: SolidarityTech) -> None: """Set api_url property.""" - assert st.api_url == "https://api.solidarity.tech/v1" + assert st.api_url == "https://api.solidarity.tech/v1/" From f4263da878aa0fc509c83181325c5acbe6320c86 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Mon, 24 Aug 2026 20:05:51 -0400 Subject: [PATCH 87/94] Improve tests to handle APIConnector changes --- test/test_solidarity-tech/test_st_base.py | 147 ++++++++++++---------- 1 file changed, 80 insertions(+), 67 deletions(-) diff --git a/test/test_solidarity-tech/test_st_base.py b/test/test_solidarity-tech/test_st_base.py index dd14185a4c..afec53ee51 100644 --- a/test/test_solidarity-tech/test_st_base.py +++ b/test/test_solidarity-tech/test_st_base.py @@ -3,15 +3,15 @@ import re from datetime import datetime, timezone from typing import TYPE_CHECKING +from urllib.parse import urlencode, urlsplit import pytest import requests -from requests_mock import GET, POST, Mocker from parsons.solidarity_tech.exceptions import STFailedResponseError, STUnexpectedResponseError if TYPE_CHECKING: - from pytest_mock import MockerFixture + from requests_mock import Mocker from parsons.solidarity_tech import SolidarityTech from parsons.utilities.api_connector import _JsonType, _ParamsType @@ -28,122 +28,129 @@ def known_status_codes() -> dict[int, tuple[bool, str]]: } -class Test_Post_Request: +class TestPostRequest: """Tests for the _post_request method.""" @pytest.mark.parametrize( "endpoint", ["custom_user_properties", "event_sessions/295876/hosts", "field_survey_urls"] ) def test_get_single_resource_handles_varied_endpoints( - self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture, endpoint: str + self, st: SolidarityTech, requests_mock: Mocker, endpoint: str ) -> None: """Make a POST request to varied endpoints.""" - endpoint_url = f"{st.api_url}/{endpoint}" + endpoint_url = f"{st.api_url}{endpoint}" + _ = requests_mock.post(endpoint_url) - requests_mock.post(endpoint_url) - spy = mocker.spy(st.api, "request") + _ = st._post_request(endpoint) - st._post_request(endpoint) - - spy.assert_called_once_with(url=endpoint_url, req_type=POST) + assert requests_mock.call_count == 1 + assert requests_mock.last_request is not None + assert requests_mock.last_request.method == "POST" + assert requests_mock.last_request.url == endpoint_url def test_get_single_resource_makes_request_with_payload( - self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture + self, st: SolidarityTech, requests_mock: Mocker ) -> None: """Makes a POST request with payload.""" payload: _JsonType = {"user_id": 654123} + _ = requests_mock.post(st.api_url) - requests_mock.post(st.api_url) - spy = mocker.spy(st.api, "request") - - st._post_request(st.api_url, payload=payload) + _ = st._post_request(st.api_url, payload=payload) - spy.assert_called_once_with(url=st.api_url, req_type=POST, json=payload) + assert requests_mock.call_count == 1 + assert requests_mock.last_request is not None + assert requests_mock.last_request.method == "POST" + assert requests_mock.last_request.url == st.api_url + assert requests_mock.last_request.json() == payload def test_get_single_resource_makes_request_with_params( - self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture + self, st: SolidarityTech, requests_mock: Mocker ) -> None: """Make a POST request with params.""" params: _ParamsType = {"automation_id": 35876} + _ = requests_mock.post(st.api_url) - requests_mock.post(st.api_url) - spy = mocker.spy(st.api, "request") + _ = st._post_request(st.api_url, params=params) - st._post_request(st.api_url, params=params) + assert requests_mock.call_count == 1 + assert requests_mock.last_request is not None + assert requests_mock.last_request.method == "POST" - spy.assert_called_once_with(url=st.api_url, req_type=POST, json=None, params=params) + last_url = urlsplit(requests_mock.last_request.url) + assert f"{last_url.scheme}://{last_url.netloc}{last_url.path}" == st.api_url + assert last_url.query == urlencode(params) -class Test_Get_Single_Resource: +class TestGetSingleResource: """Tests for the _get_single_resource method.""" def test_get_single_resource_makes_request_with_id( - self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture + self, st: SolidarityTech, requests_mock: Mocker ) -> None: """Make a GET request with an ID.""" - id = 42 + resource_id = 42 endpoint = "users" - endpoint_url = f"{st.api_url}/{endpoint}/{id}" - - requests_mock.get(endpoint_url) - spy = mocker.spy(st.api, "request") + endpoint_url = f"{st.api_url}{endpoint}/{resource_id}" + _ = requests_mock.get(endpoint_url, json={"id": resource_id}) - st._get_single_resource(endpoint, id) + _ = st._get_single_resource(endpoint, resource_id) - spy.assert_called_once_with(url=endpoint_url, req_type=GET) + assert requests_mock.call_count == 1 + assert requests_mock.last_request is not None + assert requests_mock.last_request.method == "GET" + assert requests_mock.last_request.url == endpoint_url -class Test_Get_Resources: +class TestGetResources: """Tests for the _get_resources method.""" @pytest.mark.parametrize("endpoint", ["activities", "agent_assignments", "users/124876"]) def test_get_resources_makes_request( - self, st: SolidarityTech, requests_mock: Mocker, mocker: MockerFixture, endpoint: str + self, st: SolidarityTech, requests_mock: Mocker, endpoint: str ) -> None: """Make a GET request to varied endpoints.""" - endpoint_url = f"{st.api_url}/{endpoint}" + endpoint_url = f"{st.api_url}{endpoint}" + _ = requests_mock.get(endpoint_url) - requests_mock.get(endpoint_url) - spy = mocker.spy(st.api, "request") + _ = st._get_resources(endpoint) - st._get_resources(endpoint) - - spy.assert_called_once_with(url=endpoint_url, req_type=GET) + assert requests_mock.call_count == 1 + assert requests_mock.last_request is not None + assert requests_mock.last_request.method == "GET" + assert requests_mock.last_request.url == endpoint_url def test_get_resources_datetime( self, st: SolidarityTech, requests_mock: Mocker, - mocker: MockerFixture, ) -> None: """Convert datetime-typed ``since``.""" now_datetime = datetime.now(tz=timezone.utc) now_timestamp = int(now_datetime.timestamp()) + _ = requests_mock.get(st.api_url) - requests_mock.get(st.api_url) - spy = mocker.spy(st.api, "request") - - st._get_resources( + _ = st._get_resources( st.api_url, since=now_datetime, ) - spy.assert_called_once_with( - url=st.api_url, - req_type=GET, - params={"_since": now_timestamp}, - ) + + assert requests_mock.call_count == 1 + assert requests_mock.last_request is not None + assert requests_mock.last_request.method == "GET" + + last_url = urlsplit(requests_mock.last_request.url) + assert f"{last_url.scheme}://{last_url.netloc}{last_url.path}" == st.api_url + assert last_url.query == urlencode({"_since": now_timestamp}) def test_get_resources_remaps_special_query_strings( self, st: SolidarityTech, requests_mock: Mocker, - mocker: MockerFixture, ) -> None: """Integrate special query names provided as keyword arguments.""" - requests_mock.get(st.api_url) - spy = mocker.spy(st.api, "request") + _ = requests_mock.get(st.api_url) - st._get_resources( + _ = st._get_resources( st.api_url, limit=123456, cursor=654321, @@ -152,16 +159,20 @@ def test_get_resources_remaps_special_query_strings( include_count=123654, ) - spy.assert_called_once_with( - url=st.api_url, - req_type=GET, - params={ + assert requests_mock.call_count == 1 + assert requests_mock.last_request is not None + assert requests_mock.last_request.method == "GET" + + last_url = urlsplit(requests_mock.last_request.url) + assert f"{last_url.scheme}://{last_url.netloc}{last_url.path}" == st.api_url + assert last_url.query == urlencode( + { "_limit": 123456, "_cursor": 654321, "_offset": 321456, "_since": 456321, "_include_count": 123654, - }, + } ) def test_get_resources_param_collision_error( @@ -170,13 +181,13 @@ def test_get_resources_param_collision_error( requests_mock: Mocker, ) -> None: """Raise a :class:`KeyError` when a query passed in keyword arguments collides with one passed in params.""" - requests_mock.get(st.api_url) + _ = requests_mock.get(st.api_url) with pytest.raises(KeyError, match="Request param '_limit' already exists"): - st._get_resources(st.api_url, limit=15, params={"_limit": 30}) + _ = st._get_resources(st.api_url, limit=15, params={"_limit": 30}) -class Test_Add_If_Field_Not_Empty: +class TestAddIfFieldNotEmpty: """Test the ``_add_if_field_not_empty`` method.""" @pytest.mark.parametrize( @@ -211,16 +222,18 @@ def test_add_if_field_not_empty_no_overwrite_default(self, st: SolidarityTech) - """Don't overwrite existing keys when ``overwrite`` is not provided.""" init_dict = {"test_key": "original_value"} with pytest.raises(KeyError, match="'test_key' already exists"): - st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value") + _ = st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value") def test_add_if_field_not_empty_no_overwrite(self, st: SolidarityTech) -> None: """Raise a :class`KeyError` when ``overwrite`` is ``False`` and the key already exists.""" init_dict = {"test_key": "original_value"} with pytest.raises(KeyError, match="'test_key' already exists"): - st._add_if_field_not_empty(init_dict, "test_key", "overwrite_value", overwrite=False) + _ = st._add_if_field_not_empty( + init_dict, "test_key", "overwrite_value", overwrite=False + ) -class Test_Handle_Status_Codes: +class TestHandleStatusCodes: """Test the ``_handle_status_codes`` method.""" @pytest.mark.parametrize( @@ -241,7 +254,7 @@ def test_handle_status_codes( return ``True`` if parsing a known success status code. """ - requests_mock.get("https://api.example.com", status_code=status_code) + _ = requests_mock.get("https://api.example.com", status_code=status_code) res = requests.get("https://api.example.com") success_expected = known_status_codes[status_code][0] @@ -253,7 +266,7 @@ def test_handle_status_codes( f"Request Failed (Status Code {status_code}) -- {failure_description}" ) with pytest.raises(STFailedResponseError, match=err_msg): - st._handle_status_codes(res, known_status_codes) + _ = st._handle_status_codes(res, known_status_codes) def test_handle_status_codes_unrecognized( self, @@ -264,11 +277,11 @@ def test_handle_status_codes_unrecognized( """Raise a :class:`STUnexpectedResponseError` if parsing an unrecognized status code.""" status_code = 500 - requests_mock.get("https://api.example.com", status_code=status_code) + _ = requests_mock.get("https://api.example.com", status_code=status_code) res = requests.get("https://api.example.com") with pytest.raises( STUnexpectedResponseError, match=re.escape(f"Unexpected Response (Status Code {status_code})"), ): - st._handle_status_codes(res, known_status_codes) + _ = st._handle_status_codes(res, known_status_codes) From 30441561bccaa253f58496a973734f210398f9d6 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 26 Aug 2026 21:21:53 -0400 Subject: [PATCH 88/94] remove unecessary pass statement --- parsons/solidarity_tech/solidarity_tech.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/parsons/solidarity_tech/solidarity_tech.py b/parsons/solidarity_tech/solidarity_tech.py index d8bbc44736..6e9532884a 100644 --- a/parsons/solidarity_tech/solidarity_tech.py +++ b/parsons/solidarity_tech/solidarity_tech.py @@ -92,5 +92,3 @@ class SolidarityTech( from parsons.solidarity_tech import SolidarityTechEvents """ - - pass From e7bbcc0a5c6ec87f889114daa70a2c454c847c02 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 26 Aug 2026 21:24:15 -0400 Subject: [PATCH 89/94] Docstrings --- parsons/solidarity_tech/st_activities.py | 2 ++ parsons/solidarity_tech/st_agent_assignments.py | 2 ++ parsons/solidarity_tech/st_automation_enrollments.py | 2 ++ parsons/solidarity_tech/st_calls.py | 2 ++ parsons/solidarity_tech/st_chapter_phone_numbers.py | 2 ++ parsons/solidarity_tech/st_chapters.py | 2 ++ parsons/solidarity_tech/st_custom_user_properties.py | 2 ++ parsons/solidarity_tech/st_donation_charges.py | 2 ++ parsons/solidarity_tech/st_email_blasts.py | 2 ++ parsons/solidarity_tech/st_email_senders.py | 5 ++++- parsons/solidarity_tech/st_emails.py | 5 ++++- parsons/solidarity_tech/st_event_attendances.py | 2 ++ parsons/solidarity_tech/st_event_rsvps.py | 2 ++ parsons/solidarity_tech/st_event_sessions.py | 6 ++++-- parsons/solidarity_tech/st_events.py | 6 ++++-- parsons/solidarity_tech/st_field_survey_urls.py | 4 +++- parsons/solidarity_tech/st_organizations.py | 2 ++ parsons/solidarity_tech/st_pages.py | 2 ++ parsons/solidarity_tech/st_phonebanks.py | 2 ++ parsons/solidarity_tech/st_scheduled_calls.py | 2 ++ parsons/solidarity_tech/st_scheduled_tasks.py | 2 ++ parsons/solidarity_tech/st_task_agents.py | 2 ++ parsons/solidarity_tech/st_task_assignments.py | 2 ++ parsons/solidarity_tech/st_team_members.py | 2 ++ parsons/solidarity_tech/st_text_blasts.py | 2 ++ parsons/solidarity_tech/st_text_templates.py | 2 ++ parsons/solidarity_tech/st_textbanks.py | 2 ++ parsons/solidarity_tech/st_texts.py | 4 +++- parsons/solidarity_tech/st_user_actions.py | 4 +++- parsons/solidarity_tech/st_user_lists.py | 2 ++ parsons/solidarity_tech/st_user_notes.py | 2 ++ parsons/solidarity_tech/st_user_relationships.py | 2 ++ parsons/solidarity_tech/st_users.py | 2 ++ 33 files changed, 77 insertions(+), 9 deletions(-) diff --git a/parsons/solidarity_tech/st_activities.py b/parsons/solidarity_tech/st_activities.py index aa41137fe7..141aae8044 100644 --- a/parsons/solidarity_tech/st_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -19,6 +19,8 @@ class SolidarityTechActivities(SolidarityTechBase): + """Methods for interacting with the SolidarityTech activities endpoint.""" + def get_activities( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index 8213653030..6335e704e7 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -17,6 +17,8 @@ class SolidarityTechAgentAssignments(SolidarityTechBase): + """Methods for interacting with the SolidarityTech agent assignments endpoint.""" + def get_agent_assignments( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_automation_enrollments.py b/parsons/solidarity_tech/st_automation_enrollments.py index 47ef35e3ad..06e9fa0943 100644 --- a/parsons/solidarity_tech/st_automation_enrollments.py +++ b/parsons/solidarity_tech/st_automation_enrollments.py @@ -9,6 +9,8 @@ class SolidarityTechAutomationEnrollments(SolidarityTechBase): + """Methods for interacting with the SolidarityTech automation enrollments endpoint.""" + def enroll_user_in_automation( self, automation_id: int, diff --git a/parsons/solidarity_tech/st_calls.py b/parsons/solidarity_tech/st_calls.py index fd85ab03ff..a1045a4c4a 100644 --- a/parsons/solidarity_tech/st_calls.py +++ b/parsons/solidarity_tech/st_calls.py @@ -19,6 +19,8 @@ class SolidarityTechCalls(SolidarityTechBase): + """Methods for interacting with the SolidarityTech calls endpoint.""" + def get_calls( self, user_id: int | None = None, diff --git a/parsons/solidarity_tech/st_chapter_phone_numbers.py b/parsons/solidarity_tech/st_chapter_phone_numbers.py index 99ad6d54a9..7d99629293 100644 --- a/parsons/solidarity_tech/st_chapter_phone_numbers.py +++ b/parsons/solidarity_tech/st_chapter_phone_numbers.py @@ -15,6 +15,8 @@ class SolidarityTechChapterPhoneNumbers(SolidarityTechBase): + """Methods for interacting with the SolidarityTech chapter phone numbers endpoint.""" + def get_chapter_phone_numbers( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_chapters.py b/parsons/solidarity_tech/st_chapters.py index 0dadc83d95..aa2180a150 100644 --- a/parsons/solidarity_tech/st_chapters.py +++ b/parsons/solidarity_tech/st_chapters.py @@ -16,6 +16,8 @@ class SolidarityTechChapters(SolidarityTechBase): + """Methods for interacting with the SolidarityTech chapters endpoint.""" + def get_chapters( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index 6952984830..501330a9e3 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -19,6 +19,8 @@ class SolidarityTechCustomUserProperties(SolidarityTechBase): + """Methods for interacting with the SolidarityTech custom user properties endpoint.""" + def get_custom_user_properties( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_donation_charges.py b/parsons/solidarity_tech/st_donation_charges.py index 0dd577cd62..647cba77af 100644 --- a/parsons/solidarity_tech/st_donation_charges.py +++ b/parsons/solidarity_tech/st_donation_charges.py @@ -16,6 +16,8 @@ class SolidarityTechDonationCharges(SolidarityTechBase): + """Methods for interacting with the SolidarityTech donation charges endpoint.""" + def get_donation_charges( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_email_blasts.py b/parsons/solidarity_tech/st_email_blasts.py index bc8e97e194..3a91cff1b9 100644 --- a/parsons/solidarity_tech/st_email_blasts.py +++ b/parsons/solidarity_tech/st_email_blasts.py @@ -13,6 +13,8 @@ class SolidarityTechEmailBlasts(SolidarityTechBase): + """Methods for interacting with the SolidarityTech email blasts endpoint.""" + def get_email_blasts( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_email_senders.py b/parsons/solidarity_tech/st_email_senders.py index 9fb03f6060..3e5d4358f7 100644 --- a/parsons/solidarity_tech/st_email_senders.py +++ b/parsons/solidarity_tech/st_email_senders.py @@ -12,13 +12,16 @@ class SolidarityTechEmailSenders(SolidarityTechBase): + """Methods for interacting with the SolidarityTech email senders endpoint.""" + def get_email_senders( self, limit: int = 20, offset: int = 0, ) -> tuple[Table, EmailSenderMetadata]: """ - Returns a list of email senders available for the API key's scope. + Retrieve a list of email senders available for the API key's scope. + Use these sender IDs when sending emails via the POST /emails endpoint. Args: diff --git a/parsons/solidarity_tech/st_emails.py b/parsons/solidarity_tech/st_emails.py index 71de46bd1b..90f150555c 100644 --- a/parsons/solidarity_tech/st_emails.py +++ b/parsons/solidarity_tech/st_emails.py @@ -12,6 +12,8 @@ class SolidarityTechEmails(SolidarityTechBase): + """Methods for interacting with the SolidarityTech emails endpoint.""" + def send_one_off_email( self, user_id: int, @@ -25,7 +27,8 @@ def send_one_off_email( track_clicks: bool = True, ) -> bool: """ - Sends a single transactional email to a user. + Send a single transactional email to a user. + Supports Liquid templating for personalization (e.g., {{ first_name }}). Args: diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index 549b9ce539..f43af45efc 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -17,6 +17,8 @@ class SolidarityTechEventAttendances(SolidarityTechBase): + """Methods for interacting with the SolidarityTech event attendances endpoint.""" + def get_event_attendances( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index a5ec5a0b77..5d5ba06c26 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -18,6 +18,8 @@ class SolidarityTechEventRSVPs(SolidarityTechBase): + """Methods for interacting with the SolidarityTech event rsvps endpoint.""" + def get_event_rsvps( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index 1972ebac38..a50117e5e8 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -18,6 +18,8 @@ class SolidarityTechEventSessions(SolidarityTechBase): + """Methods for interacting with the SolidarityTech event sessions endpoint.""" + def get_event_sessions( self, limit: int = 20, @@ -367,7 +369,7 @@ def add_event_host( user_id: np.int64, ) -> bool: """ - Adds a user as a host of the event session. + Add a user as a host of the event session. .. admonition:: Atomic and Idempotent @@ -415,7 +417,7 @@ def remove_event_host( user_id: int, ) -> bool: """ - Removes a user from the event session hosts. + Remove a user from the event session hosts. .. admonition:: Atomic and Idempotent diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index b325953cc3..41de378a1b 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -18,6 +18,8 @@ class SolidarityTechEvents(SolidarityTechBase): + """Methods for interacting with the SolidarityTech events API.""" + def get_events( self, limit: int = 20, @@ -27,7 +29,7 @@ def get_events( scope_type: ScopeType | None = None, ) -> Table: """ - Lists events accessible within the given scope. + Retrieve events accessible within the given scope. Each event in the response includes ``primary_event_id`` and ``is_co_hosted_mirror``. For co-hosted events that appear across multiple organizations, @@ -183,7 +185,7 @@ def get_event( include_hosts: bool = False, ) -> dict: """ - Returns a single event. + Retrieve a single event. The response includes ``primary_event_id`` (always resolves to the original event ID, even for co-hosted mirrors) and diff --git a/parsons/solidarity_tech/st_field_survey_urls.py b/parsons/solidarity_tech/st_field_survey_urls.py index 239a0a0a9e..239ceacea5 100644 --- a/parsons/solidarity_tech/st_field_survey_urls.py +++ b/parsons/solidarity_tech/st_field_survey_urls.py @@ -11,6 +11,8 @@ class SolidarityTechFieldSurveyURLs(SolidarityTechBase): + """Methods for generating SolidarityTech field survey URLs.""" + def generate_field_survey_url( self, user_id: int, @@ -18,7 +20,7 @@ def generate_field_survey_url( page_id: int, ) -> FieldSurveyURL: """ - Generates a field survey URL for the given user, agent, and page. + Generate a field survey URL for the given user, agent, and page. Response contains complete URL with access token (expires in 24 hours), and an ISO 8601 timestamp of when the access token expires. diff --git a/parsons/solidarity_tech/st_organizations.py b/parsons/solidarity_tech/st_organizations.py index 707b577a31..a7924da3f8 100644 --- a/parsons/solidarity_tech/st_organizations.py +++ b/parsons/solidarity_tech/st_organizations.py @@ -13,6 +13,8 @@ class SolidarityTechOrganizations(SolidarityTechBase): + """Methods for interacting with the SolidarityTech organizations API.""" + def get_organizations( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_pages.py b/parsons/solidarity_tech/st_pages.py index 6e0d925b9c..9f5fb0a071 100644 --- a/parsons/solidarity_tech/st_pages.py +++ b/parsons/solidarity_tech/st_pages.py @@ -15,6 +15,8 @@ class SolidarityTechPages(SolidarityTechBase): + """Methods for interacting with the SolidarityTech pages endpoint.""" + def get_pages( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py index 3ec985dd72..06c078a7e1 100644 --- a/parsons/solidarity_tech/st_phonebanks.py +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -15,6 +15,8 @@ class SolidarityTechPhonebanks(SolidarityTechBase): + """Methods for interacting with the SolidarityTech phonebanks endpoint.""" + def get_phonebanks( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_scheduled_calls.py b/parsons/solidarity_tech/st_scheduled_calls.py index 1cc872322f..a1ac01e88b 100644 --- a/parsons/solidarity_tech/st_scheduled_calls.py +++ b/parsons/solidarity_tech/st_scheduled_calls.py @@ -15,6 +15,8 @@ class SolidarityTechScheduledCalls(SolidarityTechBase): + """Methods for interacting with the SolidarityTech scheduled calls endpoint.""" + def get_scheduled_calls( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py index e9406ea09d..be11a5193a 100644 --- a/parsons/solidarity_tech/st_scheduled_tasks.py +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -16,6 +16,8 @@ class SolidarityTechScheduledTasks(SolidarityTechBase): + """Methods for interacting with the SolidarityTech scheduled tasks endpoint.""" + def get_scheduled_tasks( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_task_agents.py b/parsons/solidarity_tech/st_task_agents.py index f76a54b933..8e932812cd 100644 --- a/parsons/solidarity_tech/st_task_agents.py +++ b/parsons/solidarity_tech/st_task_agents.py @@ -17,6 +17,8 @@ class SolidarityTechTaskAgents(SolidarityTechBase): + """Methods for interacting with the SolidarityTech task agents endpoint.""" + def get_task_agents( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_task_assignments.py b/parsons/solidarity_tech/st_task_assignments.py index 48656d5e79..a3a00b8e63 100644 --- a/parsons/solidarity_tech/st_task_assignments.py +++ b/parsons/solidarity_tech/st_task_assignments.py @@ -17,6 +17,8 @@ class SolidarityTechTaskAssignments(SolidarityTechBase): + """Methods for interacting with the SolidarityTech task assignments endpoint.""" + def get_task_assignments( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py index 4534604487..88c0e0abf3 100644 --- a/parsons/solidarity_tech/st_team_members.py +++ b/parsons/solidarity_tech/st_team_members.py @@ -15,6 +15,8 @@ class SolidarityTechTeamMembers(SolidarityTechBase): + """Methods for interacting with the SolidarityTech team members endpoint.""" + def get_team_members( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_text_blasts.py b/parsons/solidarity_tech/st_text_blasts.py index d65cef1766..9bd13ce2c0 100644 --- a/parsons/solidarity_tech/st_text_blasts.py +++ b/parsons/solidarity_tech/st_text_blasts.py @@ -13,6 +13,8 @@ class SolidarityTechTextBlasts(SolidarityTechBase): + """Methods for interacting with the SolidarityTech text blasts endpoint.""" + def get_text_blasts( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_text_templates.py b/parsons/solidarity_tech/st_text_templates.py index 1a6f09713f..c71472aa39 100644 --- a/parsons/solidarity_tech/st_text_templates.py +++ b/parsons/solidarity_tech/st_text_templates.py @@ -18,6 +18,8 @@ class SolidarityTechTextTemplates(SolidarityTechBase): + """Methods for interacting with the SolidarityTech text templates endpoint.""" + def get_text_templates( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_textbanks.py b/parsons/solidarity_tech/st_textbanks.py index a1e23a5ace..845345ee09 100644 --- a/parsons/solidarity_tech/st_textbanks.py +++ b/parsons/solidarity_tech/st_textbanks.py @@ -15,6 +15,8 @@ class SolidarityTechTextbanks(SolidarityTechBase): + """Methods for interacting with the SolidarityTech text banks endpoint.""" + def get_textbanks( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_texts.py b/parsons/solidarity_tech/st_texts.py index d7224c5d58..315ed2e2b2 100644 --- a/parsons/solidarity_tech/st_texts.py +++ b/parsons/solidarity_tech/st_texts.py @@ -15,6 +15,8 @@ class SolidarityTechTexts(SolidarityTechBase): + """Methods for interacting with the SolidarityTech texts endpoint.""" + def get_texts( self, user_id: int | None = None, @@ -73,7 +75,7 @@ def send_text( shorten_urls: bool | None = None, ) -> bool: """ - Sends a text to a specific user. + Send a text to a specific user. Args: user_id: diff --git a/parsons/solidarity_tech/st_user_actions.py b/parsons/solidarity_tech/st_user_actions.py index ba5faf7391..67eddbb4ce 100644 --- a/parsons/solidarity_tech/st_user_actions.py +++ b/parsons/solidarity_tech/st_user_actions.py @@ -17,6 +17,8 @@ class SolidarityTechUserActions(SolidarityTechBase): + """Methods for interacting with the SolidarityTech user actions endpoint.""" + def get_user_actions( self, user_id: int | None = None, @@ -27,7 +29,7 @@ def get_user_actions( since: int | datetime = 0, ) -> Table: """ - Lists user actions (form submissions). + Retrieve user actions (form submissions). .. admonition:: Filtering diff --git a/parsons/solidarity_tech/st_user_lists.py b/parsons/solidarity_tech/st_user_lists.py index 76f20fcab9..e53e02895b 100644 --- a/parsons/solidarity_tech/st_user_lists.py +++ b/parsons/solidarity_tech/st_user_lists.py @@ -23,6 +23,8 @@ class SolidarityTechUserLists(SolidarityTechBase): + """Methods for interacting with the SolidarityTech user lists endpoint.""" + def get_user_lists( self, limit: int = 20, diff --git a/parsons/solidarity_tech/st_user_notes.py b/parsons/solidarity_tech/st_user_notes.py index 3b99a8e695..796a87d7aa 100644 --- a/parsons/solidarity_tech/st_user_notes.py +++ b/parsons/solidarity_tech/st_user_notes.py @@ -13,6 +13,8 @@ class SolidarityTechUserNotes(SolidarityTechBase): + """Methods for interacting with the SolidarityTech user notes endpoint.""" + def create_user_note( self, user_id: int, diff --git a/parsons/solidarity_tech/st_user_relationships.py b/parsons/solidarity_tech/st_user_relationships.py index 181a7bb448..274546205b 100644 --- a/parsons/solidarity_tech/st_user_relationships.py +++ b/parsons/solidarity_tech/st_user_relationships.py @@ -20,6 +20,8 @@ class SolidarityTechUserRelationships(SolidarityTechBase): + """Methods for interacting with the SolidarityTech user relationships endpoint.""" + def get_user_relationships( self, user_id: int, diff --git a/parsons/solidarity_tech/st_users.py b/parsons/solidarity_tech/st_users.py index cca58fa96d..d8415bdd1f 100644 --- a/parsons/solidarity_tech/st_users.py +++ b/parsons/solidarity_tech/st_users.py @@ -26,6 +26,8 @@ class SolidarityTechUsers(SolidarityTechBase): + """Methods for interacting with the SolidarityTech users endpoint.""" + def get_users( self, limit: int = 20, From dca68d5b924ddc453662846da3e78466975ed07b Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 26 Aug 2026 21:26:00 -0400 Subject: [PATCH 90/94] move exception text to variable --- parsons/solidarity_tech/st_team_members.py | 3 ++- parsons/solidarity_tech/st_user_actions.py | 3 ++- parsons/solidarity_tech/st_users.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py index 88c0e0abf3..1f355e0cfd 100644 --- a/parsons/solidarity_tech/st_team_members.py +++ b/parsons/solidarity_tech/st_team_members.py @@ -113,7 +113,8 @@ def create_team_member( """ if not member_id and not phone_number and not email: - raise ValueError("One of member_id, phone_number, or email is required.") + err_msg = "One of member_id, phone_number, or email is required." + raise ValueError(err_msg) payload: dict[str, Any] = { "role_id": role_id, diff --git a/parsons/solidarity_tech/st_user_actions.py b/parsons/solidarity_tech/st_user_actions.py index 67eddbb4ce..e0464a8249 100644 --- a/parsons/solidarity_tech/st_user_actions.py +++ b/parsons/solidarity_tech/st_user_actions.py @@ -138,7 +138,8 @@ def create_user_action( and "phone_number" not in data and "email" not in data ): - raise ValueError("Either user_id, phone_number, or email must be provided") + err_msg = "Either user_id, phone_number, or email must be provided" + raise ValueError(err_msg) payload: dict[str, Any] = {"page_id": page_id} self._add_if_field_not_empty(payload, "user_id", user_id) diff --git a/parsons/solidarity_tech/st_users.py b/parsons/solidarity_tech/st_users.py index d8415bdd1f..63dd4c7c8b 100644 --- a/parsons/solidarity_tech/st_users.py +++ b/parsons/solidarity_tech/st_users.py @@ -243,7 +243,8 @@ def create_user( """ if not phone_number and not email: - raise ValueError("Either phone_number or email must be provided") + err_msg = "Either phone_number or email must be provided" + raise ValueError(err_msg) if isinstance(timezone, ZoneInfo): timezone = str(timezone.key) From cd4231743bade7bfc57f5abfdf47e34b2da27bef Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 26 Aug 2026 21:30:14 -0400 Subject: [PATCH 91/94] Don't shadow builtin id --- .../solidarity_tech/st_agent_assignments.py | 18 +++++------ .../st_custom_user_properties.py | 12 ++++---- .../solidarity_tech/st_donation_charges.py | 6 ++-- parsons/solidarity_tech/st_email_blasts.py | 6 ++-- .../solidarity_tech/st_event_attendances.py | 6 ++-- parsons/solidarity_tech/st_event_rsvps.py | 19 ++++++------ parsons/solidarity_tech/st_event_sessions.py | 30 +++++++++---------- parsons/solidarity_tech/st_events.py | 6 ++-- parsons/solidarity_tech/st_organizations.py | 6 ++-- parsons/solidarity_tech/st_pages.py | 6 ++-- parsons/solidarity_tech/st_phonebanks.py | 8 ++--- parsons/solidarity_tech/st_scheduled_calls.py | 6 ++-- parsons/solidarity_tech/st_scheduled_tasks.py | 18 +++++------ parsons/solidarity_tech/st_task_agents.py | 12 ++++---- .../solidarity_tech/st_task_assignments.py | 18 +++++------ parsons/solidarity_tech/st_team_members.py | 6 ++-- parsons/solidarity_tech/st_text_blasts.py | 6 ++-- parsons/solidarity_tech/st_text_templates.py | 18 +++++------ parsons/solidarity_tech/st_textbanks.py | 8 ++--- parsons/solidarity_tech/st_user_lists.py | 18 +++++------ parsons/solidarity_tech/st_user_notes.py | 6 ++-- .../solidarity_tech/st_user_relationships.py | 6 ++-- parsons/solidarity_tech/st_users.py | 24 ++++++++------- 23 files changed, 136 insertions(+), 133 deletions(-) diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index 6335e704e7..4247564aaf 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -73,13 +73,13 @@ def get_agent_assignments( def get_agent_assignment( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single agent assignment. Args: - id: + resource_id: ID of the agent assignment to retrieve. Raises: @@ -93,7 +93,7 @@ def get_agent_assignment( ``__ """ - res = self._get_single_resource("agent_assignments", id) + res = self._get_single_resource("agent_assignments", resource_id) expected_responses = { 200: (True, "agent assignment found"), @@ -149,7 +149,7 @@ def create_agent_assignment( def update_agent_assignment( self, - id: int, + resource_id: int, user_id: np.int64, agent_user_id: np.int64, is_active: bool | None = None, @@ -158,7 +158,7 @@ def update_agent_assignment( Update an agent assignment with specified details. Args: - id: + resource_id: Identifier for the agent assignment to update. user_id: Identifier for the user. @@ -184,7 +184,7 @@ def update_agent_assignment( res = self._put_request( "agent_assignments", - id, + resource_id, payload=payload, additional_headers={"content-type": "application/json"}, ) @@ -198,13 +198,13 @@ def update_agent_assignment( def delete_agent_assignment( self, - id: int, + resource_id: int, ) -> bool: """ Delete an agent assignment with specified ID. Args: - id: + resource_id: Identifier for the agent assignment to update. Raises: @@ -219,7 +219,7 @@ def delete_agent_assignment( ``__ """ - res = self._del_request("agent_assignments", id) + res = self._del_request("agent_assignments", resource_id) expected_responses = {404: (False, "agent assignment not found")} return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index 501330a9e3..dd6bdf613b 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -137,7 +137,7 @@ def create_custom_user_property( def delete_custom_user_property_option( self, custom_user_property_id: int, - id: str, + resource_id: str, ) -> UserPropertyData: """ Remove an option from a custom user property. @@ -145,7 +145,7 @@ def delete_custom_user_property_option( Args: custom_user_property_id: Custom user property ID - id: + resource_id: Value of the option to remove Raises: @@ -161,7 +161,7 @@ def delete_custom_user_property_option( """ res = self._del_request( "custom_user_properties", - f"{custom_user_property_id}/options/{id}", + f"{custom_user_property_id}/options/{resource_id}", additional_headers={"accept": "application/json"}, ) @@ -176,7 +176,7 @@ def delete_custom_user_property_option( def create_custom_user_property_option( self, - id: int, + resource_id: int, label: list[dict[str, str | dict[str, str]]], value: str | None = None, ) -> UserPropertyData: @@ -184,7 +184,7 @@ def create_custom_user_property_option( Create an option for a custom user property. Args: - id: + resource_id: Custom user property ID label: Multi-language labels for the option @@ -207,7 +207,7 @@ def create_custom_user_property_option( self._add_if_field_not_empty(payload, "value", value) res = self._post_request( - f"custom_user_properties/{id}/options", + f"custom_user_properties/{resource_id}/options", payload=payload, additional_headers={"accept": "application/json", "content-type": "application/json"}, ) diff --git a/parsons/solidarity_tech/st_donation_charges.py b/parsons/solidarity_tech/st_donation_charges.py index 647cba77af..f84fbe101c 100644 --- a/parsons/solidarity_tech/st_donation_charges.py +++ b/parsons/solidarity_tech/st_donation_charges.py @@ -65,13 +65,13 @@ def get_donation_charges( def get_donation_charge( self, - id: int, + resource_id: int, ) -> DonationChargeData: """ Retrieve a single donation charge. Args: - id: + resource_id: ID of the donation charge to retrieve. Raises: @@ -85,7 +85,7 @@ def get_donation_charge( ``__ """ - res = self._get_single_resource("donation_charges", id) + res = self._get_single_resource("donation_charges", resource_id) expected_responses = {404: (False, "donation charge not found")} self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_email_blasts.py b/parsons/solidarity_tech/st_email_blasts.py index 3a91cff1b9..c304ed6af4 100644 --- a/parsons/solidarity_tech/st_email_blasts.py +++ b/parsons/solidarity_tech/st_email_blasts.py @@ -58,13 +58,13 @@ def get_email_blasts( def get_email_blast( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single email blast. Args: - id: + resource_id: ID of the email blast to retrieve. Raises: @@ -78,7 +78,7 @@ def get_email_blast( ``__ """ - res = self._get_single_resource("email_blasts", id) + res = self._get_single_resource("email_blasts", resource_id) expected_responses = { 200: (True, "email blast found"), diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index f43af45efc..becb072cf5 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -124,13 +124,13 @@ def create_event_attendance( def delete_event_attendance( self, - id: str, + resource_id: str, ) -> bool: """ Delete an event attendance with the specified ID. Args: - id: + resource_id: Identifier of the event attendance to delete Raises: @@ -147,7 +147,7 @@ def delete_event_attendance( """ res = self._del_request( "event_attendances", - id, + resource_id, ) expected_responses = { diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index 5d5ba06c26..edbb2b0fc3 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -81,14 +81,15 @@ def get_event_rsvps( def get_event_rsvp( self, - id: int, + resource_id: int, + *, full_user_payload: bool = False, ) -> dict: """ Retrieve a single event rsvp. Args: - id: + resource_id: ID of the event rsvp to retrieve. full_user_payload: If True, includes complete user data in the response instead of just basic details. @@ -106,7 +107,7 @@ def get_event_rsvp( """ params: ParamsType = {"full_user_payload": full_user_payload} - res = self._get_single_resource("event_rsvps", id, params=params) + res = self._get_single_resource("event_rsvps", resource_id, params=params) expected_responses = { 200: (True, "event rsvp found"), @@ -187,7 +188,7 @@ def create_event_rsvp( def update_event_rsvp( self, - id: int, + resource_id: int, is_attending: AttendanceStatus | None = None, is_confirmed: bool | None = None, agent_user_id: np.int64 | None = None, @@ -198,7 +199,7 @@ def update_event_rsvp( Update an event rsvp with the specified details. Args: - id: + resource_id: Identifier of the event rsvp to update. is_attending: Indicates if the user is attending the event. @@ -232,7 +233,7 @@ def update_event_rsvp( res = self._put_request( "event_rsvps", - id, + resource_id, payload=payload, additional_headers={"content-type": "application/json"}, ) @@ -245,13 +246,13 @@ def update_event_rsvp( def delete_event_rsvp( self, - id: str, + resource_id: str, ) -> bool: """ Delete an event rsvp with the specified ID. Args: - id: + resource_id: Identifier of the event rsvp to delete Raises: @@ -268,7 +269,7 @@ def delete_event_rsvp( """ res = self._del_request( "event_rsvps", - id, + resource_id, ) expected_responses = {404: (False, "event rsvp not found")} diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index a50117e5e8..bce6d7aa2e 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -208,14 +208,14 @@ def create_event_sessions( def get_event_session( self, - id: int, + resource_id: int, include_hosts: bool = False, ) -> dict: """ Retrieve a single event session. Args: - id: + resource_id: ID of the event session to retrieve. include_hosts: If True, the session includes a hosts array of @@ -234,7 +234,7 @@ def get_event_session( """ params: ParamsType = {"include_hosts": include_hosts} - res = self._get_single_resource("event_sessions", id, params=params) + res = self._get_single_resource("event_sessions", resource_id, params=params) expected_responses = { 200: (True, "event session found"), @@ -246,7 +246,7 @@ def get_event_session( def update_event_session( self, - id: int, + resource_id: int, start_time: np.int64 | None = None, end_time: np.int64 | None = None, title: str | None = None, @@ -263,7 +263,7 @@ def update_event_session( Update an event session with the specified details. Args: - id: + resource_id: Identifier of the event session to update. start_time: UTC timestamp in seconds since the Unix epoch. @@ -318,7 +318,7 @@ def update_event_session( res = self._put_request( "event_sessions", - id, + resource_id, payload=payload, additional_headers={"content-type": "application/json"}, ) @@ -332,13 +332,13 @@ def update_event_session( def delete_event_session( self, - id: str, + resource_id: str, ) -> bool: """ Delete an event session with the specified ID. Args: - id: + resource_id: Identifier of the event session to delete Raises: @@ -355,7 +355,7 @@ def delete_event_session( """ res = self._del_request( "event_sessions", - id, + resource_id, ) expected_responses = { @@ -365,7 +365,7 @@ def delete_event_session( def add_event_host( self, - id: int, + resource_id: int, user_id: np.int64, ) -> bool: """ @@ -380,7 +380,7 @@ def add_event_host( {{ event-session.host }}, and {{ event-session.host-names }} merge tags. Args: - id: + resource_id: Identifier of the event session. user_id: ID of the user to add as a host. @@ -400,7 +400,7 @@ def add_event_host( payload: dict[str, Any] = {"user_id": user_id} res = self._post_request( - f"event_sessions/{id}/hosts", + f"event_sessions/{resource_id}/hosts", payload=payload, additional_headers={"content-type": "application/json"}, ) @@ -413,7 +413,7 @@ def add_event_host( def remove_event_host( self, - id: str, + resource_id: str, user_id: int, ) -> bool: """ @@ -425,7 +425,7 @@ def remove_event_host( Args: - id: + resource_id: Identifier of the event session. user_id: ID of the user to remove from hosts. @@ -444,7 +444,7 @@ def remove_event_host( """ res = self._del_request( "event_sessions", - f"{id}/hosts/{user_id}", + f"{resource_id}/hosts/{user_id}", ) expected_responses = { diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index 41de378a1b..a45464a7e8 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -181,7 +181,7 @@ def create_event( def get_event( self, - id: int, + resource_id: int, include_hosts: bool = False, ) -> dict: """ @@ -198,7 +198,7 @@ def get_event( These fields are null when no event page exists or the value is not set. Args: - id: + resource_id: ID of the event to retrieve. Raises: @@ -214,7 +214,7 @@ def get_event( """ params: ParamsType = {"include_hosts": include_hosts} - res = self._get_single_resource("event_sessions", id, params=params) + res = self._get_single_resource("event_sessions", resource_id, params=params) expected_responses = { 200: (True, "event found"), diff --git a/parsons/solidarity_tech/st_organizations.py b/parsons/solidarity_tech/st_organizations.py index a7924da3f8..9f2dd961e6 100644 --- a/parsons/solidarity_tech/st_organizations.py +++ b/parsons/solidarity_tech/st_organizations.py @@ -58,13 +58,13 @@ def get_organizations( def get_organization( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single organization. Args: - id: + resource_id: ID of the organization to retrieve. Raises: @@ -78,7 +78,7 @@ def get_organization( ``__ """ - res = self._get_single_resource("organizations", id) + res = self._get_single_resource("organizations", resource_id) expected_responses = { 200: (True, "organization found"), diff --git a/parsons/solidarity_tech/st_pages.py b/parsons/solidarity_tech/st_pages.py index 9f5fb0a071..2dc162674c 100644 --- a/parsons/solidarity_tech/st_pages.py +++ b/parsons/solidarity_tech/st_pages.py @@ -68,14 +68,14 @@ def get_pages( def get_page( self, - id: int, + resource_id: int, include_action_counts: bool = False, ) -> dict: """ Retrieve a single page. Args: - id: + resource_id: ID of the page to retrieve. include_action_counts: If True, the page includes ``action_count`` (total submissions) and ``action_goal`` @@ -99,7 +99,7 @@ def get_page( """ params: ParamsType = {"include_action_counts": include_action_counts} - res = self._get_single_resource("pages", id, params=params) + res = self._get_single_resource("pages", resource_id, params=params) expected_responses = { 200: (True, "page found"), diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py index 06c078a7e1..eb36e31992 100644 --- a/parsons/solidarity_tech/st_phonebanks.py +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -60,7 +60,7 @@ def get_phonebanks( """ if isinstance(ids, list): - ids = ",".join(str(id) for id in ids) + ids = ",".join(str(phonebank_id) for phonebank_id in ids) params: ParamsType = {"event_id": event_id, "include_stats": include_stats} self._add_if_field_not_empty(params, "ids", ids) @@ -80,13 +80,13 @@ def get_phonebanks( def get_phonebank( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single phonebank. Args: - id: + resource_id: ID of the phonebank to retrieve. Raises: @@ -100,7 +100,7 @@ def get_phonebank( ``__ """ - res = self._get_single_resource("phonebanks", id) + res = self._get_single_resource("phonebanks", resource_id) expected_responses = { 200: (True, "phonebank found"), diff --git a/parsons/solidarity_tech/st_scheduled_calls.py b/parsons/solidarity_tech/st_scheduled_calls.py index a1ac01e88b..3031240cba 100644 --- a/parsons/solidarity_tech/st_scheduled_calls.py +++ b/parsons/solidarity_tech/st_scheduled_calls.py @@ -71,13 +71,13 @@ def get_scheduled_calls( def get_scheduled_call( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single scheduled call. Args: - id: + resource_id: ID of the scheduled call to retrieve. Raises: @@ -91,7 +91,7 @@ def get_scheduled_call( ``__ """ - res = self._get_single_resource("scheduled_calls", id) + res = self._get_single_resource("scheduled_calls", resource_id) expected_responses = { 200: (True, "scheduled call found"), diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py index be11a5193a..9342ffd583 100644 --- a/parsons/solidarity_tech/st_scheduled_tasks.py +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -72,13 +72,13 @@ def get_scheduled_tasks( def get_scheduled_task( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single scheduled task. Args: - id: + resource_id: ID of the scheduled task to retrieve. Raises: @@ -92,7 +92,7 @@ def get_scheduled_task( ``__ """ - res = self._get_single_resource("scheduled_tasks", id) + res = self._get_single_resource("scheduled_tasks", resource_id) expected_responses = { 200: (True, "scheduled task found"), @@ -170,7 +170,7 @@ def create_scheduled_task( def update_scheduled_task( self, - id: int, + resource_id: int, due_at: str | int | float | datetime | None = None, remind_at: str | int | float | datetime | None = None, agent_user_id: np.int64 | None = None, @@ -182,7 +182,7 @@ def update_scheduled_task( Update a scheduled task with specified details. Args: - id: + resource_id: Identifier for the scheduled task to update. due_at: The date and time when the task is due. @@ -226,7 +226,7 @@ def update_scheduled_task( res = self._put_request( "scheduled_tasks", - id, + resource_id, payload=payload, additional_headers={"content-type": "application/json"}, ) @@ -240,13 +240,13 @@ def update_scheduled_task( def delete_scheduled_task( self, - id: int, + resource_id: int, ) -> bool: """ Delete a scheduled task with the specified ID. Args: - id: + resource_id: Identifier for the scheduled task to delete. Raises: @@ -261,7 +261,7 @@ def delete_scheduled_task( ``__ """ - res = self._del_request("scheduled_tasks", id) + res = self._del_request("scheduled_tasks", resource_id) expected_responses = {404: (False, "scheduled task not found")} return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_task_agents.py b/parsons/solidarity_tech/st_task_agents.py index 8e932812cd..b64f120e65 100644 --- a/parsons/solidarity_tech/st_task_agents.py +++ b/parsons/solidarity_tech/st_task_agents.py @@ -69,13 +69,13 @@ def get_task_agents( def get_task_agent( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single task agent. Args: - id: + resource_id: ID of the task agent to retrieve. Raises: @@ -89,7 +89,7 @@ def get_task_agent( ``__ """ - res = self._get_single_resource("task_agents", id) + res = self._get_single_resource("task_agents", resource_id) expected_responses = { 200: (True, "task agent found"), @@ -136,13 +136,13 @@ def create_task_agent( def delete_task_agent( self, - id: int, + resource_id: int, ) -> bool: """ Delete a task agent with the specified ID. Args: - id: + resource_id: Identifier for the task agent to delete. Raises: @@ -157,7 +157,7 @@ def delete_task_agent( ``__ """ - res = self._del_request("task_agents", id) + res = self._del_request("task_agents", resource_id) expected_responses = {404: (False, "task agent not found")} return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_task_assignments.py b/parsons/solidarity_tech/st_task_assignments.py index a3a00b8e63..bc158b5753 100644 --- a/parsons/solidarity_tech/st_task_assignments.py +++ b/parsons/solidarity_tech/st_task_assignments.py @@ -71,13 +71,13 @@ def get_task_assignments( def get_task_assignment( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single task assignment. Args: - id: + resource_id: ID of the task assignment to retrieve. Raises: @@ -91,7 +91,7 @@ def get_task_assignment( ``__ """ - res = self._get_single_resource("task_assignments", id) + res = self._get_single_resource("task_assignments", resource_id) expected_responses = { 200: (True, "task assignment found"), @@ -149,14 +149,14 @@ def create_task_assignment( def update_task_assignment( self, - id: int, + resource_id: int, agent_user_id: np.int64 | None = None, ) -> bool: """ Update an task assignment with the specified details. Args: - id: + resource_id: Identifier of the task assignment to update. agent_user_id: Identifier for the agent user, if applicable. @@ -178,7 +178,7 @@ def update_task_assignment( res = self._put_request( "scheduled_tasks", - id, + resource_id, payload=payload, additional_headers={"content-type": "application/json"}, ) @@ -191,13 +191,13 @@ def update_task_assignment( def delete_task_assignment( self, - id: int, + resource_id: int, ) -> bool: """ Delete a task assignment with the specified ID. Args: - id: + resource_id: Identifier of the task assignment to delete. Raises: @@ -212,7 +212,7 @@ def delete_task_assignment( ``__ """ - res = self._del_request("task_assignments", id) + res = self._del_request("task_assignments", resource_id) expected_responses = {404: (False, "task assignment not found")} return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_team_members.py b/parsons/solidarity_tech/st_team_members.py index 1f355e0cfd..b4fd918316 100644 --- a/parsons/solidarity_tech/st_team_members.py +++ b/parsons/solidarity_tech/st_team_members.py @@ -142,7 +142,7 @@ def create_team_member( def update_team_member( self, - id: int, + resource_id: int, role_id: int, scope_type: ScopeType, scope_id: int, @@ -151,7 +151,7 @@ def update_team_member( Update a team member with the specified details. Args: - id: + resource_id: Team member ID (UserRoleScope ID). role_id: ID of the role to assign. @@ -179,7 +179,7 @@ def update_team_member( res = self._put_request( "team_members", - id, + resource_id, payload=payload, additional_headers={"content-type": "application/json"}, ) diff --git a/parsons/solidarity_tech/st_text_blasts.py b/parsons/solidarity_tech/st_text_blasts.py index 9bd13ce2c0..6c945db0c5 100644 --- a/parsons/solidarity_tech/st_text_blasts.py +++ b/parsons/solidarity_tech/st_text_blasts.py @@ -58,13 +58,13 @@ def get_text_blasts( def get_text_blast( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single text blast. Args: - id: + resource_id: ID of the text blast to retrieve. Raises: @@ -78,7 +78,7 @@ def get_text_blast( ``__ """ - res = self._get_single_resource("text_blasts", id) + res = self._get_single_resource("text_blasts", resource_id) expected_responses = { 200: (True, "text blast found"), diff --git a/parsons/solidarity_tech/st_text_templates.py b/parsons/solidarity_tech/st_text_templates.py index c71472aa39..ca949616ee 100644 --- a/parsons/solidarity_tech/st_text_templates.py +++ b/parsons/solidarity_tech/st_text_templates.py @@ -69,13 +69,13 @@ def get_text_templates( def get_text_template( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single text template. Args: - id: + resource_id: ID of the text template to retrieve. Raises: @@ -89,7 +89,7 @@ def get_text_template( ``__ """ - res = self._get_single_resource("text_templates", id) + res = self._get_single_resource("text_templates", resource_id) expected_responses = { 200: (True, "text template found"), @@ -158,7 +158,7 @@ def create_text_template( def update_text_template( self, - id: int, + resource_id: int, name: str | None = None, scope_id: np.int64 | None = None, scope_type: ScopeType | None = None, @@ -169,7 +169,7 @@ def update_text_template( Update an text template with the specified details. Args: - id: + resource_id: Identifier of the text template to update. name: Name of the entity. @@ -205,7 +205,7 @@ def update_text_template( res = self._put_request( "text_templates", - id, + resource_id, payload=payload, additional_headers={"content-type": "application/json"}, ) @@ -218,13 +218,13 @@ def update_text_template( def delete_text_template( self, - id: int, + resource_id: int, ) -> bool: """ Delete an text template with the specified ID. Args: - id: + resource_id: Identifier of the text template to delete. Raises: @@ -239,7 +239,7 @@ def delete_text_template( ``__ """ - res = self._del_request("text_templates", id) + res = self._del_request("text_templates", resource_id) expected_responses = {404: (False, "text template not found")} return self._handle_status_codes(res=res, codes=expected_responses) diff --git a/parsons/solidarity_tech/st_textbanks.py b/parsons/solidarity_tech/st_textbanks.py index 845345ee09..a8901c75e0 100644 --- a/parsons/solidarity_tech/st_textbanks.py +++ b/parsons/solidarity_tech/st_textbanks.py @@ -60,7 +60,7 @@ def get_textbanks( """ if isinstance(ids, list): - ids = ",".join(str(id) for id in ids) + ids = ",".join(str(textbank_id) for textbank_id in ids) params: ParamsType = {"event_id": event_id, "include_stats": include_stats} self._add_if_field_not_empty(params, "ids", ids) @@ -80,13 +80,13 @@ def get_textbanks( def get_textbank( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single textbank. Args: - id: + resource_id: ID of the textbank to retrieve. Raises: @@ -100,7 +100,7 @@ def get_textbank( ``__ """ - res = self._get_single_resource("textbanks", id) + res = self._get_single_resource("textbanks", resource_id) expected_responses = { 200: (True, "textbank found"), diff --git a/parsons/solidarity_tech/st_user_lists.py b/parsons/solidarity_tech/st_user_lists.py index e53e02895b..b9b0192443 100644 --- a/parsons/solidarity_tech/st_user_lists.py +++ b/parsons/solidarity_tech/st_user_lists.py @@ -68,13 +68,13 @@ def get_user_lists( def get_user_list( self, - id: int, + resource_id: int, ) -> dict: """ Retrieve a single user list. Args: - id: + resource_id: ID of the user list to retrieve. Raises: @@ -88,7 +88,7 @@ def get_user_list( ``__ """ - res = self._get_single_resource("user_lists", id) + res = self._get_single_resource("user_lists", resource_id) expected_responses = { 200: (True, "user list found"), @@ -160,7 +160,7 @@ def create_user_list( def update_user_list( self, - id: int, + resource_id: int, name: str | None = None, scope_id: np.int64 | None = None, scope_type: str | None = None, @@ -174,7 +174,7 @@ def update_user_list( For documentation, see ``__. Args: - id: + resource_id: Identifier of the user list to update. name: Name of the user list. @@ -208,7 +208,7 @@ def update_user_list( res = self._put_request( "user_lists", - id, + resource_id, payload=payload, additional_headers={"content-type": "application/json"}, ) @@ -221,13 +221,13 @@ def update_user_list( def delete_user_list( self, - id: str, + resource_id: str, ) -> bool: """ Delete a user list with the specified ID. Args: - id: + resource_id: Identifier of the user list to delete Raises: @@ -244,7 +244,7 @@ def delete_user_list( """ res = self._del_request( "user_lists", - id, + resource_id, ) expected_responses = { diff --git a/parsons/solidarity_tech/st_user_notes.py b/parsons/solidarity_tech/st_user_notes.py index 796a87d7aa..310d17d304 100644 --- a/parsons/solidarity_tech/st_user_notes.py +++ b/parsons/solidarity_tech/st_user_notes.py @@ -75,7 +75,7 @@ def create_user_note( def delete_user_note( self, - id: str, + resource_id: str, user_id: int, agent_id: int | None = None, ) -> bool: @@ -83,7 +83,7 @@ def delete_user_note( Delete a user note with the specified ID. Args: - id: + resource_id: Identifier of the user note to delete user_id: Identifier for the user the note refers to. @@ -106,7 +106,7 @@ def delete_user_note( params: ParamsType = {"user_id": user_id} self._add_if_field_not_empty(params, "agent_id", agent_id) - res = self._del_request("user_notes", id, params=params) + res = self._del_request("user_notes", resource_id, params=params) expected_responses = { 200: (True, "user note deleted"), diff --git a/parsons/solidarity_tech/st_user_relationships.py b/parsons/solidarity_tech/st_user_relationships.py index 274546205b..2d63c134e6 100644 --- a/parsons/solidarity_tech/st_user_relationships.py +++ b/parsons/solidarity_tech/st_user_relationships.py @@ -105,14 +105,14 @@ def create_user_relationship( def delete_user_relationship( self, - id: int, + resource_id: int, user_id: int, ) -> bool: """ Delete a user relationship. Args: - id: + resource_id: Identifier of the user relationship to delete. user_id: Identifier for the user. @@ -133,7 +133,7 @@ def delete_user_relationship( res = self._del_request( "user_relationships", - id, + resource_id, params=params, ) diff --git a/parsons/solidarity_tech/st_users.py b/parsons/solidarity_tech/st_users.py index 63dd4c7c8b..be34a9ed5e 100644 --- a/parsons/solidarity_tech/st_users.py +++ b/parsons/solidarity_tech/st_users.py @@ -68,7 +68,7 @@ def get_users( """ if isinstance(user_list_ids, list): - user_list_ids = ",".join(str(id) for id in user_list_ids) + user_list_ids = ",".join(str(userlist_id) for userlist_id in user_list_ids) params: ParamsType = {} self._add_if_field_not_empty(params, "user_list_ids", user_list_ids) @@ -97,13 +97,13 @@ def get_users( def get_user( self, - id: int, + resource_id: int, ) -> UserData: """ Retrieve a single user. Args: - id: + resource_id: ID of the user to retrieve. Raises: @@ -118,7 +118,7 @@ def get_user( """ res = self._get_single_resource( - "users", id, additional_headers={"accept": "application/json"} + "users", resource_id, additional_headers={"accept": "application/json"} ) expected_responses = { @@ -290,7 +290,7 @@ def create_user( def update_user( self, - id: int, + resource_id: int, phone_number: str | None = None, clear_phone_number: bool | None = None, email: str | None = None, @@ -319,7 +319,7 @@ def update_user( Update a user with the specified details. Args: - id: + resource_id: Identifier of the user to update. phone_number: Phone number of the user. @@ -431,7 +431,7 @@ def update_user( res = self._put_request( "users", - id, + resource_id, payload=payload, additional_headers={"content-type": "application/json"}, ) @@ -471,7 +471,7 @@ def merge_duplicate_users( """ if isinstance(user_ids, list): - user_ids = ",".join(str(id) for id in user_ids) + user_ids = ",".join(str(user_id) for user_id in user_ids) payload: dict[str, Any] = { "primary_user_id": primary_user_id, @@ -495,13 +495,13 @@ def merge_duplicate_users( def delete_user( self, - id: str, + resource_id: str, ) -> UserDeleteMetadata: """ Delete a user with the specified ID. Args: - id: + resource_id: Identifier of the user to delete Raises: @@ -515,7 +515,9 @@ def delete_user( ``__ """ - res = self._del_request("users", id, additional_headers={"accept": "application/json"}) + res = self._del_request( + "users", resource_id, additional_headers={"accept": "application/json"} + ) expected_responses = { 200: (True, "user deleted"), From 8c806e5d7ce8e32843fd99696b9e5a639a8e26a7 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 26 Aug 2026 21:38:47 -0400 Subject: [PATCH 92/94] boolean keyword args --- parsons/solidarity_tech/st_activities.py | 7 +- .../solidarity_tech/st_agent_assignments.py | 2 + parsons/solidarity_tech/st_emails.py | 1 + .../solidarity_tech/st_event_attendances.py | 1 + parsons/solidarity_tech/st_event_rsvps.py | 15 ++-- parsons/solidarity_tech/st_event_sessions.py | 36 +++++---- parsons/solidarity_tech/st_events.py | 2 + parsons/solidarity_tech/st_pages.py | 2 + parsons/solidarity_tech/st_phonebanks.py | 1 + parsons/solidarity_tech/st_scheduled_tasks.py | 2 + parsons/solidarity_tech/st_textbanks.py | 1 + parsons/solidarity_tech/st_texts.py | 1 + parsons/solidarity_tech/st_user_notes.py | 7 +- parsons/solidarity_tech/st_users.py | 80 ++++++++++--------- 14 files changed, 91 insertions(+), 67 deletions(-) diff --git a/parsons/solidarity_tech/st_activities.py b/parsons/solidarity_tech/st_activities.py index 141aae8044..c8092b9db2 100644 --- a/parsons/solidarity_tech/st_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -26,8 +26,9 @@ def get_activities( limit: int = 20, cursor: int | None = None, since: int | datetime = 0, - include_count: bool = False, user_id: int | None = None, + *, + include_count: bool = False, ) -> tuple[Table, ActivityMetadata]: """ Retrieve a list of activities. @@ -43,12 +44,12 @@ def get_activities( Records are returned newest first (descending id). since: UTC timestamp in seconds since the Unix epoch to filter calls created after this time. + user_id: + User ID to filter activities for a specific user. include_count: When true, meta.total_count is populated with the full result count. Off by default because counting an entire history is expensive. Omit it for normal paging. - user_id: - User ID to filter activities for a specific user. Raises: :class:`STFailedResponseError`: If the operation fails with a known error code. diff --git a/parsons/solidarity_tech/st_agent_assignments.py b/parsons/solidarity_tech/st_agent_assignments.py index 4247564aaf..111f60ddeb 100644 --- a/parsons/solidarity_tech/st_agent_assignments.py +++ b/parsons/solidarity_tech/st_agent_assignments.py @@ -107,6 +107,7 @@ def create_agent_assignment( self, user_id: np.int64, agent_user_id: np.int64, + *, is_active: bool | None = None, ) -> bool: """ @@ -152,6 +153,7 @@ def update_agent_assignment( resource_id: int, user_id: np.int64, agent_user_id: np.int64, + *, is_active: bool | None = None, ) -> bool: """ diff --git a/parsons/solidarity_tech/st_emails.py b/parsons/solidarity_tech/st_emails.py index 90f150555c..0232b0690d 100644 --- a/parsons/solidarity_tech/st_emails.py +++ b/parsons/solidarity_tech/st_emails.py @@ -23,6 +23,7 @@ def send_one_off_email( email_sender_id: int | None = None, reply_to: str | None = None, attachment_urls: list[str] | None = None, + *, track_opens: bool = True, track_clicks: bool = True, ) -> bool: diff --git a/parsons/solidarity_tech/st_event_attendances.py b/parsons/solidarity_tech/st_event_attendances.py index becb072cf5..f93e1bef30 100644 --- a/parsons/solidarity_tech/st_event_attendances.py +++ b/parsons/solidarity_tech/st_event_attendances.py @@ -76,6 +76,7 @@ def create_event_attendance( event_id: np.int64, event_session_id: np.int64, user_id: np.int64, + *, attended: bool, ) -> bool: """ diff --git a/parsons/solidarity_tech/st_event_rsvps.py b/parsons/solidarity_tech/st_event_rsvps.py index edbb2b0fc3..cbe6d6eedf 100644 --- a/parsons/solidarity_tech/st_event_rsvps.py +++ b/parsons/solidarity_tech/st_event_rsvps.py @@ -28,6 +28,7 @@ def get_event_rsvps( event_id: int | None = None, session_id: int | None = None, user_id: int | None = None, + *, full_user_payload: bool = False, ) -> Table: """ @@ -124,9 +125,10 @@ def create_event_rsvp( is_attending: AttendanceStatus, agent_user_id: np.int64 | None, user_id: np.int64 | None = None, - is_confirmed: bool | None = None, source: str | None = None, source_system: str | None = None, + *, + is_confirmed: bool | None = None, skip_email_confirmation: bool = False, ) -> bool: """ @@ -143,12 +145,12 @@ def create_event_rsvp( Identifier for the agent user, if applicable. user_id: Identifier for the user RSVPing to the event. - is_confirmed: - Indicates if the RSVP is confirmed. source: Source of the RSVP. source_system: System from which the RSVP originated. + is_confirmed: + Indicates if the RSVP is confirmed. skip_email_confirmation: If True, skips sending the initial email confirmation to the user. @@ -190,10 +192,11 @@ def update_event_rsvp( self, resource_id: int, is_attending: AttendanceStatus | None = None, - is_confirmed: bool | None = None, agent_user_id: np.int64 | None = None, source: str | None = None, source_system: str | None = None, + *, + is_confirmed: bool | None = None, ) -> bool: """ Update an event rsvp with the specified details. @@ -203,14 +206,14 @@ def update_event_rsvp( Identifier of the event rsvp to update. is_attending: Indicates if the user is attending the event. - is_confirmed: - Indicates if the RSVP is confirmed. agent_user_id: Identifier for the agent user, if applicable. source: Source of the RSVP. source_system: System from which the RSVP originated. + is_confirmed: + Indicates if the RSVP is confirmed. Raises: :class:`STFailedResponseError`: If the operation fails with a known error code. diff --git a/parsons/solidarity_tech/st_event_sessions.py b/parsons/solidarity_tech/st_event_sessions.py index bce6d7aa2e..0b5dbcd157 100644 --- a/parsons/solidarity_tech/st_event_sessions.py +++ b/parsons/solidarity_tech/st_event_sessions.py @@ -26,11 +26,12 @@ def get_event_sessions( offset: int = 0, since: int | datetime = 0, event_id: int = 0, - upcoming: bool | None = None, starts_after: int | datetime | None = None, starts_before: int | datetime | None = None, chapter_id: int | None = None, event_tags: list[str] | str | None = None, + *, + upcoming: bool | None = None, include_rsvp_counts: bool | None = None, include_confirmed_counts: bool | None = None, include_hosts: bool | None = None, @@ -49,9 +50,6 @@ def get_event_sessions( UTC timestamp in seconds since the Unix epoch to filter calls created after this time. event_id: Filters sessions by event_id within the accessible scope. - upcoming: - If True, returns only sessions that have not ended yet, - sorted by start_time ascending (soonest first). starts_after: UTC timestamp in seconds since the Unix epoch; only sessions with start_time at or after this moment. @@ -64,6 +62,9 @@ def get_event_sessions( event_tags: Comma-separated list of tags. Matches sessions whose own tags OR whose parent event tags overlap with the list. + upcoming: + If True, returns only sessions that have not ended yet, + sorted by start_time ascending (soonest first). include_rsvp_counts: If True, each session in the response includes an rsvp_counts object keyed by RSVP status. (e.g. {"yes": 12, "no": 3}) @@ -127,11 +128,12 @@ def create_event_sessions( location_name: str | None = None, location_data: dict[str, str] | None = None, location_address: str | None = None, - show_rsvp_bar: bool | None = None, - show_title_in_form: bool | None = None, note: str | None = None, max_capacity: int | None = None, tags: list[str] | None = None, + *, + show_rsvp_bar: bool | None = None, + show_title_in_form: bool | None = None, ) -> bool: """ Create an event rsvp with the specified details. @@ -157,16 +159,16 @@ def create_event_sessions( NYC borough is the entry whose types include "sublocality_level_1". location_address: Physical address of the event location. - show_rsvp_bar: - Flag to show RSVP buttons bar. - show_title_in_form: - Flag to show title in the form. note: Additional notes for the event session. max_capacity: Maximum capacity for the event session. tags: Array of tags for the event session. + show_rsvp_bar: + Flag to show RSVP buttons bar. + show_title_in_form: + Flag to show title in the form. Raises: :class:`STFailedResponseError`: If the operation fails with a known error code. @@ -209,6 +211,7 @@ def create_event_sessions( def get_event_session( self, resource_id: int, + *, include_hosts: bool = False, ) -> dict: """ @@ -253,11 +256,12 @@ def update_event_session( location_name: str | None = None, location_address: str | None = None, location_data: dict[str, str] | None = None, - show_rsvp_bar: bool | None = None, - show_title_in_form: bool | None = None, note: str | None = None, max_capacity: int | None = None, tags: list[str] | None = None, + *, + show_rsvp_bar: bool | None = None, + show_title_in_form: bool | None = None, ) -> bool: """ Update an event session with the specified details. @@ -280,16 +284,16 @@ def update_event_session( ``components``/``coordinates`` accept native JSON or JSON strings and are stored/returned as JSON strings. Omit to leave the existing location_data unchanged. - show_rsvp_bar: - Flag to show RSVP buttons bar. - show_title_in_form: - Flag to show title in the form. note: Additional notes for the event session. max_capacity: Maximum capacity of the event session. tags: List of tags for the event session. + show_rsvp_bar: + Flag to show RSVP buttons bar. + show_title_in_form: + Flag to show title in the form. Raises: :class:`STFailedResponseError`: If the operation fails with a known error code. diff --git a/parsons/solidarity_tech/st_events.py b/parsons/solidarity_tech/st_events.py index a45464a7e8..af3330a3a8 100644 --- a/parsons/solidarity_tech/st_events.py +++ b/parsons/solidarity_tech/st_events.py @@ -97,6 +97,7 @@ def create_event( max_capacity: int | None = None, latitude: float | None = None, longitude: float | None = None, + *, skip_duplicate_check: bool = False, ) -> bool: """ @@ -182,6 +183,7 @@ def create_event( def get_event( self, resource_id: int, + *, include_hosts: bool = False, ) -> dict: """ diff --git a/parsons/solidarity_tech/st_pages.py b/parsons/solidarity_tech/st_pages.py index 2dc162674c..c0fa6ca2fd 100644 --- a/parsons/solidarity_tech/st_pages.py +++ b/parsons/solidarity_tech/st_pages.py @@ -22,6 +22,7 @@ def get_pages( limit: int = 20, offset: int = 0, since: int | datetime = 0, + *, include_action_counts: bool = False, ) -> Table: """ @@ -69,6 +70,7 @@ def get_pages( def get_page( self, resource_id: int, + *, include_action_counts: bool = False, ) -> dict: """ diff --git a/parsons/solidarity_tech/st_phonebanks.py b/parsons/solidarity_tech/st_phonebanks.py index eb36e31992..dab6ae6e36 100644 --- a/parsons/solidarity_tech/st_phonebanks.py +++ b/parsons/solidarity_tech/st_phonebanks.py @@ -24,6 +24,7 @@ def get_phonebanks( since: int | datetime = 0, event_id: int = 0, ids: list[int] | str | None = None, + *, include_stats: bool = False, ) -> Table: """ diff --git a/parsons/solidarity_tech/st_scheduled_tasks.py b/parsons/solidarity_tech/st_scheduled_tasks.py index 9342ffd583..01f67b07ab 100644 --- a/parsons/solidarity_tech/st_scheduled_tasks.py +++ b/parsons/solidarity_tech/st_scheduled_tasks.py @@ -109,6 +109,7 @@ def create_scheduled_task( agent_user_id: np.int64 | None = None, user_id: np.int64 | None = None, notes: str | None = None, + *, marked_as_completed: bool | None = None, ) -> bool: """ @@ -176,6 +177,7 @@ def update_scheduled_task( agent_user_id: np.int64 | None = None, user_id: np.int64 | None = None, notes: str | None = None, + *, marked_as_completed: bool | None = None, ) -> bool: """ diff --git a/parsons/solidarity_tech/st_textbanks.py b/parsons/solidarity_tech/st_textbanks.py index a8901c75e0..24b2255ea0 100644 --- a/parsons/solidarity_tech/st_textbanks.py +++ b/parsons/solidarity_tech/st_textbanks.py @@ -24,6 +24,7 @@ def get_textbanks( since: int | datetime = 0, event_id: int = 0, ids: list[int] | str | None = None, + *, include_stats: bool = False, ) -> Table: """ diff --git a/parsons/solidarity_tech/st_texts.py b/parsons/solidarity_tech/st_texts.py index 315ed2e2b2..16c591c684 100644 --- a/parsons/solidarity_tech/st_texts.py +++ b/parsons/solidarity_tech/st_texts.py @@ -71,6 +71,7 @@ def send_text( user_id: int, body: str, media_urls: list[str] | None = None, + *, attach_contact_card: bool | None = None, shorten_urls: bool | None = None, ) -> bool: diff --git a/parsons/solidarity_tech/st_user_notes.py b/parsons/solidarity_tech/st_user_notes.py index 310d17d304..b65187a8e1 100644 --- a/parsons/solidarity_tech/st_user_notes.py +++ b/parsons/solidarity_tech/st_user_notes.py @@ -21,8 +21,9 @@ def create_user_note( content: str, agent_id: int | None = None, created_at: int | None = None, - restricted: bool = False, interaction_method: InteractionType | None = None, + *, + restricted: bool = False, ) -> bool: """ Create a user note with the specified details. @@ -37,11 +38,11 @@ def create_user_note( Content of the user note. created_at: Timestamp for when the note was created. + interaction_method: + Interaction type that produced the note. restricted: If True, the note is only visible to team members with the View Restricted Properties permission. - interaction_method: - Interaction type that produced the note. Raises: :class:`STFailedResponseError`: If the operation fails with a known error code. diff --git a/parsons/solidarity_tech/st_users.py b/parsons/solidarity_tech/st_users.py index be34a9ed5e..5daf54e7c2 100644 --- a/parsons/solidarity_tech/st_users.py +++ b/parsons/solidarity_tech/st_users.py @@ -142,19 +142,20 @@ def create_user( chapter_ids: list[int] | None = None, referred_by_user_id: int | None = None, custom_user_properties: dict[str, str | list[str]] | None = None, - append_custom_user_properties: bool = True, add_tags: list[str] | None = None, remove_tags: list[str] | None = None, donation_charge: dict[str, numbers.Number | str] | None = None, address: dict[str, str | float] | None = None, assessment: str | None = None, + timezone: ZoneInfo | str | None = None, + lookup_key: str | None = None, + *, + append_custom_user_properties: bool = True, sms_permission: bool | None = None, call_permission: bool | None = None, email_permission: bool | None = None, - timezone: ZoneInfo | str | None = None, require_contact_info: bool = True, phone_number_textable_validation: bool = True, - lookup_key: str | None = None, ) -> bool: """ Create or update a user with the specified details. @@ -194,11 +195,6 @@ def create_user( (e.g. ``"Option A, Option B"``). For Multiple Checkboxes, see ``append_custom_user_properties`` to control whether values are merged with or replace existing values. - append_custom_user_properties: - Controls how Multiple Checkboxes custom properties are written. - Defaults to True (union new values with existing values, the long-standing API behavior). - Set to False to overwrite existing values, mirroring bulk update REPLACE mode. - Has no effect on non-array field types. add_tags: List of tags to add to the user. remove_tags: @@ -210,24 +206,29 @@ def create_user( We will attempt to geocode the address if ``latitude`` and ``longitude`` are not provided. assessment: Assessment status key to set on the user (maps to classification). + timezone: + IANA timezone identifier (e.g., "America/New_York", "Europe/London"). + lookup_key: + Custom property key (internal_name) to use for user lookup/deduplication. + Value is read from ``custom_user_properties[lookup_key]``. + Allows matching existing users by external IDs stored in custom properties. + append_custom_user_properties: + Controls how Multiple Checkboxes custom properties are written. + Defaults to True (union new values with existing values, the long-standing API behavior). + Set to False to overwrite existing values, mirroring bulk update REPLACE mode. + Has no effect on non-array field types. sms_permission: SMS permission status. call_permission: Call permission status. email_permission: Email permission status. - timezone: - IANA timezone identifier (e.g., "America/New_York", "Europe/London"). require_contact_info: Whether to require phone_number or email for user creation. Defaults to True. phone_number_textable_validation: Whether to validate that phone number is textable. Defaults to True. - lookup_key: - Custom property key (internal_name) to use for user lookup/deduplication. - Value is read from ``custom_user_properties[lookup_key]``. - Allows matching existing users by external IDs stored in custom properties. Raises: :class:`ValueError`: If neither ``phone_number`` nor ``email`` is provided. @@ -292,7 +293,6 @@ def update_user( self, resource_id: int, phone_number: str | None = None, - clear_phone_number: bool | None = None, email: str | None = None, first_name: str | None = None, last_name: str | None = None, @@ -302,18 +302,20 @@ def update_user( chapter_ids: list[int] | None = None, add_chapter_ids: list[int] | None = None, remove_chapter_ids: list[int] | None = None, - set_exclusive_chapter: bool | None = None, second_language: str | None = None, referred_by_user_id: int | None = None, custom_user_properties: dict[str, str | list[str]] | None = None, - append_custom_user_properties: bool = True, address: dict[str, str | float] | None = None, assessment: str | None = None, + timezone: ZoneInfo | str | None = None, + donation_charge: dict[str, numbers.Number | str] | None = None, + *, + clear_phone_number: bool | None = None, + set_exclusive_chapter: bool | None = None, + append_custom_user_properties: bool = True, sms_permission: bool | None = None, call_permission: bool | None = None, email_permission: bool | None = None, - timezone: ZoneInfo | str | None = None, - donation_charge: dict[str, numbers.Number | str] | None = None, ) -> bool: """ Update a user with the specified details. @@ -323,13 +325,6 @@ def update_user( Identifier of the user to update. phone_number: Phone number of the user. - clear_phone_number: - If True, clears the user's primary phone number - (and removes it from ``other_phone_numbers``). - Blank ``phone_number`` values are always ignored, so this - explicit flag is the only way to clear a phone number via the API. - Cannot be combined with a non-blank ``phone_number`` - in the same request (returns 422). email: Email of the user. first_name: @@ -349,10 +344,6 @@ def update_user( Array of chapter IDs to add. Requires multi-chapter feature. remove_chapter_ids: Array of chapter IDs to remove. Requires multi-chapter feature. - set_exclusive_chapter: - When True with ``chapter_id``, - sets that chapter as the only chapter - (removes all other chapter memberships). second_language: Second language of the user. referred_by_user_id: @@ -366,27 +357,38 @@ def update_user( (e.g. ``"Option A, Option B"``). For Multiple Checkboxes, see ``append_custom_user_properties`` to control whether values are merged with or replace existing values. - append_custom_user_properties: - Controls how Multiple Checkboxes custom properties are written. - Defaults to True (union new values with existing values, the long-standing API behavior). - Set to False to overwrite existing values, mirroring bulk update REPLACE mode. - Has no effect on non-array field types. address: Optional address to update. We will attempt to geocode the address if latitude and longitude are not provided. assessment: Assessment status key to set on the user (maps to classification). + timezone: + IANA timezone identifier (e.g., "America/New_York", "Europe/London"). + donation_charge: + Optional external donation charge to create. + clear_phone_number: + If True, clears the user's primary phone number + (and removes it from ``other_phone_numbers``). + Blank ``phone_number`` values are always ignored, so this + explicit flag is the only way to clear a phone number via the API. + Cannot be combined with a non-blank ``phone_number`` + in the same request (returns 422). + set_exclusive_chapter: + When True with ``chapter_id``, + sets that chapter as the only chapter + (removes all other chapter memberships). + append_custom_user_properties: + Controls how Multiple Checkboxes custom properties are written. + Defaults to True (union new values with existing values, the long-standing API behavior). + Set to False to overwrite existing values, mirroring bulk update REPLACE mode. + Has no effect on non-array field types. sms_permission: If True, the user has permission to receive SMS messages. call_permission: If True, the user has permission to receive call messages. email_permission: If True, the user has permission to receive email messages. - timezone: - IANA timezone identifier (e.g., "America/New_York", "Europe/London"). - donation_charge: - Optional external donation charge to create. Raises: :class:`STFailedResponseError`: If the operation fails with a known error code. From 9358f6707064d841ca2cd56121fa382ecc5b6424 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Wed, 26 Aug 2026 21:46:26 -0400 Subject: [PATCH 93/94] Update ratelimited API connector from #1908 --- parsons/utilities/api_connector.py | 40 +++++++++++++++++++----------- pyproject.toml | 1 - 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/parsons/utilities/api_connector.py b/parsons/utilities/api_connector.py index cf17c18c41..471c0591c4 100644 --- a/parsons/utilities/api_connector.py +++ b/parsons/utilities/api_connector.py @@ -95,7 +95,8 @@ def __init__( self.data_key = data_key if session and ratelimiter: - raise ValueError("session and ratelimiter cannot both be provided") + err_msg = "session and ratelimiter cannot both be provided" + raise ValueError(err_msg) if session: self.session = session @@ -108,7 +109,7 @@ def __init__( self.session.auth = auth if headers: - self.session.headers = headers # ignore: type[ty:invalid-assignment] # pyright: ignore [reportAttributeAccessIssue] + self.session.headers = headers # type: ignore[ty:invalid-assignment] # pyright: ignore [reportAttributeAccessIssue] @property def auth(self) -> _AuthType: @@ -128,7 +129,7 @@ def headers(self) -> _HeadersType: @headers.setter def headers(self, inp: _HeadersType) -> None: - self.session.headers = inp # ignore: type[ty:invalid-assignment] # pyright: ignore [reportAttributeAccessIssue] + self.session.headers = inp # type: ignore[ty:invalid-assignment] # pyright: ignore [reportAttributeAccessIssue] @headers.deleter def headers(self) -> None: @@ -147,7 +148,7 @@ def request( **kwargs, ) -> requests.Response: """ - Base request using requests libary. + Make a request using requests libary. Args: url: @@ -208,22 +209,22 @@ def request( @overload def get_request( self, - url: ..., + url: str, *, - params: ... = ..., + params: _ParamsType | None = None, return_format: Literal["json"] = "json", - raise_on_error: ... = ..., + raise_on_error: bool = True, **kwargs, ) -> _JsonType: ... @overload def get_request( self, - url: ..., + url: str, *, - params: ... = ..., + params: _ParamsType | None = None, return_format: Literal["content"], - raise_on_error: ... = ..., + raise_on_error: bool = True, **kwargs, ) -> bytes: ... @@ -267,7 +268,8 @@ def get_request( if return_format == "content": return r.content - raise RuntimeError(f"{return_format} is not a valid format, change to json or content") + err_msg = f"{return_format} is not a valid format, change to json or content" + raise RuntimeError(err_msg) def post_request( self, @@ -327,6 +329,8 @@ def post_request( return r.status_code + return None + def delete_request( self, url: str, @@ -373,6 +377,8 @@ def delete_request( return r.status_code + return None + def put_request( self, url: str, @@ -425,6 +431,8 @@ def put_request( return r.status_code + return None + def patch_request( self, url: str, @@ -483,6 +491,8 @@ def patch_request( return r.status_code + return None + def validate_response(self, resp: requests.Response) -> None: """ Validate that the response is not an error code. @@ -516,7 +526,7 @@ def data_parse(self, resp: list) -> list: ... def data_parse(self, resp: dict[str, Any] | list) -> dict[str, Any] | list: """ - Determines if the response json has nested data. + Determine if the response json has nested data. If it is nested, it just returns the data. This is useful in dealing with requests that might return multiple records, @@ -541,7 +551,7 @@ def data_parse(self, resp: dict[str, Any] | list) -> dict[str, Any] | list: def next_page_check_url(self, resp: dict[str, Any]) -> bool: """ - Check to determine if there is a next page. + Determine if there is a next page. This requires that the response json contains a pagination key that is empty if there is not a next page. @@ -553,7 +563,7 @@ def next_page_check_url(self, resp: dict[str, Any]) -> bool: return False def json_check(self, resp: requests.Response) -> bool: - """Check to see if a response has a json included in it.""" + """Check if a response has a json included in it.""" try: resp.json() return True @@ -562,5 +572,5 @@ def json_check(self, resp: requests.Response) -> bool: return False def convert_to_table(self, data: list | Any) -> Table: - """Internal method to create a Parsons table from a data element.""" + """Create a Parsons table from a data element.""" return Table(data) if isinstance(data, list) else Table([data]) diff --git a/pyproject.toml b/pyproject.toml index 6ca7a2b034..014bf330c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ classifiers = [ ] dependencies = [ "petl >= 1.7.17", - "pyrate-limiter >= 4.0", "python-dateutil >= 2.0", "requests >= 2.0", "requests-ratelimiter >= 0.10", From 78f0a1577d767539ec2ecb65e043d5d11a014743 Mon Sep 17 00:00:00 2001 From: Ramona T Date: Sat, 29 Aug 2026 20:23:30 -0400 Subject: [PATCH 94/94] Create TypedDicts for return data --- parsons/solidarity_tech/base.py | 8 ++- parsons/solidarity_tech/st_activities.py | 35 +++++++++-- parsons/solidarity_tech/st_calls.py | 37 ++++++++--- parsons/solidarity_tech/st_chapters.py | 18 ++++-- .../st_custom_user_properties.py | 24 +++++-- .../solidarity_tech/st_donation_charges.py | 60 ++++++++++++++++-- parsons/solidarity_tech/st_email_senders.py | 23 +++++-- .../solidarity_tech/st_field_survey_urls.py | 7 ++- parsons/solidarity_tech/st_user_lists.py | 24 ++++--- .../solidarity_tech/st_user_relationships.py | 12 ++-- parsons/solidarity_tech/st_users.py | 62 +++++++++++++++---- 11 files changed, 246 insertions(+), 64 deletions(-) diff --git a/parsons/solidarity_tech/base.py b/parsons/solidarity_tech/base.py index d3959070f3..28c327f846 100644 --- a/parsons/solidarity_tech/base.py +++ b/parsons/solidarity_tech/base.py @@ -3,7 +3,7 @@ import logging from datetime import datetime from enum import Enum -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, TypedDict, cast import numpy as np import pyrate_limiter @@ -24,6 +24,12 @@ ParamsType = _JsonType | np.int64 +class Metadata(TypedDict): + total_count: int + limit: int + offset: int + + class SolidarityTechBase: """Base class for interacting with the SolidarityTech API.""" diff --git a/parsons/solidarity_tech/st_activities.py b/parsons/solidarity_tech/st_activities.py index c8092b9db2..7ea86f93dc 100644 --- a/parsons/solidarity_tech/st_activities.py +++ b/parsons/solidarity_tech/st_activities.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict from parsons import Table from parsons.solidarity_tech.base import SolidarityTechBase @@ -13,9 +13,36 @@ logger = logging.getLogger(__name__) -ActionData = dict[str, int | str] -ActivityData = dict[str, int | str | ActionData] -ActivityMetadata = dict[str, int] + +class ActionData(TypedDict): + id: int + user_id: int + agent_user_id: int | None + field_type: str | None + old_value: str | None + new_value: str | None + data_import_id: int | None + created_at: str + updated_at: str + + +class ActivityData(TypedDict): + id: int + user_id: int + name: str + actionable_id: int + actionable_type: str + action: ActionData + created_at: str + updated_at: str + + +class ActivityMetadata(TypedDict): + total_count: int | None + limit: int + offset: int + cursor: int | None + next_cursor: int | None class SolidarityTechActivities(SolidarityTechBase): diff --git a/parsons/solidarity_tech/st_calls.py b/parsons/solidarity_tech/st_calls.py index a1045a4c4a..69e4d72327 100644 --- a/parsons/solidarity_tech/st_calls.py +++ b/parsons/solidarity_tech/st_calls.py @@ -1,10 +1,10 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict from parsons import Table -from parsons.solidarity_tech.base import SolidarityTechBase +from parsons.solidarity_tech.base import Metadata, SolidarityTechBase if TYPE_CHECKING: from datetime import datetime @@ -13,9 +13,32 @@ logger = logging.getLogger(__name__) -TranscriptData = dict[str, str | int] -CallData = dict[str, int | str | bool | TranscriptData] -CallMetadata = dict[str, int] + +class TranscriptData(TypedDict): + summary: str | None + rating: int | None + sentiment: str | None + engagement_analysis: str | None + engagement_analysis_justification: str | None + + +class CallData(TypedDict): + id: int + user_id: int + chapter_id: int | None + direction: str + from_number: str | None + to_number: str | None + phonebank_id: int | None + agent_user_id: int | None + notes: str | None + duration: int + picked_up: bool + left_voicemail: bool + twilio_call_sid: str + created_at: str + ended_at: str | None + transcription: TranscriptData | None class SolidarityTechCalls(SolidarityTechBase): @@ -27,7 +50,7 @@ def get_calls( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> tuple[Table, CallMetadata]: + ) -> tuple[Table, Metadata]: """ Retrieve a list of calls. @@ -68,6 +91,6 @@ def get_calls( self._handle_status_codes(res=res, codes=expected_responses) data: list[CallData] = res.json()["data"] - meta: CallMetadata = res.json()["meta"] + meta: Metadata = res.json()["meta"] return Table(data), meta diff --git a/parsons/solidarity_tech/st_chapters.py b/parsons/solidarity_tech/st_chapters.py index aa2180a150..d6f5558138 100644 --- a/parsons/solidarity_tech/st_chapters.py +++ b/parsons/solidarity_tech/st_chapters.py @@ -1,18 +1,24 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict from parsons import Table -from parsons.solidarity_tech.base import SolidarityTechBase +from parsons.solidarity_tech.base import Metadata, SolidarityTechBase if TYPE_CHECKING: from datetime import datetime logger = logging.getLogger(__name__) -ChapterData = dict[str, int | str] -ChapterMetadata = dict[str, int] + +class ChapterData(TypedDict): + id: int + name: str + logo_url: str + organization_id: int + chapter_phone_number: str + calendar_feed_url: str class SolidarityTechChapters(SolidarityTechBase): @@ -23,7 +29,7 @@ def get_chapters( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> tuple[Table, ChapterMetadata]: + ) -> tuple[Table, Metadata]: """ Retrieve a list of chapters. @@ -59,6 +65,6 @@ def get_chapters( self._handle_status_codes(res=res, codes=expected_responses) data: list[ChapterData] = res.json()["data"] - meta: ChapterMetadata = res.json()["meta"] + meta: Metadata = res.json()["meta"] return Table(data), meta diff --git a/parsons/solidarity_tech/st_custom_user_properties.py b/parsons/solidarity_tech/st_custom_user_properties.py index dd6bdf613b..833b5c3d9a 100644 --- a/parsons/solidarity_tech/st_custom_user_properties.py +++ b/parsons/solidarity_tech/st_custom_user_properties.py @@ -1,10 +1,10 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypedDict from parsons import Table -from parsons.solidarity_tech.base import SolidarityTechBase +from parsons.solidarity_tech.base import Metadata, SolidarityTechBase if TYPE_CHECKING: from datetime import datetime @@ -14,8 +14,20 @@ logger = logging.getLogger(__name__) -UserPropertyData = dict[str, int | str | list[dict[str, str | dict]]] -UserPropertyMetadata = dict[str, int] + +class UserPropertyDataValue(TypedDict): + label: dict[str, Any] + value: str + + +class UserPropertyData(TypedDict): + id: int + name: str + key: str + field_type: FieldType + options: list[UserPropertyDataValue] | None + scope_id: int | None + scope_type: ScopeType | None class SolidarityTechCustomUserProperties(SolidarityTechBase): @@ -28,7 +40,7 @@ def get_custom_user_properties( since: int | datetime = 0, scope_id: int | None = None, scope_type: ScopeType | None = None, - ) -> tuple[Table, UserPropertyMetadata]: + ) -> tuple[Table, Metadata]: """ Retrieve a list of custom user properties. @@ -72,7 +84,7 @@ def get_custom_user_properties( self._handle_status_codes(res=res, codes=expected_responses) data: list[UserPropertyData] = res.json()["data"] - meta: UserPropertyMetadata = res.json()["meta"] + meta: Metadata = res.json()["meta"] return Table(data), meta diff --git a/parsons/solidarity_tech/st_donation_charges.py b/parsons/solidarity_tech/st_donation_charges.py index f84fbe101c..834ba3971e 100644 --- a/parsons/solidarity_tech/st_donation_charges.py +++ b/parsons/solidarity_tech/st_donation_charges.py @@ -1,18 +1,66 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypedDict from parsons import Table -from parsons.solidarity_tech.base import SolidarityTechBase +from parsons.solidarity_tech.base import Metadata, SolidarityTechBase if TYPE_CHECKING: from datetime import datetime logger = logging.getLogger(__name__) -DonationChargeData = dict[str, int | str | bool | dict | dict[str, Any]] -DonationChargeMetadata = dict[str, int] + +class DonationChargeDataChapter(TypedDict): + id: int + name: str + + +class DonationChargeDataUser(TypedDict): + id: int + email: str + first_name: str + last_name: str + phone_number: str + created_at: str + address1: str | None + address2: str | None + city: str | None + state: str | None + zip_code: str | None + country_name: str | None + + +class DonationChargeDataActionPage(TypedDict): + id: int + title: str + url_slug: str + + +class DonationChargeData(TypedDict): + id: int + amount: int + created_at: str + updated_at: str + success: bool + refunded: bool + receipt_number: str + hash_id: str + processing_fee_cents: int | None + external_donation_id: str | None + external_donation_date: str | None + is_external: bool + amount_in_dollars: str + currency: str + currency_symbol: str + receipt_url: str + brand: str + last4: str + json: dict[str, Any] + user: DonationChargeDataUser + action_page: DonationChargeDataActionPage + chapter: DonationChargeDataChapter class SolidarityTechDonationCharges(SolidarityTechBase): @@ -23,7 +71,7 @@ def get_donation_charges( limit: int = 20, offset: int = 0, since: int | datetime = 0, - ) -> tuple[Table, DonationChargeMetadata]: + ) -> tuple[Table, Metadata]: """ Retrieve a list of donation charges. @@ -59,7 +107,7 @@ def get_donation_charges( self._handle_status_codes(res=res, codes=expected_responses) data: list[DonationChargeData] = res.json()["data"] - meta: DonationChargeMetadata = res.json()["meta"] + meta: Metadata = res.json()["meta"] return Table(data), meta diff --git a/parsons/solidarity_tech/st_email_senders.py b/parsons/solidarity_tech/st_email_senders.py index 3e5d4358f7..b090098984 100644 --- a/parsons/solidarity_tech/st_email_senders.py +++ b/parsons/solidarity_tech/st_email_senders.py @@ -1,14 +1,27 @@ from __future__ import annotations import logging +from typing import TypedDict from parsons import Table -from parsons.solidarity_tech.base import SolidarityTechBase +from parsons.solidarity_tech.base import Metadata, SolidarityTechBase logger = logging.getLogger(__name__) -EmailSenderData = dict[str, int | str | bool] -EmailSenderMetadata = dict[str, int] + +EmailSenderData = TypedDict( + "EmailSenderData", + { + "id": int, + "name": str, + "email": str, + "from": str, + "default_for_scope": bool, + "scope_type": str, + "scope_id": int, + "created_at": str, + }, +) class SolidarityTechEmailSenders(SolidarityTechBase): @@ -18,7 +31,7 @@ def get_email_senders( self, limit: int = 20, offset: int = 0, - ) -> tuple[Table, EmailSenderMetadata]: + ) -> tuple[Table, Metadata]: """ Retrieve a list of email senders available for the API key's scope. @@ -53,5 +66,5 @@ def get_email_senders( self._handle_status_codes(res=res, codes=expected_responses) data: list[EmailSenderData] = res.json()["data"] - meta: EmailSenderMetadata = res.json()["meta"] + meta: Metadata = res.json()["meta"] return Table(data), meta diff --git a/parsons/solidarity_tech/st_field_survey_urls.py b/parsons/solidarity_tech/st_field_survey_urls.py index 239ceacea5..4725fd37b5 100644 --- a/parsons/solidarity_tech/st_field_survey_urls.py +++ b/parsons/solidarity_tech/st_field_survey_urls.py @@ -1,13 +1,16 @@ from __future__ import annotations import logging -from typing import Any, Literal +from typing import Any, TypedDict from parsons.solidarity_tech.base import SolidarityTechBase logger = logging.getLogger(__name__) -FieldSurveyURL = dict[Literal["url", "expires_at"], str] + +class FieldSurveyURL(TypedDict): + url: str + expires_at: str class SolidarityTechFieldSurveyURLs(SolidarityTechBase): diff --git a/parsons/solidarity_tech/st_user_lists.py b/parsons/solidarity_tech/st_user_lists.py index b9b0192443..081ac3354a 100644 --- a/parsons/solidarity_tech/st_user_lists.py +++ b/parsons/solidarity_tech/st_user_lists.py @@ -2,7 +2,7 @@ import logging import numbers -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal, TypedDict from parsons import Table from parsons.solidarity_tech.base import SolidarityTechBase @@ -14,12 +14,22 @@ from parsons.solidarity_tech.enums import ScopeType +logger = logging.getLogger(__name__) + CompareValueType = str | numbers.Rational | bool -QueryParamType = dict[ - str, str | bool | list[dict[str, CompareValueType | list[dict[str, CompareValueType]]]] -] -logger = logging.getLogger(__name__) + +class QueryRule(TypedDict): + id: str + type: str + operator: str + value: CompareValueType | list[CompareValueType] + + +class QueryParams(TypedDict): + condition: Literal["AND", "OR"] + valid: bool + rules: list[QueryRule] class SolidarityTechUserLists(SolidarityTechBase): @@ -105,7 +115,7 @@ def create_user_list( scope_type: ScopeType, event_id: np.int64 | None = None, user_id: np.int64 | None = None, - parameters: QueryParamType | None = None, + parameters: QueryParams | None = None, ) -> bool: """ Create a user list with the specified details. @@ -164,7 +174,7 @@ def update_user_list( name: str | None = None, scope_id: np.int64 | None = None, scope_type: str | None = None, - parameters: QueryParamType | None = None, + parameters: QueryParams | None = None, event_id: np.int64 | None = None, ) -> bool: """ diff --git a/parsons/solidarity_tech/st_user_relationships.py b/parsons/solidarity_tech/st_user_relationships.py index 2d63c134e6..18d3d36053 100644 --- a/parsons/solidarity_tech/st_user_relationships.py +++ b/parsons/solidarity_tech/st_user_relationships.py @@ -1,8 +1,7 @@ from __future__ import annotations import logging -import numbers -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, TypedDict from parsons import Table from parsons.solidarity_tech.base import SolidarityTechBase @@ -12,11 +11,10 @@ logger = logging.getLogger(__name__) -CompareValueType = str | numbers.Rational | bool -QueryParamType = dict[ - str, str | bool | list[dict[str, CompareValueType | list[dict[str, CompareValueType]]]] -] -UserRelationshipData = dict[Literal["id", "text"], str] + +class UserRelationshipData(TypedDict): + id: str + text: str class SolidarityTechUserRelationships(SolidarityTechBase): diff --git a/parsons/solidarity_tech/st_users.py b/parsons/solidarity_tech/st_users.py index 5daf54e7c2..8dab88c019 100644 --- a/parsons/solidarity_tech/st_users.py +++ b/parsons/solidarity_tech/st_users.py @@ -1,28 +1,64 @@ from __future__ import annotations import logging -import numbers -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypedDict from zoneinfo import ZoneInfo from parsons import Table -from parsons.solidarity_tech.base import SolidarityTechBase +from parsons.solidarity_tech.base import Metadata, SolidarityTechBase if TYPE_CHECKING: + import numbers from datetime import datetime from parsons.solidarity_tech.base import ParamsType logger = logging.getLogger(__name__) -CompareValueType = str | numbers.Rational | bool -QueryParamType = dict[ - str, str | bool | list[dict[str, CompareValueType | list[dict[str, CompareValueType]]]] -] -UserData = dict[str, str | int | list[int] | list[str] | dict[str, Any] | bool] -UserMetadata = dict[str, int] -UserMergeMetadata = dict[str, str | int | list[int]] -UserDeleteMetadata = dict[str, int] + +class AddressData(TypedDict): + address1: str | None + address2: str | None + city: str | None + state: str | None + zip_code: str | None + country: str | None + + +class UserData(TypedDict): + id: int + hash_id: str + phone_number: str | None + email: str | None + first_name: str | None + last_name: str | None + alternate_name: str | None + preferred_language: str + second_language: str | None + chapter_id: int + chapter_ids: list[int] + branch_id: int | None + created_at: str + custom_user_properties: dict[str, str | list[str]] + address: AddressData + sms_permission: bool + call_permission: bool + email_permission: bool + other_emails: list[str] + other_phone_numbers: list[str] + + +class UserMergeMetadata(TypedDict): + message: str + primary_user_id: int + merged_user_ids: list[int] + merged_count: int + not_found_user_ids: list[int] | None + + +class UserDeleteMetadata(TypedDict): + message: str + id: int | None class SolidarityTechUsers(SolidarityTechBase): @@ -36,7 +72,7 @@ def get_users( user_list_ids: str | list[int] | None = None, phone_number: str | None = None, email: str | None = None, - ) -> tuple[Table, UserMetadata]: + ) -> tuple[Table, Metadata]: """ Retrieve a list of users. @@ -91,7 +127,7 @@ def get_users( self._handle_status_codes(res=res, codes=expected_responses) data: list[UserData] = res.json()["data"] - meta: UserMetadata = res.json()["meta"] + meta: Metadata = res.json()["meta"] return Table(data), meta