From eccf70bd91c5456bd249479c396b07485713b141 Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Sun, 28 Apr 2024 12:50:59 -0500 Subject: [PATCH 01/10] init openfield class --- docs/openfield.rst | 69 ++++++ parsons/openfield/__init__.py | 3 + parsons/openfield/openfield.py | 372 +++++++++++++++++++++++++++++++++ 3 files changed, 444 insertions(+) create mode 100644 docs/openfield.rst create mode 100644 parsons/openfield/__init__.py create mode 100644 parsons/openfield/openfield.py diff --git a/docs/openfield.rst b/docs/openfield.rst new file mode 100644 index 0000000000..4f9efb00ff --- /dev/null +++ b/docs/openfield.rst @@ -0,0 +1,69 @@ +OpenField +========= + +******** +Overview +******** + +`OpenField `_ is a canvassing and VPB tool for organizing and election campaigns. +`OpenField REST API `_ + +.. note:: + Authentication + OpenField requires `HTTP Basic Auth `_. + Clients with an OpenField account can obtain the domain, username, and password needed + to access the OpenField API. + +********** +Quickstart +********** + +To instantiate the OpenField class, you can either store your OpenField API +domain, username, and password as environmental variables (``OPENFIELD_DOMAIN``, +``OPENFIELD_USERNAME``, and ``OPENFIELD_PASSWORD``, respectively) or pass in your +domain, username, and password as arguments: + +.. code-block:: python + + from parsons import OpenField + + # First approach: Use API credentials via environmental variables + openfield = OpenField() + + # Second approach: Pass API credentials as arguments + openfield = OpenField(domain='myorg.openfield.ai', username='my_name', password='1234') + +You can then call various endpoints: + +.. code-block:: python + + # Create a new person + person = { + "first_name": 'John', + "last_name": 'Smith', + "prov_city": 'Boston', + "prov_state": 'MA', + "prov_zip_5": '02108' + "email1": 'john@email.com', + "phone1": '2345678901', + } + openfield.create_person(person=person) + + # Fetch person + user_fields = openfield.get_person(person_id=123) + + # Update person fields + data= { + "phone1": '5558765432' + } + openfield.update_user(user_id=123, data=data) + + # Delete person + openfield.destroy_user(user_id=123) + +*** +API +*** + +.. autoclass :: parsons.OpenField + :inherited-members: diff --git a/parsons/openfield/__init__.py b/parsons/openfield/__init__.py new file mode 100644 index 0000000000..db4b5de1c7 --- /dev/null +++ b/parsons/openfield/__init__.py @@ -0,0 +1,3 @@ +from parsons.openfield.openfield import OpenField + +__all__ = ["OpenField"] diff --git a/parsons/openfield/openfield.py b/parsons/openfield/openfield.py new file mode 100644 index 0000000000..48d6eebc5a --- /dev/null +++ b/parsons/openfield/openfield.py @@ -0,0 +1,372 @@ +import json +import logging +import requests + +from parsons.etl.table import Table +from parsons.utilities import check_env + +logger = logging.getLogger(__name__) + + +class OpenField: + """ + Instantiate the OpenField class + + `Args:` + domain: str + The OpenField domain (e.g. ``org-name.openfield.ai``) + Not required if ``OPENFIELD_DOMAIN`` env variable set. + username: str + The authorized OpenField username. + Not required if ``OPENFIELD_USERNAME`` env variable set. + password: str + The authorized OpenField user password. + Not required if ``OPENFIELD_PASSWORD`` env variable set. + """ + + _default_headers = { + "content-type": "application/json", + "accepts": "application/json", + } + + def __init__(self, domain=None, username=None, password=None): + self.domain = check_env.check("OPENFIELD_DOMAIN", domain) + self.username = check_env.check("OPENFIELD_USERNAME", username) + self.password = check_env.check("OPENFIELD_PASSWORD", password) + self.conn = self._conn() + + def _conn(self, default_headers=None): + if default_headers is None: + default_headers = self._default_headers + client = requests.Session() + client.auth = (self.username, self.password) + client.headers.update(default_headers) + return client + + def _base_endpoint(self, endpoint, entity_id=None): + # Create the base endpoint URL + + url = f"https://{self.domain}.openfield.ai/api/v1/{endpoint}/" + + if entity_id: + return f"{url}{entity_id}/" + return url + + def _base_get(self, endpoint, entity_id=None, exception_message=None, params=None): + # Make a general GET request + + resp = self.conn.get(self._base_endpoint(endpoint, entity_id), params=params) + if resp.status_code >= 400: + raise Exception(self.parse_error(resp, exception_message)) + return resp.json() + + def _base_post(self, endpoint, data, exception_message=None): + # Make a general POST request + + resp = self.conn.post(self._base_endpoint(endpoint), data=json.dumps(data)) + + if resp.status_code >= 400: + raise Exception(self.parse_error(resp, exception_message)) + + # Not all responses return a json + try: + return resp.json() + + except ValueError: + return None + + def _base_put( + self, + endpoint, + entity_id=None, + data=None, + params=None, + exception_message=None, + ): + # Make a general PUT request + + endpoint = self._base_endpoint(endpoint, entity_id) + + resp = self.conn.put(endpoint, data=json.dumps(data), params=params) + + if resp.status_code >= 400: + raise Exception(self.parse_error(resp, exception_message)) + + # Not all responses return a json + try: + return resp.json() + + except ValueError: + return None + + def _base_delete(self, endpoint, entity_id=None, exception_message=None): + # Make a general DELETE request + + resp = self.conn.delete(self._base_endpoint(endpoint, entity_id)) + + if resp.status_code >= 400: + raise Exception(self.parse_error(resp, exception_message)) + + # Not all responses return a json + try: + return resp.json() + + except ValueError: + return None + + def parse_error(self, resp, exception_message): + """ + Parse error responses from the API + """ + message = f"Status {str(resp.status_code)}" + if exception_message: + message += exception_message + + try: + json = resp.json() + return message + "\n" + str(json) + except ValueError: + return message + + def retrieve_person(self, person_id): + """ + Get a person. + + `Args:` + person_id: int + The id of the record. + `Returns`: + JSON object + """ + + return self._base_get( + endpoint="people", entity_id=person_id, exception_message="Person not found" + ) + + def list_people(self, page=1, page_size=100, search=None, ordering=None, **kwargs): + """ + List people + + `Args:` + page: integer + A page number within the paginated result set. + page_size: integer + Number of results to return per page. + Defaults to 100 + search: string + A search term. + ordering: string + Which field to use when ordering the results. + **kwargs: + Optional arguments to pass to the client. A full list can be found + in the `OpenField API docs + + `Returns:` + Parsons.Table + The people data. + """ + + res = self._base_get( + endpoint="people", + params={ + "page": page, + "page_size": page_size, + "search": search, + "ordering": ordering, + **kwargs, + }, + ) + + return Table(res["results"]) + + def create_person(self, person): + """ + Create a person. + + `Args:` + person: dict + Shape of the record + `Full list of fields `_ + `Returns:` + JSON object + """ + + return self._base_post( + endpoint="people", data=person, exception_message="Could not create person" + ) + + def bulk_upsert_people(self, people): + """ + Given a list of objects, tries the match the object with a provided ID. + Otherwise, creates a new record for a person without an ID match. + + `Args:` + people: list of dicts + List containing the records + `Full list of fields `_ + `Returns:` + Parsons.Table + The people data. + """ + + res = self._base_post( + endpoint="people/bulk-upsert", + data=people, + ) + + return Table(res) + + def update_person(self, person_id, data): + """ + Updates a person. + + `Args:` + person_id: int + The id of the record. + data: dict + Person data to update + `Full list of fields `_ + `Returns:` + JSON object + """ + + return self._base_put(endpoint="people", entity_id=person_id, data=data) + + def destroy_person(self, person_id): + """ + Delete a person. + + `Args:` + person_id: int + The id of the record. + `Returns`: + None + """ + + return self._base_delete( + endpoint="people", entity_id=person_id, exception_message="Person not found" + ) + + def retrieve_label(self, label_id): + """ + Get a label + + `Args:` + label_id: int + The id of the record. + `Returns`: + JSON object + """ + + return self._base_get( + endpoint="labels", entity_id=label_id, exception_message="Label not found" + ) + + def list_labels(self, page=1, page_size=100, search=None, ordering=None, **kwargs): + """ + List labels + + `Args:` + page: integer + A page number within the paginated result set. + page_size: integer + Number of results to return per page. + Defaults to 100 + search: string + A search term. + ordering: string + Which field to use when ordering the results. + **kwargs: + Optional arguments to pass to the client. A full list can be found + in the `OpenField API docs `_ + + `Returns:` + Parsons.Table + The labels data. + """ + + res = self._base_get( + endpoint="labels", + params={ + "page": page, + "page_size": page_size, + "search": search, + "ordering": ordering, + **kwargs, + }, + ) + + return Table(res["results"]) + + def create_label(self, name, description): + """ + Create a label. + + `Args:` + name: string <= 100 characters + label name + description: string <= 255 characters + label description + `Returns:` + JSON object + """ + + return self._base_post( + endpoint="labels", + data={ + "name": name, + "description": description, + }, + exception_message="Could not create label", + ) + + def apply_person_label(self, person_id, label_id): + """ + Apply a label to a person. + + `Args:` + person_id: int + ID of the person + label_id: int + ID of the label + `Returns:` + JSON object + """ + + return self._base_post( + endpoint="people-labels", + data={"person_id": person_id, "label_id": label_id}, + ) + + def bulk_apply_people_labels(self, data): + """ + Bulk apply labels to people. + + `Args:` + data: list of dicts with keys `person_id` and `label_id` + `Returns:` + JSON object + """ + + return self._base_post( + endpoint="people-labels/bulk-upsert", + data=data, + ) + + def remove_person_label(self, person_id, label_id): + """ + Remove a label from a person. + + `Args:` + person_id: int + ID of the person + label_id: int + ID of the label + `Returns:` + JSON object + """ + + return self._base_delete( + endpoint="people-labels", + params={"person_id": person_id, "label_id": label_id}, + ) From 9e807cf041554395d6c381f7efb2a8d3beb2962d Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Sun, 28 Apr 2024 13:05:49 -0500 Subject: [PATCH 02/10] update openfield doc example --- docs/openfield.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/openfield.rst b/docs/openfield.rst index 4f9efb00ff..1de563bb76 100644 --- a/docs/openfield.rst +++ b/docs/openfield.rst @@ -50,16 +50,16 @@ You can then call various endpoints: openfield.create_person(person=person) # Fetch person - user_fields = openfield.get_person(person_id=123) + person = openfield.retrieve_person(person_id=123) # Update person fields data= { - "phone1": '5558765432' + "phone1": '5558765432', } - openfield.update_user(user_id=123, data=data) + updated_person = openfield.update_person(person_id=123, data=data) # Delete person - openfield.destroy_user(user_id=123) + openfield.destroy_person(person_id=123) *** API From 12a4d2ceab9e99dc1813dbd3683aa5ce957128bb Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Sun, 28 Apr 2024 13:42:17 -0500 Subject: [PATCH 03/10] openfield class - fix E501s --- parsons/openfield/openfield.py | 77 ++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/parsons/openfield/openfield.py b/parsons/openfield/openfield.py index 48d6eebc5a..00b6db6e3e 100644 --- a/parsons/openfield/openfield.py +++ b/parsons/openfield/openfield.py @@ -52,10 +52,19 @@ def _base_endpoint(self, endpoint, entity_id=None): return f"{url}{entity_id}/" return url - def _base_get(self, endpoint, entity_id=None, exception_message=None, params=None): + def _base_get( + self, + endpoint, + entity_id=None, + exception_message=None, + params=None, + ): # Make a general GET request - resp = self.conn.get(self._base_endpoint(endpoint, entity_id), params=params) + resp = self.conn.get( + self._base_endpoint(endpoint, entity_id), + params=params, + ) if resp.status_code >= 400: raise Exception(self.parse_error(resp, exception_message)) return resp.json() @@ -63,7 +72,10 @@ def _base_get(self, endpoint, entity_id=None, exception_message=None, params=Non def _base_post(self, endpoint, data, exception_message=None): # Make a general POST request - resp = self.conn.post(self._base_endpoint(endpoint), data=json.dumps(data)) + resp = self.conn.post( + self._base_endpoint(endpoint), + data=json.dumps(data), + ) if resp.status_code >= 400: raise Exception(self.parse_error(resp, exception_message)) @@ -140,10 +152,19 @@ def retrieve_person(self, person_id): """ return self._base_get( - endpoint="people", entity_id=person_id, exception_message="Person not found" + endpoint="people", + entity_id=person_id, + exception_message="Person not found", ) - def list_people(self, page=1, page_size=100, search=None, ordering=None, **kwargs): + def list_people( + self, + page=1, + page_size=100, + search=None, + ordering=None, + **kwargs, + ): """ List people @@ -158,8 +179,9 @@ def list_people(self, page=1, page_size=100, search=None, ordering=None, **kwarg ordering: string Which field to use when ordering the results. **kwargs: - Optional arguments to pass to the client. A full list can be found - in the `OpenField API docs + Optional arguments to pass to the client. A full list can be + found in the `OpenField API docs + `Returns:` Parsons.Table @@ -186,13 +208,16 @@ def create_person(self, person): `Args:` person: dict Shape of the record - `Full list of fields `_ + `Full list of fields + `_ `Returns:` JSON object """ return self._base_post( - endpoint="people", data=person, exception_message="Could not create person" + endpoint="people", + data=person, + exception_message="Could not create person", ) def bulk_upsert_people(self, people): @@ -203,7 +228,8 @@ def bulk_upsert_people(self, people): `Args:` people: list of dicts List containing the records - `Full list of fields `_ + `Full list of fields + `_ `Returns:` Parsons.Table The people data. @@ -225,12 +251,17 @@ def update_person(self, person_id, data): The id of the record. data: dict Person data to update - `Full list of fields `_ + `Full list of fields + `_ `Returns:` JSON object """ - return self._base_put(endpoint="people", entity_id=person_id, data=data) + return self._base_put( + endpoint="people", + entity_id=person_id, + data=data, + ) def destroy_person(self, person_id): """ @@ -244,7 +275,9 @@ def destroy_person(self, person_id): """ return self._base_delete( - endpoint="people", entity_id=person_id, exception_message="Person not found" + endpoint="people", + entity_id=person_id, + exception_message="Person not found", ) def retrieve_label(self, label_id): @@ -259,10 +292,19 @@ def retrieve_label(self, label_id): """ return self._base_get( - endpoint="labels", entity_id=label_id, exception_message="Label not found" + endpoint="labels", + entity_id=label_id, + exception_message="Label not found", ) - def list_labels(self, page=1, page_size=100, search=None, ordering=None, **kwargs): + def list_labels( + self, + page=1, + page_size=100, + search=None, + ordering=None, + **kwargs, + ): """ List labels @@ -277,8 +319,9 @@ def list_labels(self, page=1, page_size=100, search=None, ordering=None, **kwarg ordering: string Which field to use when ordering the results. **kwargs: - Optional arguments to pass to the client. A full list can be found - in the `OpenField API docs `_ + Optional arguments to pass to the client. A full list can be + found in the `OpenField API docs + `_ `Returns:` Parsons.Table From f9e3069784e803680e2818c2d99766ca45b903c6 Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Mon, 29 Apr 2024 10:43:20 -0500 Subject: [PATCH 04/10] add openfield to root __init__ --- parsons/__init__.py | 1 + parsons/openfield/openfield.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/parsons/__init__.py b/parsons/__init__.py index ebc743e37f..4321a6dd64 100644 --- a/parsons/__init__.py +++ b/parsons/__init__.py @@ -71,6 +71,7 @@ ("parsons.mobilize_america.ma", "MobilizeAmerica"), ("parsons.nation_builder.nation_builder", "NationBuilder"), ("parsons.newmode.newmode", "Newmode"), + ("parsons.openfield.openfield", "OpenField"), ("parsons.ngpvan.van", "VAN"), ("parsons.notifications.gmail", "Gmail"), ("parsons.notifications.slack", "Slack"), diff --git a/parsons/openfield/openfield.py b/parsons/openfield/openfield.py index 00b6db6e3e..df19773a50 100644 --- a/parsons/openfield/openfield.py +++ b/parsons/openfield/openfield.py @@ -111,10 +111,19 @@ def _base_put( except ValueError: return None - def _base_delete(self, endpoint, entity_id=None, exception_message=None): + def _base_delete( + self, + endpoint, + entity_id=None, + exception_message=None, + params=None, + ): # Make a general DELETE request - resp = self.conn.delete(self._base_endpoint(endpoint, entity_id)) + resp = self.conn.delete( + self._base_endpoint(endpoint, entity_id), + params=params, + ) if resp.status_code >= 400: raise Exception(self.parse_error(resp, exception_message)) @@ -386,7 +395,7 @@ def bulk_apply_people_labels(self, data): Bulk apply labels to people. `Args:` - data: list of dicts with keys `person_id` and `label_id` + data: list of dicts with keys `people`: int and `label`: int `Returns:` JSON object """ From fa5c2ecb70f85b544f80e3964b89f38b001cf73e Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Mon, 29 Apr 2024 11:12:32 -0500 Subject: [PATCH 05/10] OF - use junction ID for DELETE people-labels --- parsons/openfield/openfield.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/parsons/openfield/openfield.py b/parsons/openfield/openfield.py index df19773a50..39bdb07935 100644 --- a/parsons/openfield/openfield.py +++ b/parsons/openfield/openfield.py @@ -116,14 +116,10 @@ def _base_delete( endpoint, entity_id=None, exception_message=None, - params=None, ): # Make a general DELETE request - resp = self.conn.delete( - self._base_endpoint(endpoint, entity_id), - params=params, - ) + resp = self.conn.delete(self._base_endpoint(endpoint, entity_id)) if resp.status_code >= 400: raise Exception(self.parse_error(resp, exception_message)) @@ -405,20 +401,15 @@ def bulk_apply_people_labels(self, data): data=data, ) - def remove_person_label(self, person_id, label_id): + def remove_person_label(self, junction_id): """ Remove a label from a person. `Args:` - person_id: int - ID of the person - label_id: int - ID of the label + junction_id: int + Primary Key ID of the `people_labels` junction table `Returns:` JSON object """ - return self._base_delete( - endpoint="people-labels", - params={"person_id": person_id, "label_id": label_id}, - ) + return self._base_delete(endpoint="people-labels", entity_id=junction_id) From d7b11ff4787415986e0efd0c6c3bb63154baf8d6 Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Tue, 30 Apr 2024 10:16:53 -0500 Subject: [PATCH 06/10] openfield - create conversation code --- parsons/openfield/openfield.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/parsons/openfield/openfield.py b/parsons/openfield/openfield.py index 39bdb07935..d7c7a0321a 100644 --- a/parsons/openfield/openfield.py +++ b/parsons/openfield/openfield.py @@ -412,4 +412,25 @@ def remove_person_label(self, junction_id): JSON object """ - return self._base_delete(endpoint="people-labels", entity_id=junction_id) + return self._base_delete( + endpoint="people-labels", + entity_id=junction_id, + ) + + def create_conversation_code(self, conversation_code): + """ + Create a conversation code. + + `Args:` + conversation_code: dict + `Full list of fields + `_ + `Returns:` + JSON object + """ + + return self._base_post( + endpoint="conversation-codes", + data=conversation_code, + exception_message="Could not create conversation code", + ) From bf6625ab30e5f1735f428732e09da727a1f0b194 Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Sun, 5 May 2024 13:33:15 -0500 Subject: [PATCH 07/10] openfield - add conversation-codes/people endpoints --- parsons/openfield/openfield.py | 81 +++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/parsons/openfield/openfield.py b/parsons/openfield/openfield.py index d7c7a0321a..3a3faf7d5c 100644 --- a/parsons/openfield/openfield.py +++ b/parsons/openfield/openfield.py @@ -111,15 +111,50 @@ def _base_put( except ValueError: return None + def _base_patch( + self, + endpoint, + entity_id=None, + data=None, + params=None, + exception_message=None, + ): + # Make a general PATCH request + + endpoint = self._base_endpoint(endpoint, entity_id) + + resp = self.conn.patch( + endpoint, + data=json.dumps(data), + params=params, + ) + + if resp.status_code >= 400: + raise Exception(self.parse_error(resp, exception_message)) + + # Not all responses return a json + try: + return resp.json() + + except ValueError: + return None + def _base_delete( self, endpoint, entity_id=None, + data=None, exception_message=None, ): # Make a general DELETE request - resp = self.conn.delete(self._base_endpoint(endpoint, entity_id)) + resp = self.conn.delete( + self._base_endpoint( + endpoint, + entity_id, + data=json.dumps(data) if data else None, + ) + ) if resp.status_code >= 400: raise Exception(self.parse_error(resp, exception_message)) @@ -434,3 +469,47 @@ def create_conversation_code(self, conversation_code): data=conversation_code, exception_message="Could not create conversation code", ) + + def add_people_to_conversation_code( + self, + conversation_code_id, + people_ids, + ): + """ + Adds people to a conversation code. + + `Args:` + conversation_code_id: int + ID of the conversation code + people_ids: list of ints + List of people IDs to add to the conversation code + `Returns:` + JSON object + """ + + return self._base_patch( + endpoint=f"conversation-codes/{conversation_code_id}/people", + data={"people_ids": people_ids}, + ) + + def remove_people_from_conversation_code( + self, + conversation_code_id, + people_ids, + ): + """ + Removes people from a conversation code. + + `Args:` + conversation_code_id: int + ID of the conversation code + people_ids: list of ints + List of people IDs to remove from the conversation code + `Returns:` + JSON object + """ + + return self._base_delete( + endpoint=f"conversation-codes/{conversation_code_id}/people", + data={"people_ids": people_ids}, + ) From 34b41aefd8f5062c1aad7b85f0c15a387cb2d136 Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Tue, 7 May 2024 14:27:17 -0500 Subject: [PATCH 08/10] openfield - fix delete endpoint --- parsons/openfield/openfield.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/parsons/openfield/openfield.py b/parsons/openfield/openfield.py index 3a3faf7d5c..4748064e6a 100644 --- a/parsons/openfield/openfield.py +++ b/parsons/openfield/openfield.py @@ -148,12 +148,11 @@ def _base_delete( ): # Make a general DELETE request + endpoint = self._base_endpoint(endpoint, entity_id) + resp = self.conn.delete( - self._base_endpoint( - endpoint, - entity_id, - data=json.dumps(data) if data else None, - ) + endpoint, + data=json.dumps(data) if data else None, ) if resp.status_code >= 400: From 261eb4e8bc60894bd972070d5f96a12565166038 Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Tue, 14 May 2024 15:55:29 -0500 Subject: [PATCH 09/10] openfield - add update_conversation_code --- parsons/openfield/openfield.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/parsons/openfield/openfield.py b/parsons/openfield/openfield.py index 4748064e6a..1d171bd8cc 100644 --- a/parsons/openfield/openfield.py +++ b/parsons/openfield/openfield.py @@ -469,6 +469,28 @@ def create_conversation_code(self, conversation_code): exception_message="Could not create conversation code", ) + def update_conversation_code(self, conversation_code_id, data): + """ + Update a conversation code. + + `Args:` + person_id: int + The id of the record. + data: dict + Conversation code data to update + `Full list of fields + `_ + `Returns:` + JSON object + """ + + return self._base_put( + endpoint="conversation-codes", + entity_id=conversation_code_id, + data=data, + exception_message="Could not update conversation code", + ) + def add_people_to_conversation_code( self, conversation_code_id, From ac74af96989b5dd0a71eb93bf2c83a9561013179 Mon Sep 17 00:00:00 2001 From: Cody Gordon Date: Tue, 14 May 2024 18:16:03 -0500 Subject: [PATCH 10/10] openfield - use FailureException for errors --- parsons/openfield/openfield.py | 45 ++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/parsons/openfield/openfield.py b/parsons/openfield/openfield.py index 1d171bd8cc..8f3ba7ca06 100644 --- a/parsons/openfield/openfield.py +++ b/parsons/openfield/openfield.py @@ -8,6 +8,21 @@ logger = logging.getLogger(__name__) +class FailureException(Exception): + def __init__(self, resp, message="Error in request"): + self.status_code = resp.status_code + message = f"{resp.status_code} - {message}" + + try: + self.json = resp.json() + except ValueError: + self.json = None + if resp.text: + message = f"{message}\n{resp.text}" + + super(FailureException, self).__init__(message) + + class OpenField: """ Instantiate the OpenField class @@ -66,7 +81,8 @@ def _base_get( params=params, ) if resp.status_code >= 400: - raise Exception(self.parse_error(resp, exception_message)) + raise FailureException(resp, exception_message) + return resp.json() def _base_post(self, endpoint, data, exception_message=None): @@ -78,7 +94,7 @@ def _base_post(self, endpoint, data, exception_message=None): ) if resp.status_code >= 400: - raise Exception(self.parse_error(resp, exception_message)) + raise FailureException(resp, exception_message) # Not all responses return a json try: @@ -102,7 +118,7 @@ def _base_put( resp = self.conn.put(endpoint, data=json.dumps(data), params=params) if resp.status_code >= 400: - raise Exception(self.parse_error(resp, exception_message)) + raise FailureException(resp, exception_message) # Not all responses return a json try: @@ -130,7 +146,7 @@ def _base_patch( ) if resp.status_code >= 400: - raise Exception(self.parse_error(resp, exception_message)) + raise FailureException(resp, exception_message) # Not all responses return a json try: @@ -156,7 +172,7 @@ def _base_delete( ) if resp.status_code >= 400: - raise Exception(self.parse_error(resp, exception_message)) + raise FailureException(resp, exception_message) # Not all responses return a json try: @@ -165,20 +181,6 @@ def _base_delete( except ValueError: return None - def parse_error(self, resp, exception_message): - """ - Parse error responses from the API - """ - message = f"Status {str(resp.status_code)}" - if exception_message: - message += exception_message - - try: - json = resp.json() - return message + "\n" + str(json) - except ValueError: - return message - def retrieve_person(self, person_id): """ Get a person. @@ -272,11 +274,16 @@ def bulk_upsert_people(self, people): `Returns:` Parsons.Table The people data. + If there is an error in any of the rows' columns, you will get + Exception.status_code == 400, and you can use Exception.json attr + to map back to the list you provided and fix any issues in the + columns it has flagged. """ res = self._base_post( endpoint="people/bulk-upsert", data=people, + exception_message="Failed to upsert people, check Exception.json", ) return Table(res)