From 554432a8cfece7fdcfa7a5d4686b394a07660f6d Mon Sep 17 00:00:00 2001 From: Forest Gregg Date: Thu, 5 Jun 2025 14:33:22 -0400 Subject: [PATCH 01/11] first slice of apiconnector refactor --- parsons/ngpvan/van.py | 19 +++++- parsons/ngpvan/van_connector.py | 87 +++++++------------------ parsons/utilities/api_connector_next.py | 67 +++++++++++++++++++ 3 files changed, 108 insertions(+), 65 deletions(-) create mode 100644 parsons/utilities/api_connector_next.py diff --git a/parsons/ngpvan/van.py b/parsons/ngpvan/van.py index 5c1e90c2de..95fae5b67d 100644 --- a/parsons/ngpvan/van.py +++ b/parsons/ngpvan/van.py @@ -1,6 +1,8 @@ import logging from typing import Literal, Optional +from scraplib import Scraper + from parsons.ngpvan.activist_codes import ActivistCodes from parsons.ngpvan.bulk_import import BulkImport from parsons.ngpvan.canvass_responses import CanvassResponses @@ -66,9 +68,20 @@ def __init__( api_key: Optional[str] = None, db: Optional[Literal["MyVoters", "MyCampaign", "MyMembers", "EveryAction"]] = None, ): - self.connection = VANConnector(api_key=api_key, db=db) - self.api_key = api_key - self.db = db + if db == "MyVoters": + db_code = 0 + elif db in ["MyMembers", "MyCampaign", "EveryAction"]: + db_code = 1 + else: + raise KeyError( + "Invalid database type specified. Pick one of:" + " MyVoters, MyCampaign, MyMembers, EveryAction." + ) + + session = Scraper() + session.auth = ("default", api_key + "|" + str(db_code)) + + self.connection = VANConnector(session) # The size of each page to return. Currently set to maximum. self.page_size = 200 diff --git a/parsons/ngpvan/van_connector.py b/parsons/ngpvan/van_connector.py index f9937ee69d..b553f2b8e2 100644 --- a/parsons/ngpvan/van_connector.py +++ b/parsons/ngpvan/van_connector.py @@ -1,46 +1,31 @@ import logging -from suds.client import Client - -from parsons.utilities import check_env -from parsons.utilities.api_connector import APIConnector +from parsons.utilities.api_connector_next import APIConnector logger = logging.getLogger(__name__) -URI = "https://api.securevan.com/v4/" -SOAP_URI = "https://api.securevan.com/Services/V3/ListService.asmx?WSDL" +class VanConnector(APIConnector): + def __init__(self, *, session, api_key, auth_name, db_code): + self.session = session + self.URI = "https://api.securevan.com/v4/" -class VANConnector(object): - def __init__(self, api_key=None, auth_name="default", db=None): - self.api_key = check_env.check("VAN_API_KEY", api_key) + def items(self, endpoint, **kwargs): + response = self.get_request(endpoint, **kwargs) - if db == "MyVoters": - self.db_code = 0 - elif db in ["MyMembers", "MyCampaign", "EveryAction"]: - self.db_code = 1 - else: - raise KeyError( - "Invalid database type specified. Pick one of:" - " MyVoters, MyCampaign, MyMembers, EveryAction." - ) + data = response.json() + next_page_url = data["nextPageLink"] + items = data["items"] + + yield from items - self.uri = URI - self.db = db - self.auth_name = auth_name - self.pagination_key = "nextPageLink" - self.auth = (self.auth_name, self.api_key + "|" + str(self.db_code)) - self.api = APIConnector( - self.uri, - auth=self.auth, - data_key="items", - pagination_key=self.pagination_key, - ) - - # We will not create the SOAP client unless we need to as this triggers checking for - # valid credentials. As not all API keys are provisioned for SOAP, this keeps it from - # raising a permission exception when creating the class. - self._soap_client = None + while items and isinstance(data, dict) and next_page_url: + response = self.session.get(next_page_url, **kwargs) + data = response.json() + next_page_url = data["nextPageLink"] + items = data["items"] + + yield from items @property def api_key_profile(self): @@ -48,10 +33,12 @@ def api_key_profile(self): Returns the API key profile with includes permissions and other metadata. """ - return self.get_request("apiKeyProfiles")[0] + return self.get("apiKeyProfiles")[0] @property def soap_client(self): + from suds.client import Client + if not self._soap_client: # Create the SOAP client soap_auth = { @@ -60,7 +47,9 @@ def soap_client(self): "APIKey": self.api_key, } } - self._soap_client = Client(SOAP_URI, soapheaders=soap_auth) + self._soap_client = Client( + "https://api.securevan.com/Services/V3/ListService.asmx?WSDL", soapheaders=soap_auth + ) return self._soap_client @@ -75,29 +64,3 @@ def soap_client_db(self): return "MyCampaign" else: return self.db - - def get_request(self, endpoint, **kwargs): - r = self.api.get_request(self.uri + endpoint, **kwargs) - data = self.api.data_parse(r) - - # Paginate - while isinstance(r, dict) and self.api.next_page_check_url(r): - if endpoint == "savedLists" and not r["items"]: - break - if endpoint == "printedLists" and not r["items"]: - break - r = self.api.get_request(r[self.pagination_key], **kwargs) - data.extend(self.api.data_parse(r)) - return data - - def post_request(self, endpoint, **kwargs): - return self.api.post_request(endpoint, **kwargs) - - def delete_request(self, endpoint, **kwargs): - return self.api.delete_request(endpoint, **kwargs) - - def patch_request(self, endpoint, **kwargs): - return self.api.patch_request(endpoint, **kwargs) - - def put_request(self, endpoint, **kwargs): - return self.api.put_request(endpoint, **kwargs) diff --git a/parsons/utilities/api_connector_next.py b/parsons/utilities/api_connector_next.py new file mode 100644 index 0000000000..02e6806d89 --- /dev/null +++ b/parsons/utilities/api_connector_next.py @@ -0,0 +1,67 @@ +from typing import Any, Iterable, Mapping, Optional, Text, Union + +import requests + +Data = Optional[ + Union[ + Iterable[bytes], + str, + bytes, + list[tuple[Any, Any]], + tuple[tuple[Any, Any], ...], + Mapping[Any, Any], + ] +] + +Response = requests.models.Response + + +class APIConnector: + def __init__(self, *, session, **kwargs): + self.session = session + self.URI + + def get_request(self, endpoint: Union[str, bytes, Text], **kwargs) -> Response: + r"""Sends a GET request. Returns :class:`Response` object.""" + url = self.URI + endpoint + return self.session.get(url, **kwargs) + + def options_request(self, endpoint: Union[str, bytes, Text], **kwargs) -> Response: + r"""Sends a OPTIONS request. Returns :class:`Response` object.""" + url = self.URI + endpoint + return self.session.options(url, **kwargs) + + def head_request(self, endpoint: Union[str, bytes, Text], **kwargs) -> Response: + r"""Sends a HEAD request. Returns :class:`Response` object.""" + url = self.URI + endpoint + return self.session.head(url, **kwargs) + + def post_request( + self, + endpoint: Union[str, bytes, Text], + data: Data = None, + json: Optional[Any] = None, + **kwargs, + ) -> Response: + r"""Sends a POST request. Returns :class:`Response` object.""" + url = self.URI + endpoint + return self.post(url, data=data, json=json, **kwargs) + + def put_request( + self, endpoint: Union[str, bytes, Text], data: Data = None, **kwargs + ) -> Response: + r"""Sends a PUT request. Returns :class:`Response` object.""" + + url = self.URI + endpoint + return self.session.put(url, data=data, **kwargs) + + def patch_request(self, endpoint: Union[str, bytes, Text], data=None, **kwargs): + r"""Sends a PATCH request. Returns :class:`Response` object.""" + url = self.URI + endpoint + return self.sessions.patch(url, data=data, **kwargs) + + def delete_request(self, endpoint: Union[str, bytes, Text], **kwargs) -> Response: + r"""Sends a DELETE request. Returns :class:`Response` object.""" + + url = self.URI + endpoint + return self.delete("DELETE", url, **kwargs) From 55021438a03d20fffb1c9c6d6e39d3a96e8867cf Mon Sep 17 00:00:00 2001 From: Forest Gregg Date: Thu, 5 Jun 2025 14:34:49 -0400 Subject: [PATCH 02/11] first slice of apiconnector refactor --- parsons/utilities/api_connector_next.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parsons/utilities/api_connector_next.py b/parsons/utilities/api_connector_next.py index 02e6806d89..e918ed9fa5 100644 --- a/parsons/utilities/api_connector_next.py +++ b/parsons/utilities/api_connector_next.py @@ -58,10 +58,10 @@ def put_request( def patch_request(self, endpoint: Union[str, bytes, Text], data=None, **kwargs): r"""Sends a PATCH request. Returns :class:`Response` object.""" url = self.URI + endpoint - return self.sessions.patch(url, data=data, **kwargs) + return self.session.patch(url, data=data, **kwargs) def delete_request(self, endpoint: Union[str, bytes, Text], **kwargs) -> Response: r"""Sends a DELETE request. Returns :class:`Response` object.""" url = self.URI + endpoint - return self.delete("DELETE", url, **kwargs) + return self.session.delete("DELETE", url, **kwargs) From c8adb2b7dd72ed104c08217e52952f01315a983a Mon Sep 17 00:00:00 2001 From: Forest Gregg Date: Thu, 5 Jun 2025 14:39:28 -0400 Subject: [PATCH 03/11] explicitly pass in the URI --- parsons/ngpvan/van.py | 2 +- parsons/ngpvan/van_connector.py | 4 ---- parsons/utilities/api_connector_next.py | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/parsons/ngpvan/van.py b/parsons/ngpvan/van.py index 95fae5b67d..cfe4b7369b 100644 --- a/parsons/ngpvan/van.py +++ b/parsons/ngpvan/van.py @@ -81,7 +81,7 @@ def __init__( session = Scraper() session.auth = ("default", api_key + "|" + str(db_code)) - self.connection = VANConnector(session) + self.connection = VANConnector(session=session, URI="https://api.securevan.com/v4/") # The size of each page to return. Currently set to maximum. self.page_size = 200 diff --git a/parsons/ngpvan/van_connector.py b/parsons/ngpvan/van_connector.py index b553f2b8e2..b2436ab93c 100644 --- a/parsons/ngpvan/van_connector.py +++ b/parsons/ngpvan/van_connector.py @@ -6,10 +6,6 @@ class VanConnector(APIConnector): - def __init__(self, *, session, api_key, auth_name, db_code): - self.session = session - self.URI = "https://api.securevan.com/v4/" - def items(self, endpoint, **kwargs): response = self.get_request(endpoint, **kwargs) diff --git a/parsons/utilities/api_connector_next.py b/parsons/utilities/api_connector_next.py index e918ed9fa5..2e410f6992 100644 --- a/parsons/utilities/api_connector_next.py +++ b/parsons/utilities/api_connector_next.py @@ -17,9 +17,9 @@ class APIConnector: - def __init__(self, *, session, **kwargs): + def __init__(self, *, session, URI, **kwargs): self.session = session - self.URI + self.URI = URI def get_request(self, endpoint: Union[str, bytes, Text], **kwargs) -> Response: r"""Sends a GET request. Returns :class:`Response` object.""" From 41d0a263668b889e7d6ff74873eac47fdf2a9659 Mon Sep 17 00:00:00 2001 From: Forest Gregg Date: Thu, 5 Jun 2025 14:40:38 -0400 Subject: [PATCH 04/11] using get_request not get --- parsons/ngpvan/van_connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsons/ngpvan/van_connector.py b/parsons/ngpvan/van_connector.py index b2436ab93c..1452fa8b61 100644 --- a/parsons/ngpvan/van_connector.py +++ b/parsons/ngpvan/van_connector.py @@ -29,7 +29,7 @@ def api_key_profile(self): Returns the API key profile with includes permissions and other metadata. """ - return self.get("apiKeyProfiles")[0] + return self.get_request("apiKeyProfiles")[0] @property def soap_client(self): From ceee97e0651cae2f0243b4a26cbfc2310d7f4f2a Mon Sep 17 00:00:00 2001 From: Forest Gregg Date: Thu, 19 Jun 2025 13:59:23 -0400 Subject: [PATCH 05/11] getting tests going --- parsons/__init__.py | 1 + parsons/ngpvan/van.py | 4 ++-- parsons/ngpvan/van_connector.py | 2 +- parsons/utilities/api_connector_next.py | 18 +++++++++--------- setup.py | 1 + 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/parsons/__init__.py b/parsons/__init__.py index 48b0296a06..48d37bbccd 100644 --- a/parsons/__init__.py +++ b/parsons/__init__.py @@ -100,4 +100,5 @@ globals()[connector_name] = getattr(importlib.import_module(module_path), connector_name) __all__.append(connector_name) except ImportError: + raise logger.debug(f"Could not import {module_path}.{connector_name}; skipping") diff --git a/parsons/ngpvan/van.py b/parsons/ngpvan/van.py index cfe4b7369b..67ef4f0182 100644 --- a/parsons/ngpvan/van.py +++ b/parsons/ngpvan/van.py @@ -1,7 +1,7 @@ import logging from typing import Literal, Optional -from scraplib import Scraper +from scrapelib import Scraper from parsons.ngpvan.activist_codes import ActivistCodes from parsons.ngpvan.bulk_import import BulkImport @@ -81,7 +81,7 @@ def __init__( session = Scraper() session.auth = ("default", api_key + "|" + str(db_code)) - self.connection = VANConnector(session=session, URI="https://api.securevan.com/v4/") + self.connection = VANConnector(session=session, uri="https://api.securevan.com/v4/") # The size of each page to return. Currently set to maximum. self.page_size = 200 diff --git a/parsons/ngpvan/van_connector.py b/parsons/ngpvan/van_connector.py index 1452fa8b61..97c60ee0cf 100644 --- a/parsons/ngpvan/van_connector.py +++ b/parsons/ngpvan/van_connector.py @@ -5,7 +5,7 @@ logger = logging.getLogger(__name__) -class VanConnector(APIConnector): +class VANConnector(APIConnector): def items(self, endpoint, **kwargs): response = self.get_request(endpoint, **kwargs) diff --git a/parsons/utilities/api_connector_next.py b/parsons/utilities/api_connector_next.py index 2e410f6992..99ed3eef48 100644 --- a/parsons/utilities/api_connector_next.py +++ b/parsons/utilities/api_connector_next.py @@ -17,23 +17,23 @@ class APIConnector: - def __init__(self, *, session, URI, **kwargs): + def __init__(self, *, session, uri, **kwargs): self.session = session - self.URI = URI + self.uri = uri def get_request(self, endpoint: Union[str, bytes, Text], **kwargs) -> Response: r"""Sends a GET request. Returns :class:`Response` object.""" - url = self.URI + endpoint + url = self.uri + endpoint return self.session.get(url, **kwargs) def options_request(self, endpoint: Union[str, bytes, Text], **kwargs) -> Response: r"""Sends a OPTIONS request. Returns :class:`Response` object.""" - url = self.URI + endpoint + url = self.uri + endpoint return self.session.options(url, **kwargs) def head_request(self, endpoint: Union[str, bytes, Text], **kwargs) -> Response: r"""Sends a HEAD request. Returns :class:`Response` object.""" - url = self.URI + endpoint + url = self.uri + endpoint return self.session.head(url, **kwargs) def post_request( @@ -44,7 +44,7 @@ def post_request( **kwargs, ) -> Response: r"""Sends a POST request. Returns :class:`Response` object.""" - url = self.URI + endpoint + url = self.uri + endpoint return self.post(url, data=data, json=json, **kwargs) def put_request( @@ -52,16 +52,16 @@ def put_request( ) -> Response: r"""Sends a PUT request. Returns :class:`Response` object.""" - url = self.URI + endpoint + url = self.uri + endpoint return self.session.put(url, data=data, **kwargs) def patch_request(self, endpoint: Union[str, bytes, Text], data=None, **kwargs): r"""Sends a PATCH request. Returns :class:`Response` object.""" - url = self.URI + endpoint + url = self.uri + endpoint return self.session.patch(url, data=data, **kwargs) def delete_request(self, endpoint: Union[str, bytes, Text], **kwargs) -> Response: r"""Sends a DELETE request. Returns :class:`Response` object.""" - url = self.URI + endpoint + url = self.uri + endpoint return self.session.delete("DELETE", url, **kwargs) diff --git a/setup.py b/setup.py index 3f2ff5e20e..d9a0ad0f0d 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ def main(): "requests", "requests_oauthlib", "simplejson", + "scrapelib", ] extras_require = { "airtable": ["pyairtable"], From 37830fba16a7667c617179a22b34ce13b79aae27 Mon Sep 17 00:00:00 2001 From: Forest Gregg Date: Thu, 19 Jun 2025 14:01:23 -0400 Subject: [PATCH 06/11] remove debug line --- parsons/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/parsons/__init__.py b/parsons/__init__.py index 48d37bbccd..48b0296a06 100644 --- a/parsons/__init__.py +++ b/parsons/__init__.py @@ -100,5 +100,4 @@ globals()[connector_name] = getattr(importlib.import_module(module_path), connector_name) __all__.append(connector_name) except ImportError: - raise logger.debug(f"Could not import {module_path}.{connector_name}; skipping") From 55278e3026056137806eb4378331c2670f91f3c6 Mon Sep 17 00:00:00 2001 From: Forest Gregg Date: Mon, 14 Jul 2025 12:32:40 -0400 Subject: [PATCH 07/11] working through fixing tests --- parsons/etl/table.py | 47 +++++++++++++++---------- parsons/ngpvan/activist_codes.py | 6 ++-- parsons/ngpvan/bulk_import.py | 2 +- parsons/utilities/api_connector_next.py | 2 +- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/parsons/etl/table.py b/parsons/etl/table.py index 82d2d264d2..27e21e3641 100644 --- a/parsons/etl/table.py +++ b/parsons/etl/table.py @@ -1,3 +1,4 @@ +import itertools import logging import pickle from enum import Enum @@ -58,34 +59,42 @@ def __init__( if lst is _EMPTYDEFAULT: self.table = petl.fromdicts([]) - elif isinstance(lst, list) or isinstance(lst, tuple): - # Check for empty list - if not len(lst): - self.table = petl.fromdicts([]) - else: - row_type = type(lst[0]) - # Check for list of dicts - if row_type is dict: - self.table = petl.fromdicts(lst) - # Check for list of lists - elif row_type in [list, tuple]: - self.table = petl.wrap(lst) - elif isinstance(lst, petl.util.base.Table): # Create from a petl table self.table = lst else: - raise ValueError( - f"Could not initialize table from input type. " - f"Got {type(lst)}, expected list, tuple, or petl Table" - ) + try: + iterable_data = iter(lst) + except TypeError: + raise ValueError( + f"Could not initialize table from input type. " + f"Got {type(lst)}, expected list, tuple, or petl Table" + ) + + try: + peek = next(iterable_data) + except StopIteration: + self.table = petl.fromdicts([]) + else: + # petl can handle generators but does an explicit + # inspect.generator check instead of duck typing, so we have to make + # sure that this is a generator + iterable_data = (each for each in itertools.chain([peek], iterable_data)) + + row_type = type(peek) + # Check for list of dicts + if row_type is dict: + self.table = petl.fromdicts(iterable_data) + # Check for list of lists + elif row_type in [list, tuple]: + self.table = petl.wrap(iterable_data) if not self.is_valid_table(): raise ValueError("Could not create Table") - # Count how many times someone is indexing directly into this table, so we can warn - # against inefficient usage. + # Count how many times someone is indexing directly into this + # table, so we can warn against inefficient usage. self._index_count = 0 def __repr__(self): diff --git a/parsons/ngpvan/activist_codes.py b/parsons/ngpvan/activist_codes.py index cfee2d7789..6f54f60905 100644 --- a/parsons/ngpvan/activist_codes.py +++ b/parsons/ngpvan/activist_codes.py @@ -21,7 +21,7 @@ def get_activist_codes(self): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("activistCodes")) + tbl = Table(self.connection.items("activistCodes")) logger.info(f"Found {tbl.num_rows} activist codes.") return tbl @@ -39,7 +39,7 @@ def get_activist_code(self, activist_code_id): r = self.connection.get_request(f"activistCodes/{activist_code_id}") logger.info(f"Found activist code {activist_code_id}.") - return r + return r.json() def toggle_activist_code( self, id, activist_code_id, action, id_type="vanid", omit_contact=True @@ -60,7 +60,7 @@ def toggle_activist_code( f"{id_type.upper()} {id} {action.capitalize()} " + f"activist code {activist_code_id}" ) - return r + return r.status_code def apply_activist_code(self, id, activist_code_id, id_type="vanid", omit_contact=True): """ diff --git a/parsons/ngpvan/bulk_import.py b/parsons/ngpvan/bulk_import.py index 242990809a..7c172181b1 100644 --- a/parsons/ngpvan/bulk_import.py +++ b/parsons/ngpvan/bulk_import.py @@ -162,7 +162,7 @@ def post_bulk_import( result_fields = [{"name": c} for c in result_fields] json["actions"][0]["columnsToIncludeInResultsFile"] = result_fields - r = self.connection.post_request("bulkImportJobs", json=json) + r = self.connection.post_request("bulkImportJobs", json=json).json() logger.info(f"Bulk upload {r['jobId']} created.") return r["jobId"] diff --git a/parsons/utilities/api_connector_next.py b/parsons/utilities/api_connector_next.py index 99ed3eef48..5f4d5f09b4 100644 --- a/parsons/utilities/api_connector_next.py +++ b/parsons/utilities/api_connector_next.py @@ -45,7 +45,7 @@ def post_request( ) -> Response: r"""Sends a POST request. Returns :class:`Response` object.""" url = self.uri + endpoint - return self.post(url, data=data, json=json, **kwargs) + return self.session.post(url, data=data, json=json, **kwargs) def put_request( self, endpoint: Union[str, bytes, Text], data: Data = None, **kwargs From 13a74463cfbda5125fe02d55e6403d6be9b17a3c Mon Sep 17 00:00:00 2001 From: Forest Gregg Date: Mon, 14 Jul 2025 15:30:44 -0400 Subject: [PATCH 08/11] add a data helper method to the van connector class --- parsons/etl/table.py | 5 ++++- parsons/ngpvan/activist_codes.py | 4 ++-- parsons/ngpvan/bulk_import.py | 10 +++++----- parsons/ngpvan/canvass_responses.py | 6 +++--- parsons/ngpvan/changed_entities.py | 8 ++++---- parsons/ngpvan/codes.py | 10 +++++----- parsons/ngpvan/contact_notes.py | 2 +- parsons/ngpvan/custom_fields.py | 4 ++-- parsons/ngpvan/email.py | 4 ++-- parsons/ngpvan/events.py | 8 ++++---- parsons/ngpvan/introspection.py | 4 ++-- parsons/ngpvan/locations.py | 6 +++--- parsons/ngpvan/people.py | 10 +++++----- parsons/ngpvan/printed_lists.py | 4 ++-- parsons/ngpvan/saved_lists.py | 14 +++++++------- parsons/ngpvan/scores.py | 12 ++++++------ parsons/ngpvan/signups.py | 12 ++++++------ parsons/ngpvan/supporter_groups.py | 4 ++-- parsons/ngpvan/survey_questions.py | 4 ++-- parsons/ngpvan/targets.py | 8 ++++---- parsons/ngpvan/van.py | 6 +++--- parsons/ngpvan/van_connector.py | 11 ++++++++--- parsons/utilities/api_connector_next.py | 2 +- test/test_van/test_ngpvan.py | 2 ++ test/test_van/test_saved_lists.py | 2 ++ 25 files changed, 87 insertions(+), 75 deletions(-) diff --git a/parsons/etl/table.py b/parsons/etl/table.py index 27e21e3641..c6bcac8ec9 100644 --- a/parsons/etl/table.py +++ b/parsons/etl/table.py @@ -88,7 +88,10 @@ def __init__( self.table = petl.fromdicts(iterable_data) # Check for list of lists elif row_type in [list, tuple]: - self.table = petl.wrap(iterable_data) + # the wrap method does not support generators (or + # more precisely only allows us to read a table + # created from generator once + self.table = petl.wrap(list(iterable_data)) if not self.is_valid_table(): raise ValueError("Could not create Table") diff --git a/parsons/ngpvan/activist_codes.py b/parsons/ngpvan/activist_codes.py index 6f54f60905..db9a4e9df8 100644 --- a/parsons/ngpvan/activist_codes.py +++ b/parsons/ngpvan/activist_codes.py @@ -37,9 +37,9 @@ def get_activist_code(self, activist_code_id): The activist code """ - r = self.connection.get_request(f"activistCodes/{activist_code_id}") + r = self.connection.data(f"activistCodes/{activist_code_id}") logger.info(f"Found activist code {activist_code_id}.") - return r.json() + return r def toggle_activist_code( self, id, activist_code_id, action, id_type="vanid", omit_contact=True diff --git a/parsons/ngpvan/bulk_import.py b/parsons/ngpvan/bulk_import.py index 7c172181b1..5c1c576ada 100644 --- a/parsons/ngpvan/bulk_import.py +++ b/parsons/ngpvan/bulk_import.py @@ -25,7 +25,7 @@ def get_bulk_import_resources(self): A list of resources. """ - r = self.connection.get_request("bulkImportJobs/resources") + r = self.connection.data("bulkImportJobs/resources") logger.info(f"Found {len(r)} bulk import resources.") return r @@ -41,7 +41,7 @@ def get_bulk_import_job(self, job_id): The bulk import job """ - r = self.connection.get_request(f"bulkImportJobs/{job_id}") + r = self.connection.data(f"bulkImportJobs/{job_id}") logger.info(f"Found bulk import job {job_id}.") return r @@ -77,7 +77,7 @@ def get_bulk_import_mapping_types(self): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("bulkImportMappingTypes")) + tbl = Table(self.connection.data("bulkImportMappingTypes")) logger.info(f"Found {tbl.num_rows} bulk import mapping types.") return tbl @@ -92,7 +92,7 @@ def get_bulk_import_mapping_type(self, type_name): A mapping type json """ - r = self.connection.get_request(f"bulkImportMappingTypes/{type_name}") + r = self.connection.data(f"bulkImportMappingTypes/{type_name}") logger.info(f"Found {type_name} bulk import mapping type.") return r @@ -110,7 +110,7 @@ def get_bulk_import_mapping_type_fields(self, type_name, field_name): A mapping type fields json """ - r = self.connection.get_request(f"bulkImportMappingTypes/{type_name}/{field_name}/values") + r = self.connection.data(f"bulkImportMappingTypes/{type_name}/{field_name}/values") logger.info(f"Found {type_name} bulk import mapping type field values.") return r diff --git a/parsons/ngpvan/canvass_responses.py b/parsons/ngpvan/canvass_responses.py index f4356b7197..ffc1e77ede 100644 --- a/parsons/ngpvan/canvass_responses.py +++ b/parsons/ngpvan/canvass_responses.py @@ -20,7 +20,7 @@ def get_canvass_responses_contact_types(self): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("canvassResponses/contactTypes")) + tbl = Table(self.connection.data("canvassResponses/contactTypes")) logger.info(f"Found {tbl.num_rows} canvass response contact types.") return tbl @@ -33,7 +33,7 @@ def get_canvass_responses_input_types(self): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("canvassResponses/inputTypes")) + tbl = Table(self.connection.data("canvassResponses/inputTypes")) logger.info(f"Found {tbl.num_rows} canvass response input types.") return tbl @@ -46,6 +46,6 @@ def get_canvass_responses_result_codes(self): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("canvassResponses/resultCodes")) + tbl = Table(self.connection.data("canvassResponses/resultCodes")) logger.info(f"Found {tbl.num_rows} canvass response result codes.") return tbl diff --git a/parsons/ngpvan/changed_entities.py b/parsons/ngpvan/changed_entities.py index d85455e6cc..6dde18fe21 100644 --- a/parsons/ngpvan/changed_entities.py +++ b/parsons/ngpvan/changed_entities.py @@ -22,7 +22,7 @@ def get_changed_entity_resources(self): list """ - r = self.connection.get_request("changedEntityExportJobs/resources") + r = self.connection.data("changedEntityExportJobs/resources") logger.info(f"Found {len(r)} changed entity resources.") return r @@ -37,7 +37,7 @@ def get_changed_entity_resource_fields(self, resource_type): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request(f"changedEntityExportJobs/fields/{resource_type}")) + tbl = Table(self.connection.data(f"changedEntityExportJobs/fields/{resource_type}")) logger.info(f"Found {tbl.num_rows} fields for {resource_type}.") return tbl @@ -87,7 +87,7 @@ def get_changed_entities( "includeInactive": include_inactive, } - r = self.connection.post_request("changedEntityExportJobs", json=json) + r = self.connection.post_request("changedEntityExportJobs", json=json).json() while True: status = self._get_changed_entity_job(r["exportJobId"]) @@ -100,5 +100,5 @@ def get_changed_entities( raise ValueError(status["message"]) def _get_changed_entity_job(self, job_id): - r = self.connection.get_request(f"changedEntityExportJobs/{job_id}") + r = self.connection.data(f"changedEntityExportJobs/{job_id}") return r diff --git a/parsons/ngpvan/codes.py b/parsons/ngpvan/codes.py index 7d5d7bb6d2..c4a3a3b724 100644 --- a/parsons/ngpvan/codes.py +++ b/parsons/ngpvan/codes.py @@ -37,7 +37,7 @@ def get_codes(self, name=None, supported_entities=None, parent_code_id=None, cod "$top": 200, } - tbl = Table(self.connection.get_request("codes", params=params)) + tbl = Table(self.connection.items("codes", params=params)) logger.info(f"Found {tbl.num_rows} codes.") return tbl @@ -53,7 +53,7 @@ def get_code(self, code_id): See :ref:`parsons-table` for output options. """ - c = self.connection.get_request(f"codes/{code_id}") + c = self.connection.data(f"codes/{code_id}") logger.debug(c) logger.info(f"Found code {code_id}.") return c @@ -67,7 +67,7 @@ def get_code_types(self): A list of code types. """ - lst = self.connection.get_request("codeTypes") + lst = self.connection.data("codeTypes") logger.info(f"Found {len(lst)} code types.") return lst @@ -131,7 +131,7 @@ def create_code( json["supportedEntities"] = se - r = self.connection.post_request("codes", json=json) + r = self.connection.post_request("codes", json=json).json() logger.info(f"Code {r} created.") return r @@ -229,6 +229,6 @@ def get_code_supported_entities(self): A list of code supported entities. """ - lst = self.connection.get_request("codes/supportedEntities") + lst = self.connection.data("codes/supportedEntities") logger.info(f"Found {len(lst)} code supported entities.") return lst diff --git a/parsons/ngpvan/contact_notes.py b/parsons/ngpvan/contact_notes.py index 038b6f2e24..df66c7bcfc 100644 --- a/parsons/ngpvan/contact_notes.py +++ b/parsons/ngpvan/contact_notes.py @@ -23,7 +23,7 @@ def get_contact_notes(self, van_id): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request(f"people/{van_id}/notes")) + tbl = Table(self.connection.data(f"people/{van_id}/notes")) logger.info(f"Found {tbl.num_rows} custom fields.") return tbl diff --git a/parsons/ngpvan/custom_fields.py b/parsons/ngpvan/custom_fields.py index cf4d6d6cfc..8dc6dc5656 100644 --- a/parsons/ngpvan/custom_fields.py +++ b/parsons/ngpvan/custom_fields.py @@ -24,7 +24,7 @@ def get_custom_fields(self, field_type="contacts"): params = {"customFieldsGroupType": field_type.capitalize()} - tbl = Table(self.connection.get_request("customFields", params=params)) + tbl = Table(self.connection.data("customFields", params=params)) logger.info(f"Found {tbl.num_rows} custom fields.") return tbl @@ -73,6 +73,6 @@ def get_custom_field(self, custom_field_id): A json. """ - r = self.connection.get_request(f"customFields/{custom_field_id}") + r = self.connection.data(f"customFields/{custom_field_id}") logger.info(f"Found custom field {custom_field_id}.") return r diff --git a/parsons/ngpvan/email.py b/parsons/ngpvan/email.py index f17b39b2c3..40d42b21ed 100644 --- a/parsons/ngpvan/email.py +++ b/parsons/ngpvan/email.py @@ -38,7 +38,7 @@ def get_emails(self, ascending: bool = True) -> Table: "$orderby": "dateModified desc", } - tbl = Table(self.connection.get_request("email/messages", params=params)) + tbl = Table(self.connection.data("email/messages", params=params)) logger.debug(f"Found {tbl.num_rows} emails.") return tbl @@ -66,7 +66,7 @@ def get_email(self, email_id: int, expand: bool = True) -> Table: ), } - r = self.connection.get_request(f"email/message/{email_id}", params=params) + r = self.connection.data(f"email/message/{email_id}", params=params) logger.debug(f"Found email {email_id}.") return r diff --git a/parsons/ngpvan/events.py b/parsons/ngpvan/events.py index 0d535417d5..26d12a90ce 100644 --- a/parsons/ngpvan/events.py +++ b/parsons/ngpvan/events.py @@ -70,7 +70,7 @@ def get_events( "$expand": expand_fields, } - tbl = Table(self.connection.get_request("events", params=params)) + tbl = Table(self.connection.items("events", params=params)) logger.info(f"Found {tbl.num_rows} events.") return tbl @@ -107,7 +107,7 @@ def get_event( if expand_fields: expand_fields = ",".join(expand_fields) - r = self.connection.get_request(f"events/{event_id}", params={"$expand": expand_fields}) + r = self.connection.data(f"events/{event_id}", params={"$expand": expand_fields}) logger.info(f"Found event {event_id}.") return r @@ -219,7 +219,7 @@ def create_event( if code_ids: event["codes"] = [{"codeID": c} for c in code_ids] - r = self.connection.post_request("events", json=event) + r = self.connection.post_request("events", json=event).json() logger.info(f"Event {r} created.") return r @@ -271,6 +271,6 @@ def get_event_types(self): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("events/types")) + tbl = Table(self.connection.data("events/types")) logger.info(f"Found {tbl.num_rows} events.") return tbl diff --git a/parsons/ngpvan/introspection.py b/parsons/ngpvan/introspection.py index 3b7732fb5b..a2f4278725 100644 --- a/parsons/ngpvan/introspection.py +++ b/parsons/ngpvan/introspection.py @@ -17,6 +17,6 @@ def get_apikeyprofiles(self): JSON response """ - response = self.connection.get_request("apiKeyProfiles") - logger.info(f"Returned {len(response[0])} API key profiles.") + response = tuple(self.connection.items("apiKeyProfiles")) + logger.info(f"Returned {len(response)} API key profiles. Returning the first one.") return response[0] diff --git a/parsons/ngpvan/locations.py b/parsons/ngpvan/locations.py index 6518eece5f..b18d49b21e 100644 --- a/parsons/ngpvan/locations.py +++ b/parsons/ngpvan/locations.py @@ -23,7 +23,7 @@ def get_locations(self, name=None): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("locations", params={"name": name})) + tbl = Table(self.connection.items("locations", params={"name": name})) logger.info(f"Found {tbl.num_rows} locations.") return self._unpack_loc(tbl) @@ -38,7 +38,7 @@ def get_location(self, location_id): dict """ - r = self.connection.get_request(f"locations/{location_id}") + r = self.connection.data(f"locations/{location_id}") logger.info(f"Found location {location_id}.") return r @@ -83,7 +83,7 @@ def create_location( }, } - r = self.connection.post_request("locations/findOrCreate", json=location) + r = self.connection.post_request("locations/findOrCreate", json=location).json() logger.info(f"Location {r} created.") return r diff --git a/parsons/ngpvan/people.py b/parsons/ngpvan/people.py index c7ce7f2430..1433572214 100644 --- a/parsons/ngpvan/people.py +++ b/parsons/ngpvan/people.py @@ -365,7 +365,7 @@ def _people_search( json_flat = json_format.flatten_json(json) self._valid_search(**json_flat) - return self.connection.post_request(url, json=json) + return self.connection.post_request(url, json=json).json() def _valid_search( self, @@ -457,13 +457,13 @@ def get_person( # Removing the fields that are not returned in MyVoters NOT_IN_MYVOTERS = ["codes", "contribution_history", "organization_roles"] - if self.connection.db_code == 0: + if self.db_code == 0: expand_fields = [v for v in expand_fields if v not in NOT_IN_MYVOTERS] expand_fields = ",".join([json_format.arg_format(f) for f in expand_fields]) logger.info(f"Getting person with {id_type or 'vanid'} of {id} at url {url}") - return self.connection.get_request(url, params={"$expand": expand_fields}) + return self.connection.data(url, params={"$expand": expand_fields}) def delete_person(self, vanid): """ @@ -476,7 +476,7 @@ def delete_person(self, vanid): Success or error. """ url = f"people/{vanid}" - r = self.connection.delete_request(url) + r = self.connection.delete_request(url).json() logger.info(f"Van ID {vanid} suppressed.") return r @@ -750,5 +750,5 @@ def merge_contacts(self, primary_vanid, source_vanid): url = f"people/{source_vanid}/mergeInto" json = {"vanId": primary_vanid} - r = self.connection.put_request(url, json=json) + r = self.connection.put_request(url, json=json).json() return r diff --git a/parsons/ngpvan/printed_lists.py b/parsons/ngpvan/printed_lists.py index 6152e2e4cf..2fb7e0330e 100644 --- a/parsons/ngpvan/printed_lists.py +++ b/parsons/ngpvan/printed_lists.py @@ -41,7 +41,7 @@ def get_printed_lists( params = {key: value for key, value in params.items() if value is not None} - tbl = Table(self.connection.get_request("printedLists", params=params)) + tbl = Table(self.connection.data("printedLists", params=params)) logger.info(f"Found {tbl.num_rows} printed lists.") return tbl @@ -57,6 +57,6 @@ def get_printed_list(self, printed_list_number): dict """ - r = self.connection.get_request(f"printedLists/{printed_list_number}") + r = self.connection.data(f"printedLists/{printed_list_number}") logger.info(f"Found printed list {printed_list_number}.") return r diff --git a/parsons/ngpvan/saved_lists.py b/parsons/ngpvan/saved_lists.py index c678836daa..57a80bb712 100644 --- a/parsons/ngpvan/saved_lists.py +++ b/parsons/ngpvan/saved_lists.py @@ -28,7 +28,7 @@ def get_saved_lists(self, folder_id=None): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("savedLists", params={"folderId": folder_id})) + tbl = Table(self.connection.items("savedLists", params={"folderId": folder_id})) logger.info(f"Found {tbl.num_rows} saved lists.") return tbl @@ -43,7 +43,7 @@ def get_saved_list(self, saved_list_id): dict """ - r = self.connection.get_request(f"savedLists/{saved_list_id}") + r = self.connection.data(f"savedLists/{saved_list_id}") logger.info(f"Found saved list {saved_list_id}.") return r @@ -290,7 +290,7 @@ def get_folders(self): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("folders")) + tbl = Table(self.connection.items("folders")) logger.info(f"Found {tbl.num_rows} folders.") return tbl @@ -306,7 +306,7 @@ def get_folder(self, folder_id): See :ref:`parsons-table` for output options. """ - r = self.connection.get_request(f"folders/{folder_id}") + r = self.connection.data(f"folders/{folder_id}") logger.info(f"Found folder {folder_id}.") return r @@ -324,7 +324,7 @@ def get_export_job_types(self): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("exportJobTypes")) + tbl = Table(self.connection.items("exportJobTypes")) logger.info(f"Found {tbl.num_rows} export job types.") return tbl @@ -354,7 +354,7 @@ def export_job_create(self, list_id, export_type=4, webhookUrl="https://www.noth "webhookUrl": webhookUrl, } - r = self.connection.post_request("exportJobs", json=json) + r = self.connection.post_request("exportJobs", json=json).json() logger.info("Retrieved export job.") return r @@ -370,6 +370,6 @@ def get_export_job(self, export_job_id): See :ref:`parsons-table` for output options. """ - r = self.connection.get_request(f"exportJobs/{export_job_id}") + r = self.connection.data(f"exportJobs/{export_job_id}") logger.info(f"Found export job {export_job_id}.") return r diff --git a/parsons/ngpvan/scores.py b/parsons/ngpvan/scores.py index 2ea01aa97f..d61a952c39 100644 --- a/parsons/ngpvan/scores.py +++ b/parsons/ngpvan/scores.py @@ -24,7 +24,7 @@ def get_scores(self): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("scores")) + tbl = Table(self.connection.items("scores")) logger.info(f"Found {tbl.num_rows} scores.") return tbl @@ -39,7 +39,7 @@ def get_score(self, score_id): dict """ - r = self.connection.get_request(f"scores/{score_id}") + r = self.connection.data(f"scores/{score_id}") logger.info(f"Found score {score_id}.") return r @@ -65,7 +65,7 @@ def get_score_updates(self, created_before=None, created_after=None, score_id=No "scoreId": score_id, } - tbl = Table(self.connection.get_request("scoreUpdates", params=params)) + tbl = Table(self.connection.items("scoreUpdates", params=params)) if tbl.num_rows: tbl.unpack_dict("updateStatistics", prepend=False) tbl.unpack_dict("score", prepend=False) @@ -83,7 +83,7 @@ def get_score_update(self, score_update_id): dict """ - r = self.connection.get_request(f"scoreUpdates/{score_update_id}") + r = self.connection.data(f"scoreUpdates/{score_update_id}") logger.info(f"Returning score update {score_update_id}.") return r @@ -223,7 +223,7 @@ def upload_scores( json["listeners"] = [{"type": "EMAIL", "value": email}] # Upload scores - r = self.connection.post_request("fileLoadingJobs", json=json) + r = self.connection.post_request("fileLoadingJobs", json=json).json() logger.info(f"Scores job {r['jobId']} created.") return r["jobId"] @@ -324,7 +324,7 @@ def create_file_load( "tolerance": auto_tolerance, } - r = self.connection.post_request("fileLoadingJobs", json=json)["jobId"] + r = self.connection.post_request("fileLoadingJobs", json=json).json()["jobId"] logger.info(f"Score loading job {r} created.") return r diff --git a/parsons/ngpvan/signups.py b/parsons/ngpvan/signups.py index 32b28cedb9..af3aeb279c 100644 --- a/parsons/ngpvan/signups.py +++ b/parsons/ngpvan/signups.py @@ -38,7 +38,7 @@ def get_signups_statuses(self, event_id=None, event_type_id=None): if event_type_id: params = {"eventTypeId": event_type_id} - tbl = Table(self.connection.get_request("signups/statuses", params=params)) + tbl = Table(self.connection.data("signups/statuses", params=params)) logger.info(f"Found {tbl.num_rows} signups.") return tbl @@ -54,7 +54,7 @@ def get_person_signups(self, vanid): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("signups", params={"vanID": vanid})) + tbl = Table(self.connection.items("signups", params={"vanID": vanid})) logger.info(f"Found {tbl.num_rows} signups for {vanid}.") return self._unpack_signups(tbl) @@ -70,7 +70,7 @@ def get_event_signups(self, event_id): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("signups", params={"eventId": event_id})) + tbl = Table(self.connection.items("signups", params={"eventId": event_id})) logger.info(f"Found {tbl.num_rows} signups for event {event_id}.") return self._unpack_signups(tbl) @@ -86,7 +86,7 @@ def get_signup(self, event_signup_id): See :ref:`parsons-table` for output options. """ - r = self.connection.get_request(f"signups/{event_signup_id}") + r = self.connection.data(f"signups/{event_signup_id}") logger.info(f"Found sign up {event_signup_id}.") return r @@ -121,7 +121,7 @@ def create_signup(self, vanid, event_id, shift_id, role_id, status_id, location_ "location": {"locationId": location_id}, } - r = self.connection.post_request("signups", json=signup) + r = self.connection.post_request("signups", json=signup).json() logger.info(f"Signup {r} created.") return r @@ -153,7 +153,7 @@ def update_signup( """ # Get the signup object - signup = self.connection.get_request(f"signups/{event_signup_id}") + signup = self.connection.data(f"signups/{event_signup_id}") # Update the signup object if shift_id: diff --git a/parsons/ngpvan/supporter_groups.py b/parsons/ngpvan/supporter_groups.py index be68e2dad7..5af146b304 100644 --- a/parsons/ngpvan/supporter_groups.py +++ b/parsons/ngpvan/supporter_groups.py @@ -20,7 +20,7 @@ def get_supporter_groups(self): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("supporterGroups")) + tbl = Table(self.connection.items("supporterGroups")) logger.info(f"Found {tbl.num_rows} supporter groups.") return tbl @@ -35,7 +35,7 @@ def get_supporter_group(self, supporter_group_id): dict """ - r = self.connection.get_request(f"supporterGroups/{supporter_group_id}") + r = self.connection.data(f"supporterGroups/{supporter_group_id}") logger.info(f"Found supporter group {supporter_group_id}.") return r diff --git a/parsons/ngpvan/survey_questions.py b/parsons/ngpvan/survey_questions.py index c1e8252f6f..9280f15edd 100644 --- a/parsons/ngpvan/survey_questions.py +++ b/parsons/ngpvan/survey_questions.py @@ -43,7 +43,7 @@ def get_survey_questions( "cycle": cycle, } - tbl = Table(self.connection.get_request("surveyQuestions", params=params)) + tbl = Table(self.connection.items("surveyQuestions", params=params)) logger.info(f"Found {tbl.num_rows} survey questions.") return tbl @@ -59,7 +59,7 @@ def get_survey_question(self, survey_question_id): See :ref:`parsons-table` for output options. """ - r = self.connection.get_request(f"surveyQuestions/{survey_question_id}") + r = self.connection.item(f"surveyQuestions/{survey_question_id}") logger.info(f"Found survey question {survey_question_id}.") return r diff --git a/parsons/ngpvan/targets.py b/parsons/ngpvan/targets.py index a5630ee732..194b1362d0 100644 --- a/parsons/ngpvan/targets.py +++ b/parsons/ngpvan/targets.py @@ -29,7 +29,7 @@ def get_targets(self): See :ref:`parsons-table` for output options. """ - tbl = Table(self.connection.get_request("targets")) + tbl = Table(self.connection.items("targets")) logger.info(f"Found {tbl.num_rows} targets.") return tbl @@ -45,7 +45,7 @@ def get_target(self, target_id): The target """ - r = self.connection.get_request(f"targets/{target_id}") + r = self.connection.data(f"targets/{target_id}") logger.info(f"Found target {target_id}.") return r @@ -58,7 +58,7 @@ def get_target_export(self, export_job_id): See :ref:`parsons-table` for output options. """ - response = self.connection.get_request(f"targetExportJobs/{export_job_id}") + response = self.connection.data(f"targetExportJobs/{export_job_id}") job_status = response.get("jobStatus") if job_status == "Complete": url = response["file"]["downloadUrl"] @@ -81,6 +81,6 @@ def create_target_export(self, target_id, webhook_url=None): """ target_export = {"targetId": target_id} - r = self.connection.post_request("targetExportJobs", json=target_export) + r = self.connection.post_request("targetExportJobs", json=target_export).json() logger.info(f"Created new target export job for {target_id}.") return r diff --git a/parsons/ngpvan/van.py b/parsons/ngpvan/van.py index 67ef4f0182..331a875c57 100644 --- a/parsons/ngpvan/van.py +++ b/parsons/ngpvan/van.py @@ -69,9 +69,9 @@ def __init__( db: Optional[Literal["MyVoters", "MyCampaign", "MyMembers", "EveryAction"]] = None, ): if db == "MyVoters": - db_code = 0 + self.db_code = 0 elif db in ["MyMembers", "MyCampaign", "EveryAction"]: - db_code = 1 + self.db_code = 1 else: raise KeyError( "Invalid database type specified. Pick one of:" @@ -79,7 +79,7 @@ def __init__( ) session = Scraper() - session.auth = ("default", api_key + "|" + str(db_code)) + session.auth = ("default", api_key + "|" + str(self.db_code)) self.connection = VANConnector(session=session, uri="https://api.securevan.com/v4/") diff --git a/parsons/ngpvan/van_connector.py b/parsons/ngpvan/van_connector.py index 97c60ee0cf..210929827c 100644 --- a/parsons/ngpvan/van_connector.py +++ b/parsons/ngpvan/van_connector.py @@ -1,4 +1,5 @@ import logging +import typing from parsons.utilities.api_connector_next import APIConnector @@ -6,10 +7,10 @@ class VANConnector(APIConnector): - def items(self, endpoint, **kwargs): - response = self.get_request(endpoint, **kwargs) + def items(self, endpoint: str, **kwargs) -> typing.Generator[dict, None, None]: + "Returns all the items for A GET endpoint, handling pagination" + data = self.data(endpoint, **kwargs) - data = response.json() next_page_url = data["nextPageLink"] items = data["items"] @@ -23,6 +24,10 @@ def items(self, endpoint, **kwargs): yield from items + def data(self, endpoint: str, **kwargs) -> dict: + "Returns the json result of GET endpoint" + return self.get_request(endpoint, **kwargs).json() + @property def api_key_profile(self): """ diff --git a/parsons/utilities/api_connector_next.py b/parsons/utilities/api_connector_next.py index 5f4d5f09b4..c84f6a2b68 100644 --- a/parsons/utilities/api_connector_next.py +++ b/parsons/utilities/api_connector_next.py @@ -64,4 +64,4 @@ def delete_request(self, endpoint: Union[str, bytes, Text], **kwargs) -> Respons r"""Sends a DELETE request. Returns :class:`Response` object.""" url = self.uri + endpoint - return self.session.delete("DELETE", url, **kwargs) + return self.session.delete(url, **kwargs) diff --git a/test/test_van/test_ngpvan.py b/test/test_van/test_ngpvan.py index 6183fc763d..8476c40df2 100644 --- a/test/test_van/test_ngpvan.py +++ b/test/test_van/test_ngpvan.py @@ -7,6 +7,8 @@ from parsons import VAN, Table from test.utils import assert_matching_tables, validate_list +os.environ["VAN_API_KEY"] = "SOME_KEY" + class TestNGPVAN(unittest.TestCase): def setUp(self): diff --git a/test/test_van/test_saved_lists.py b/test/test_van/test_saved_lists.py index 14d46e0684..e09eaba0bd 100644 --- a/test/test_van/test_saved_lists.py +++ b/test/test_van/test_saved_lists.py @@ -8,6 +8,8 @@ from parsons.utilities import cloud_storage from test.utils import validate_list +os.environ["VAN_API_KEY"] = "SOME_KEY" + class TestSavedLists(unittest.TestCase): def setUp(self): From 7a726bea78aece7e1349235c388b76652a2474ba Mon Sep 17 00:00:00 2001 From: Forest Gregg Date: Mon, 14 Jul 2025 15:33:40 -0400 Subject: [PATCH 09/11] suppress exception chaining --- parsons/etl/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsons/etl/table.py b/parsons/etl/table.py index c6bcac8ec9..8739d19ad7 100644 --- a/parsons/etl/table.py +++ b/parsons/etl/table.py @@ -70,7 +70,7 @@ def __init__( raise ValueError( f"Could not initialize table from input type. " f"Got {type(lst)}, expected list, tuple, or petl Table" - ) + ) from None try: peek = next(iterable_data) From fc500186cb9f0ce13b0e401fcb769ca985fdc72f Mon Sep 17 00:00:00 2001 From: Forest Gregg Date: Mon, 14 Jul 2025 15:41:28 -0400 Subject: [PATCH 10/11] remove the generator handling --- parsons/etl/table.py | 55 +++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/parsons/etl/table.py b/parsons/etl/table.py index 8739d19ad7..eb9ce2a40c 100644 --- a/parsons/etl/table.py +++ b/parsons/etl/table.py @@ -1,4 +1,4 @@ -import itertools +import inspect import logging import pickle from enum import Enum @@ -51,6 +51,10 @@ def __init__( ): self.table = None + # + if inspect.isgenerator(lst): + lst = tuple(lst) + # Normally we would use None as the default argument here # Instead of using None, we use a sentinal # This allows us to maintain the existing behavior @@ -59,45 +63,34 @@ def __init__( if lst is _EMPTYDEFAULT: self.table = petl.fromdicts([]) - elif isinstance(lst, petl.util.base.Table): - # Create from a petl table - self.table = lst - - else: - try: - iterable_data = iter(lst) - except TypeError: - raise ValueError( - f"Could not initialize table from input type. " - f"Got {type(lst)}, expected list, tuple, or petl Table" - ) from None - - try: - peek = next(iterable_data) - except StopIteration: + elif isinstance(lst, list) or isinstance(lst, tuple): + # Check for empty list + if not len(lst): self.table = petl.fromdicts([]) else: - # petl can handle generators but does an explicit - # inspect.generator check instead of duck typing, so we have to make - # sure that this is a generator - iterable_data = (each for each in itertools.chain([peek], iterable_data)) - - row_type = type(peek) + row_type = type(lst[0]) # Check for list of dicts if row_type is dict: - self.table = petl.fromdicts(iterable_data) - # Check for list of lists + self.table = petl.fromdicts(lst) + # Check for list of lists elif row_type in [list, tuple]: - # the wrap method does not support generators (or - # more precisely only allows us to read a table - # created from generator once - self.table = petl.wrap(list(iterable_data)) + self.table = petl.wrap(lst) + + elif isinstance(lst, petl.util.base.Table): + # Create from a petl table + self.table = lst + + else: + raise ValueError( + f"Could not initialize table from input type. " + f"Got {type(lst)}, expected list, tuple, or petl Table" + ) if not self.is_valid_table(): raise ValueError("Could not create Table") - # Count how many times someone is indexing directly into this - # table, so we can warn against inefficient usage. + # Count how many times someone is indexing directly into this table, so we can warn + # against inefficient usage. self._index_count = 0 def __repr__(self): From 856d14f679d580328f16dfb00c8f5ccc2fc01cf5 Mon Sep 17 00:00:00 2001 From: Forest Gregg Date: Mon, 14 Jul 2025 15:46:41 -0400 Subject: [PATCH 11/11] add scraplib to requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f8eb77fbf..4168a90291 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,7 @@ PyGitHub==2.6.0 python-dateutil==2.9.0.post0 requests==2.32.3 requests_oauthlib==2.0.0 +scrapelib==2.4.1 setuptools==80.9.0 simple-salesforce==1.12.6 simplejson==3.20.1 @@ -51,4 +52,3 @@ jinja2>=3.0.2 selenium==3.141.0 us==3.2.0 sshtunnel==0.4.0 -