From 3433986f3efdc8914657b51bea1cd97e94f55dc6 Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Fri, 20 Jan 2023 12:43:22 -0500 Subject: [PATCH 01/12] Fixing a couple issues with the pipeline variable names --- README.md | 2 +- hs_api/.env.template | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a785b7c..ee08dd0 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ pipeline_id = 'my_pipeline_id' client = HubSpotClient( access_token=access_token, - PIPELINE_ID=pipeline_id, + pipeline_id=pipeline_id, ) ``` diff --git a/hs_api/.env.template b/hs_api/.env.template index 42d945b..0087396 100644 --- a/hs_api/.env.template +++ b/hs_api/.env.template @@ -1,5 +1,5 @@ HUBSPOT_ACCESS_TOKEN= -HUBS_PIPELINE_ID= +HUBSPOT_TEST_PIPELINE_ID= HUBSPOT_TEST_ACCESS_TOKEN= -HUBS_TEST_PIPELINE_ID= +HUBSPOT_TEST_PIPELINE_ID= From 9550ecc5d17526bc5801c583c5b26380f736b85f Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Fri, 3 Mar 2023 15:03:03 -0500 Subject: [PATCH 02/12] Wrapping additional API calls. --- hs_api/api/hubspot_api.py | 110 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 13 ++--- setup.py | 2 +- 3 files changed, 118 insertions(+), 7 deletions(-) diff --git a/hs_api/api/hubspot_api.py b/hs_api/api/hubspot_api.py index a795a78..5f8978f 100644 --- a/hs_api/api/hubspot_api.py +++ b/hs_api/api/hubspot_api.py @@ -1,3 +1,4 @@ +from datetime import datetime import time import requests @@ -11,6 +12,8 @@ SimplePublicObjectInput, ) from requests.exceptions import HTTPError +from typing import Dict, Optional +from collections.abc import Generator from hs_api.settings.settings import HUBSPOT_ACCESS_TOKEN, HUBSPOT_PIPELINE_ID @@ -64,6 +67,8 @@ def create_lookup(self): "contact": self._client.crm.contacts.basic_api.create, "company": self._client.crm.companies.basic_api.create, "deal": self._client.crm.deals.basic_api.create, + "ticket": self._client.crm.tickets.basic_api.create, + "email": self._client.crm.objects.emails.basic_api.create, } @property @@ -71,6 +76,7 @@ def search_lookup(self): return { "contact": self._client.crm.contacts.search_api.do_search, "company": self._client.crm.companies.search_api.do_search, + "email": self._client.crm.objects.emails.search_api.do_search, } @property @@ -156,6 +162,29 @@ def find_contact(self, property_name, value): response = self._find("contact", property_name, value, sort) return response.results + def find_contact_iter(self, property_name: str, value: str, limit: int = 20) -> Generator[Dict, None, None]: + """ + Searches for a contact in Hubspot and returns results as a generator + + :param property_name: The field name from Hubspot + :param value: The value to search in the field property_name + :param limit: The number of results to return per iteration + :return: Dictionary of results + """ + sort = [{"propertyName": "hs_object_id", "direction": "ASCENDING"}] + after = 0 + + while True: + response = self._find("contact", property_name, value, sort, limit=limit, after=after) + if not response.results: + break + + yield response.results + + if not response.paging: + break + after = response.paging.next.after + def find_company(self, property_name, value): sort = [{"propertyName": "hs_lastmodifieddate", "direction": "DESCENDING"}] @@ -163,6 +192,29 @@ def find_company(self, property_name, value): response = self._find("company", property_name, value, sort) return response.results + def find_company_iter(self, property_name: str, value: str, limit: int = 20) -> Generator[Dict, None, None]: + """ + Searches for a company in Hubspot and returns results as a generator + + :param property_name: The field name from Hubspot + :param value: The value to search in the field property_name + :param limit: The number of results to return per iteration + :return: Dictionary of results + """ + sort = [{"propertyName": "hs_lastmodifieddate", "direction": "DESCENDING"}] + after = 0 + + while True: + response = self._find("company", property_name, value, sort, limit=limit, after=after) + if not response.results: + break + + yield response.results + + if not response.paging: + break + after = response.paging.next.after + def find_deal(self, property_name, value): pipeline_filter = Filter( property_name="pipeline", operator="EQ", value=self.pipeline_id @@ -195,6 +247,16 @@ def _find_owner_by_id(self, owner_id): response = self._client.crm.owners.owners_api.get_by_id(owner_id=owner_id) return response + def find_all_owners(self): + after = None + while True: + response = self._client.crm.owners.owners_api.get_page(after=after) + yield response + + if not response.paging: + break + after = response.paging.next.after + def find_owner(self, property_name, value): if property_name not in ("id", "email"): raise NameError( @@ -319,6 +381,9 @@ def find_all_tickets( else: after = None + def find_ticket(self, ticket_id): + return self._client.crm.tickets.basic_api.get_by_id(ticket_id) + def find_all_deals( self, filter_name=None, @@ -428,6 +493,37 @@ def create_deal( ) return response + def create_ticket(self, subject, **properties): + properties = dict(subject=subject, **properties) + response = self._create("ticket", properties) + return response + + def create_email(self, hs_timestamp: Optional[datetime] = None, hs_email_direction: Optional[str] = 'EMAIL', + **properties): + """ + See documentation at https://developers.hubspot.com/docs/api/crm/email + + :param hs_timestamp: This field marks the email's time of creation and determines where the email sits on the + record timeline. You can use either a Unix timestamp in milliseconds or UTC format. If not provided, then the + current time is used. + :param hs_email_direction: The direction the email was sent in. Possible values include: + + EMAIL: the email was sent from the CRM or sent and logged to the CRM with the BCC address. + INCOMING_EMAIL: the email was a reply to a logged outgoing email. + + FORWARDED_EMAIL: the email was forwarded to the CRM. + :param properties: Dictionary of properties as documented on hubspot + :return: + """ + if not hs_timestamp: + hs_timestamp = int(datetime.now().timestamp()) + else: + hs_timestamp = int(hs_timestamp.timestamp()) + + properties = dict(hs_timestamp=hs_timestamp, hs_email_direction=hs_email_direction, **properties) + response = self._create("email", properties) + return response + def delete_contact(self, value, property_name=None): try: public_gdpr_delete_input = PublicGdprDeleteInput( @@ -455,6 +551,20 @@ def delete_deal(self, deal_id): except ApiException as e: print(f"Exception when deleting deal: {e}\n") + def delete_ticket(self, ticket_id): + try: + api_response = self._client.crm.tickets.basic_api.archive(ticket_id) + return api_response + except ApiException as e: + print(f"Exception when deleting ticket: {e}\n") + + def delete_email(self, email_id): + try: + api_response = self._client.crm.objects.emails.basic_api.archive(email_id) + return api_response + except ApiException as e: + print(f"Exception when deleting email: {e}\n") + def update_company(self, object_id, **properties): response = self._update("company", object_id, properties) return response diff --git a/requirements.txt b/requirements.txt index e9f6f99..be78bd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ requests -isort==5.10.1 -flake8==4.0.1 -black==22.3.0 -pytest==6.2.5 -python-dotenv==0.19.2 -hubspot-api-client==5.0.1 +isort==5.12.0 +flake8==6.0.0 +black==23.1.0 +pytest==7.2.2 +python-dotenv==1.0.0 +hubspot-api-client==7.5.0 +tenacity==8.2.2 diff --git a/setup.py b/setup.py index 4bd2ece..3e230a1 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,6 @@ description="Superscript Hubspot API", author="Superscript", author_email="paul.lucas@gosuperscript.com", - install_requires=["requests", "python-dotenv==0.19.2", "hubspot-api-client==5.0.1"], + install_requires=["requests", "python-dotenv==0.19.2", "hubspot-api-client==7.5.0"], packages=find_packages(include=["hs_api*"]), ) From 18dc3ff70a093bcb96cadb6b5ed92fa7ce873bcb Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Fri, 3 Mar 2023 15:06:04 -0500 Subject: [PATCH 03/12] Fixing up the tests so they all pass more reliably, and so they work on a clean Hubspot test account --- README.md | 6 +- hs_api/api/hubspot_api.py | 68 ++-- tests/api/test_integration_hubspot_api.py | 359 +++++++++++----------- tests/conftest.py | 122 ++++++++ 4 files changed, 342 insertions(+), 213 deletions(-) create mode 100644 tests/conftest.py diff --git a/README.md b/README.md index ee08dd0..04a0b26 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,9 @@ client = HubSpotClient( You can also set the environment variables `HUBSPOT_ACCESS_TOKEN` and `HUBS_PIPELINE_ID` which will be used as defaults if no access_token or -pipeline_id are passed to the `HubSpotClient`. +pipeline_id are passed to the `HubSpotClient`. This can be done by copying +the .env.template file from `hs_api\.env.template` into the root of the +project and renaming it to .env. More details on how to use the client can be found in the test cases that @@ -77,7 +79,7 @@ normal, which will kick off the github actions to run the linting and tests. Be aware that a couple of the tests can be flakey due to the delay in the asynchronous way hubspot returns results and actually applies them to the -underlying data. There are dealys in place to account for this but there can +underlying data. There are delays in place to account for this but there can be cases where a test fails because a record appears to have not been created. This probably needs reworking, but feel free to re-run the tests. diff --git a/hs_api/api/hubspot_api.py b/hs_api/api/hubspot_api.py index 5f8978f..0ed885e 100644 --- a/hs_api/api/hubspot_api.py +++ b/hs_api/api/hubspot_api.py @@ -1,7 +1,6 @@ from datetime import datetime import time -import requests from hubspot import HubSpot from hubspot.auth.oauth import ApiException from hubspot.crm.contacts import ( @@ -27,7 +26,7 @@ } BATCH_LIMITS = 50 -EMAIL_BATCH_LIMIT = 1000 +EMAIL_BATCH_LIMIT = 10 RETRY_LIMIT = 3 RETRY_WAIT = 60 @@ -118,14 +117,17 @@ def pipeline_details(self, pipeline_id=None, return_all_pipelines=False): pipelines = [x for x in pipelines if x.id == pipeline_id] return pipelines - def _find(self, object_name, property_name, value, sort): - query = Filter(property_name=property_name, operator="EQ", value=value) - filter_groups = [FilterGroup(filters=[query])] + def _find(self, object_name, property_name, value, sort, limit=20, after=0): + filter_groups = None + if property_name and value: + query = Filter(property_name=property_name, operator="EQ", value=value) + filter_groups = [FilterGroup(filters=[query])] public_object_search_request = PublicObjectSearchRequest( - limit=20, + limit=limit, filter_groups=filter_groups, sorts=sort, + after=after, ) response = self.search_lookup[object_name]( @@ -268,7 +270,7 @@ def find_owner(self, property_name, value): if property_name == "email": return self._find_owner_by_email(email=value) - def find_all_email_events(self, filter_name=None, filter_value=None): + def find_all_email_events(self, filter_name=None, filter_value=None, limit=EMAIL_BATCH_LIMIT, **parameters): """ Finds and returns all email events, using the filter name and value as the high watermark for the events to return. If None are provided, it @@ -277,40 +279,31 @@ def find_all_email_events(self, filter_name=None, filter_value=None): This iterates over batches, using the previous batch as the new high watermark for the next batch to be returned until there are no more records or batches to return. - - NOTE: This currently uses the requests library to use the v1 api for the - events as there is currently as per the Hubspot website - https://developers.hubspot.com/docs/api/events/email-analytics. - Once this is released we can transition over to using that. """ + sort = [{"propertyName": "hs_lastmodifieddate", "direction": "DESCENDING"}] retry = 0 - offset = None + after = None while True: try: - params = { - "limit": EMAIL_BATCH_LIMIT, - "offset": offset, - } if filter_name: - params[filter_name] = filter_value - - response = requests.get( - "https://api.hubapi.com/email/public/v1/events", - headers={"Authorization": f"Bearer {self._access_token}"}, - params=params, - ) - response.raise_for_status() - - response_json = response.json() + parameters[filter_name] = filter_value + + resp = self._find("email", + property_name=filter_name, + value=filter_value, + limit=limit, + after=after, + sort=sort, + ) + if not resp.results: + break - yield response_json.get("events", []) + yield resp.results - # Update after to page onto next batch if there is next otherwise break as - # there are no more batches to iterate over. - offset = response_json.get("offset", False) - if not response_json.get("hasMore", False): + if not resp.paging: break - retry = 0 + after = resp.paging.next.after + except HTTPError as e: status_code = e.response.status_code if retry >= RETRY_LIMIT: @@ -439,10 +432,9 @@ def find_all_deals( # Update after to page onto next batch if there is next otherwise break as # there are no more batches to iterate over. - if response.paging: - after = response.paging.next.after - else: - after = None + if not response.paging: + break + after = response.paging.next.after def create_contact(self, email, first_name, last_name, **properties): properties = dict( @@ -598,7 +590,7 @@ def create_association( from_object_id, to_object_type, to_object_id, - get_association_id(from_object_type, to_object_type), + [], ) return result diff --git a/tests/api/test_integration_hubspot_api.py b/tests/api/test_integration_hubspot_api.py index af65e11..d4346b3 100644 --- a/tests/api/test_integration_hubspot_api.py +++ b/tests/api/test_integration_hubspot_api.py @@ -1,7 +1,5 @@ -import datetime -import time - import pytest +from tenacity import retry, stop_after_attempt, wait_fixed from hs_api.api.hubspot_api import EMAIL_BATCH_LIMIT, BATCH_LIMITS, HubSpotClient from hs_api.settings.settings import ( @@ -10,40 +8,6 @@ HUBSPOT_TEST_TICKET_PIPELINE_ID, ) -# Test Pipeline - -current_timestamp = datetime.datetime.now() -UNIQUE_ID = f"{current_timestamp:%Y%m%d%H%M%S%f}" - -TEST_COMPANY_NAME = f"{UNIQUE_ID} company" -TEST_EMAIL = f"{UNIQUE_ID}@email.com" -TEST_EMAIL_CUSTOM_DOMAIN = f"{UNIQUE_ID}@domain{UNIQUE_ID}.ai" -TEST_DEAL_NAME = f"{UNIQUE_ID} deal name" - - -def clear_down_test_objects(client): - companies = client.find_company("name", TEST_COMPANY_NAME) - for company in companies: - client.delete_company(company.id) - - deals = client.find_deal("dealname", TEST_DEAL_NAME) - for deal in deals: - client.delete_deal(deal.id) - - client.delete_contact(value=TEST_EMAIL, property_name="email") - client.delete_contact(value=TEST_EMAIL_CUSTOM_DOMAIN, property_name="email") - - -@pytest.fixture() -def hubspot_client(): - client = HubSpotClient( - access_token=HUBSPOT_TEST_ACCESS_TOKEN, pipeline_id=HUBSPOT_TEST_PIPELINE_ID - ) - try: - yield client - finally: - clear_down_test_objects(client) - def test_pipeline_id_none_raises_value_error(): with pytest.raises(ValueError): @@ -51,247 +15,292 @@ def test_pipeline_id_none_raises_value_error(): client.pipeline_stages -def test_create_and_find_contact(hubspot_client): - - test_first_name = f"{UNIQUE_ID} first name" - test_last_name = f"{UNIQUE_ID} last name" - test_phone = f"{UNIQUE_ID}" +def test_create_and_find_contact(hubspot_client, first_name, last_name, email, phone, company_name): # Assert the contact doesn't already exist - contact = hubspot_client.find_contact("email", TEST_EMAIL) + contact = hubspot_client.find_contact("email", email) assert not contact # Create the contact contact_result = hubspot_client.create_contact( - email=TEST_EMAIL, - first_name=test_first_name, - last_name=test_last_name, - phone=test_phone, - company=TEST_COMPANY_NAME, + email=email, + first_name=first_name, + last_name=last_name, + phone=phone, + company=company_name, ) assert contact_result assert contact_result.id + @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + def _get_contact(): + _contact = hubspot_client.find_contact("hs_object_id", contact_result.id) + assert _contact + # Assert the contact now exists based on previous creation - time.sleep(10) - contact = hubspot_client.find_contact("hs_object_id", contact_result.id) - assert contact + _get_contact() + + +def test_create_and_find_company(hubspot_client, company_name, domain): + # Assert the company doesn't already exist + company = hubspot_client.find_company("name", company_name) + assert not company + # Create the company + company_result = hubspot_client.create_company( + name=company_name, domain=domain + ) -def test_create_and_find_company(hubspot_client): + assert company_result + assert company_result.id - test_domain = f"{UNIQUE_ID}.test" + @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + def _test(): + _company = hubspot_client.find_company("hs_object_id", company_result.id) + assert _company + + # Assert the company now exists based on previous creation + _test() + + +def test_create_and_find_company_iter(hubspot_client, domain, unique_id): + company_name = f"{unique_id} test_create_and_find_company_iter" # Assert the company doesn't already exist - company = hubspot_client.find_company("name", TEST_COMPANY_NAME) - assert not company + with pytest.raises(StopIteration): + next(hubspot_client.find_company_iter("name", company_name)) # Create the company company_result = hubspot_client.create_company( - name=TEST_COMPANY_NAME, domain=test_domain + name=company_name, domain=domain ) assert company_result assert company_result.id + @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + def _test(): + _company = next(hubspot_client.find_company_iter("hs_object_id", company_result.id)) + assert _company + # Assert the company now exists based on previous creation - time.sleep(7) - company = hubspot_client.find_company("hs_object_id", company_result.id) - assert company + _test() -def test_create_contact_and_associated_company_with_auto_created_company( - hubspot_client, -): +def test_create_and_find_ticket(hubspot_client: HubSpotClient, unique_id): + ticket_name = f"{unique_id}0 ticket name" + ticket = hubspot_client.find_all_tickets(filter_name="subject", filter_value=ticket_name) + assert ticket - test_first_name = f"{UNIQUE_ID} first name" - test_last_name = f"{UNIQUE_ID} last name" - test_phone = f"{UNIQUE_ID}" +def test_create_and_find_email(hubspot_client: HubSpotClient): + email_resp = hubspot_client.find_all_email_events() + assert next(email_resp) + + +def test_create_contact_and_associated_company_with_auto_created_company( + hubspot_client, first_name, last_name, email_custom_domain, phone, unique_id, +): + company_name = f"{unique_id} test_create_contact_and_associated_company_with_auto_created_company" # Assert the company and contact don't already exist - company = hubspot_client.find_company("name", TEST_COMPANY_NAME) + company = hubspot_client.find_company("name", company_name) assert not company - contact = hubspot_client.find_contact("email", TEST_EMAIL_CUSTOM_DOMAIN) + email_custom_domain = f"{unique_id}@testcreatecontactandassociatedcompanywithautocreatedcompany.com" + contact = hubspot_client.find_contact("email", email_custom_domain) assert not contact # Create the contact and company result = hubspot_client.create_contact_and_company( - email=TEST_EMAIL_CUSTOM_DOMAIN, - first_name=test_first_name, - last_name=test_last_name, - phone=test_phone, - company=TEST_COMPANY_NAME, + email=email_custom_domain, + first_name=first_name, + last_name=last_name, + phone=phone, + company=company_name, ) assert result assert result["contact"].id assert result["company"].id + @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + def _test(): + _company = hubspot_client.find_company("hs_object_id", result["company"].id) + assert _company + assert _company[0].properties["name"] == company_name + + _contact = hubspot_client.find_contact("email", email_custom_domain) + assert _contact + # Assert the company and contact now exists based on previous creation # and are linked - time.sleep(7) - company = hubspot_client.find_company("hs_object_id", result["company"].id) - assert company - assert company[0].properties["name"] == TEST_COMPANY_NAME - - contact = hubspot_client.find_contact("email", TEST_EMAIL_CUSTOM_DOMAIN) - assert contact + _test() def test_create_contact_and_associated_company_without_auto_created_company( - hubspot_client, + hubspot_client, first_name, last_name, phone, unique_id ): - - test_first_name = f"{UNIQUE_ID} first name" - test_last_name = f"{UNIQUE_ID} last name" - test_phone = f"{UNIQUE_ID}" - # Assert the company and contact don't already exist - company = hubspot_client.find_company("name", TEST_COMPANY_NAME) + company_name = f"{unique_id} test_create_contact_and_associated_company_without_auto_created_company" + company = hubspot_client.find_company("name", company_name) assert not company - contact = hubspot_client.find_contact("email", TEST_EMAIL) + email = f"{unique_id}@testcreatecontactandassociatedcompanywithoutautocreatedcompany.com" + contact = hubspot_client.find_contact("email", email) assert not contact # Create the contact and company result = hubspot_client.create_contact_and_company( - email=TEST_EMAIL, - first_name=test_first_name, - last_name=test_last_name, - phone=test_phone, - company=TEST_COMPANY_NAME, + email=email, + first_name=first_name, + last_name=last_name, + phone=phone, + company=company_name, ) assert result assert result["contact"].id assert result["company"].id - # Assert the company and contact now exists based on previous creation - # and are linked - time.sleep(7) - company = hubspot_client.find_company("hs_object_id", result["company"].id) - assert company - assert company[0].properties["name"] == TEST_COMPANY_NAME + @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + def _test(): + _company = hubspot_client.find_company("hs_object_id", result["company"].id) + assert _company + assert _company[0].properties["name"] == company_name - contact = hubspot_client.find_contact("email", TEST_EMAIL) - assert contact + _contact = hubspot_client.find_contact("email", email) + assert _contact - association = hubspot_client.contact_associations(result["contact"].id, "company") - assert association - assert association[0].id == result["company"].id + _association = hubspot_client.contact_associations(result["contact"].id, "company") + assert _association + assert _association[0].to_object_id == int(result["company"].id) + # Assert the company and contact now exists based on previous creation + # and are linked + _test() -def test_create_and_find_deal(hubspot_client): +def test_create_and_find_deal(hubspot_client, unique_id): test_amount = 99.99 # Assert the deal doesn't already exist - deal = hubspot_client.find_deal("dealname", TEST_DEAL_NAME) + deal_name = f"{unique_id} test_create_and_find_deal" + deal = hubspot_client.find_deal("dealname", deal_name) assert not deal # Create the deal deal_result = hubspot_client.create_deal( - name=TEST_DEAL_NAME, + name=deal_name, amount=test_amount, ) assert deal_result assert deal_result.id - # Assert the deal now exists based on previous creation - time.sleep(7) - deal = hubspot_client.find_deal("dealname", TEST_DEAL_NAME) - assert deal + @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + def _test(): + _deal = hubspot_client.find_deal("dealname", deal_name) + assert _deal + # Assert the deal now exists based on previous creation + _test() -def test_create_deal_for_company(hubspot_client): +def test_create_deal_for_company(hubspot_client, unique_id): test_amount = 99.99 # Assert the deal and company don't already exist - deal = hubspot_client.find_deal("dealname", TEST_DEAL_NAME) + deal_name = f"{unique_id} test_create_deal_for_company" + deal = hubspot_client.find_deal("dealname", deal_name) assert not deal - company = hubspot_client.find_company("name", TEST_COMPANY_NAME) + company_name = f"{unique_id} test_create_deal_for_company" + company = hubspot_client.find_company("name", company_name) assert not company # Create the company - company_result = hubspot_client.create_company(name=TEST_COMPANY_NAME) + company_result = hubspot_client.create_company(name=company_name) # Create the deal deal_result = hubspot_client.create_deal( - name=TEST_DEAL_NAME, amount=test_amount, company_id=company_result.id + name=deal_name, amount=test_amount, company_id=company_result.id ) assert deal_result assert deal_result.id - # Assert the company and deal now exists based on previous creation - # and are linked - time.sleep(7) - deal = hubspot_client.find_deal("dealname", TEST_DEAL_NAME) - assert deal + @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + def _test(): + _deal = hubspot_client.find_deal("dealname", deal_name) + assert _deal - company = hubspot_client.find_company("hs_object_id", company_result.id) - assert company + _company = hubspot_client.find_company("hs_object_id", company_result.id) + assert _company - association = hubspot_client.deal_associations(deal_result.id, "company") - assert association - assert association[0].id == company_result.id + _association = hubspot_client.deal_associations(deal_result.id, "company") + assert _association + assert _association[0].to_object_id == int(company_result.id) + # Assert the company and deal now exists based on previous creation + # and are linked + _test() -def test_create_deal_for_contact(hubspot_client): - test_first_name = f"{UNIQUE_ID} first name" - test_last_name = f"{UNIQUE_ID} last name" - test_phone = f"{UNIQUE_ID}" +def test_create_deal_for_contact(hubspot_client, first_name, last_name, phone, company_name, unique_id): test_amount = 99.99 # Assert the deal and company don't already exist - deal = hubspot_client.find_deal("dealname", TEST_DEAL_NAME) + deal_name = f"{unique_id} test_create_deal_for_contact" + deal = hubspot_client.find_deal("dealname", deal_name) assert not deal - contact = hubspot_client.find_contact("email", TEST_EMAIL) + email = f"{unique_id}@testcreatedealforcontact.com" + contact = hubspot_client.find_contact("email", email) assert not contact # Create the contact contact_result = hubspot_client.create_contact( - email=TEST_EMAIL, - first_name=test_first_name, - last_name=test_last_name, - phone=test_phone, - company=TEST_COMPANY_NAME, + email=email, + first_name=first_name, + last_name=last_name, + phone=phone, + company=company_name, ) # Create the deal deal_result = hubspot_client.create_deal( - name=TEST_DEAL_NAME, amount=test_amount, contact_id=contact_result.id + name=deal_name, amount=test_amount, contact_id=contact_result.id ) assert deal_result assert deal_result.id - # Assert the company and deal now exists based on previous creation - # and are linked - time.sleep(7) - deal = hubspot_client.find_deal("dealname", TEST_DEAL_NAME) - assert deal + @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + def _test(): + _deal = hubspot_client.find_deal("dealname", deal_name) + assert _deal - company = hubspot_client.find_contact("hs_object_id", contact_result.id) - assert company + _company = hubspot_client.find_contact("hs_object_id", contact_result.id) + assert _company - association = hubspot_client.deal_associations(deal_result.id, "contact") - assert association - assert association[0].id == contact_result.id + _association = hubspot_client.deal_associations(deal_result.id, "contact") + assert _association + assert _association[0].to_object_id == int(contact_result.id) + + # Assert the company and deal now exists based on previous creation + # and are linked + _test() def test_find_owner_by_email(hubspot_client): - # This test relies on owner_id "49185288" existing in the testing environment. - owner = hubspot_client.find_owner("email", "lovely-whole.abcaebiz@mailosaur.io") + owners = next(hubspot_client.find_all_owners()) + oid = owners.results[0].id + oemail = owners.results[0].email + + owner = hubspot_client.find_owner("email", oemail) assert owner - assert owner.id == "49185288" + assert owner.id == oid def test_find_owner_not_found_returns_none(hubspot_client): @@ -300,10 +309,13 @@ def test_find_owner_not_found_returns_none(hubspot_client): def test_find_owner_by_id(hubspot_client): - # This test relies on owner_id "49185288" existing in the testing environment. - owner = hubspot_client.find_owner("id", "49185288") + owners = next(hubspot_client.find_all_owners()) + oid = owners.results[0].id + oemail = owners.results[0].email + + owner = hubspot_client.find_owner("id", oid) assert owner - assert owner.email == "lovely-whole.abcaebiz@mailosaur.io" + assert owner.email == oemail def test_find_owner_without_id_or_email(hubspot_client): @@ -311,21 +323,24 @@ def test_find_owner_without_id_or_email(hubspot_client): hubspot_client.find_owner("some_id", "some_value") -def test_find_all_tickets_returns_batches(hubspot_client): - tickets = hubspot_client.find_all_tickets() +def test_find_all_tickets_returns_batches(hubspot_client: HubSpotClient): + @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + def _test(): + tickets = hubspot_client.find_all_tickets() - # Assert that the first batch contains the limit of records - # for a batch - initial_batch = next(tickets) - assert len(initial_batch) == BATCH_LIMITS + # Assert that the first batch contains the limit of records + # for a batch + initial_batch = next(tickets) + assert len(initial_batch) == BATCH_LIMITS - following_batch = next(tickets) + following_batch = next(tickets) - # Assert that the next batch follows on from the previous - assert following_batch[0].updated_at > initial_batch[-1].updated_at + # Assert that the next batch follows on from the previous + assert following_batch[0].updated_at > initial_batch[-1].updated_at + _test() -def test_find_all_tickets_returns_default_properties(hubspot_client): +def test_find_all_tickets_returns_default_properties(hubspot_client: HubSpotClient): tickets = hubspot_client.find_all_tickets() actual = next(tickets)[0].properties expected = { @@ -344,7 +359,7 @@ def test_find_all_tickets_returns_default_properties(hubspot_client): assert actual.keys() == expected.keys() -def test_find_all_tickets_returns_given_properties(hubspot_client): +def test_find_all_tickets_returns_given_properties(hubspot_client: HubSpotClient): tickets = hubspot_client.find_all_tickets( properties=["hs_lastmodifieddate", "hs_object_id"] ) @@ -360,7 +375,7 @@ def test_find_all_tickets_returns_given_properties(hubspot_client): assert actual.keys() == expected.keys() -def test_find_all_tickets_returns_after_given_hs_lastmodifieddate(hubspot_client): +def test_find_all_tickets_returns_after_given_hs_lastmodifieddate(hubspot_client: HubSpotClient): all_tickets = hubspot_client.find_all_tickets() filter_value = next(all_tickets)[0].updated_at filtered_tickets = hubspot_client.find_all_tickets( @@ -373,7 +388,7 @@ def test_find_all_tickets_returns_after_given_hs_lastmodifieddate(hubspot_client assert next(filtered_tickets)[0].updated_at > filter_value -def test_find_all_tickets_returns_after_given_hs_object_id(hubspot_client): +def test_find_all_tickets_returns_after_given_hs_object_id(hubspot_client: HubSpotClient): all_tickets = hubspot_client.find_all_tickets() filter_value = next(all_tickets)[0].id filtered_tickets = hubspot_client.find_all_tickets( @@ -386,7 +401,7 @@ def test_find_all_tickets_returns_after_given_hs_object_id(hubspot_client): assert next(filtered_tickets)[0].id > filter_value -def test_find_all_tickets_returns_for_given_pipeline_id(hubspot_client): +def test_find_all_tickets_returns_for_given_pipeline_id(hubspot_client: HubSpotClient): all_tickets = hubspot_client.find_all_tickets( pipeline_id=HUBSPOT_TEST_TICKET_PIPELINE_ID ) @@ -515,11 +530,9 @@ def test_find_all_email_events_returns_batches(hubspot_client): def test_find_all_email_events_returns_after_given_starttimestamp_epoch(hubspot_client): all_events = hubspot_client.find_all_email_events() - filter_value = next(all_events)[-1]["created"] - filtered_events = hubspot_client.find_all_email_events( - filter_name="startTimestamp", - filter_value=filter_value + 1, - ) + filter_value = next(all_events)[-1].created_at + parameters = dict(created_after=filter_value.timestamp()+1) + filtered_events = hubspot_client.find_all_email_events(**parameters) # Assert that the first record of the returned filtered list starts # after the original returned list - assert next(filtered_events)[0]["created"] > filter_value + assert next(filtered_events)[0].created_at > filter_value diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e4b9dc0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,122 @@ +import pytest +from hs_api.api.hubspot_api import HubSpotClient +import datetime + +from hs_api.settings.settings import ( + HUBSPOT_TEST_ACCESS_TOKEN, + HUBSPOT_TEST_PIPELINE_ID, + HUBSPOT_TEST_TICKET_PIPELINE_ID, +) +from hs_api.api.hubspot_api import BATCH_LIMITS, EMAIL_BATCH_LIMIT + + +@pytest.fixture(scope="session") +def hubspot_client(deal_name, company_name, email, email_custom_domain, unique_id): + client = HubSpotClient(access_token=HUBSPOT_TEST_ACCESS_TOKEN, pipeline_id=HUBSPOT_TEST_PIPELINE_ID) + test_deal = None + test_tickets = None + test_emails = [] + + try: + # create Hubspot elements + test_deal = client.create_deal(deal_name) + test_tickets = create_tickets(client, unique_id, BATCH_LIMITS+1) + test_emails = create_emails(client, EMAIL_BATCH_LIMIT+1) + + yield client + + finally: + # clean up all created elements + if test_deal: + client.delete_deal(test_deal.id) + + if test_tickets: + for tid in test_tickets: + hubspot_client.delete_ticket(tid) + + for tid in test_emails: + client.delete_email(tid) + + clear_down_test_objects(client, company_name, deal_name, email, email_custom_domain) + + +def create_emails(client, quantity): + test_emails = [] + for _ in range(quantity): + test_emails.append(client.create_email().id) + return test_emails + + +def create_tickets(client, unique_id, quantity): + ticket_ids = [] + for i in range(quantity): + test_ticket_name = f"{unique_id}{i} ticket name" + properties = dict(subject=test_ticket_name, + hs_pipeline=HUBSPOT_TEST_TICKET_PIPELINE_ID, + hs_pipeline_stage=1, + hs_ticket_priority="HIGH", + ) + + ticket_result = client.create_ticket(**properties) + assert ticket_result + assert ticket_result.id + return ticket_ids + + +def clear_down_test_objects(client, company_name, deal_name, email, email_custom_domain): + companies = client.find_company("name", company_name) + for company in companies: + client.delete_company(company.id) + + deals = client.find_deal("dealname", deal_name) + for deal in deals: + client.delete_deal(deal.id) + + client.delete_contact(value=email, property_name="email") + client.delete_contact(value=email_custom_domain, property_name="email") + + +@pytest.fixture(scope="session") +def unique_id(): + current_timestamp = datetime.datetime.now() + return f"{current_timestamp:%Y%m%d%H%M%S%f}" + + +@pytest.fixture(scope="session") +def company_name(unique_id): + return f"{unique_id} company" + + +@pytest.fixture(scope="session") +def email(unique_id): + return f"{unique_id}@email.com" + + +@pytest.fixture(scope="session") +def email_custom_domain(unique_id): + return f"{unique_id}@domain{unique_id}.ai" + + +@pytest.fixture(scope="session") +def domain(unique_id): + return f"{unique_id}.test" + + +@pytest.fixture(scope="session") +def deal_name(unique_id): + return f"{unique_id} deal name" + + +@pytest.fixture(scope="session") +def first_name(unique_id): + return f"{unique_id} first name" + + +@pytest.fixture(scope="session") +def last_name(unique_id): + return f"{unique_id} last name" + + +@pytest.fixture(scope="session") +def phone(unique_id): + return f"{unique_id}" From 0f78f7c16c6674d259ddbd545ae42b8fed9eab3f Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Fri, 3 Mar 2023 15:23:51 -0500 Subject: [PATCH 04/12] Linter issues --- hs_api/api/hubspot_api.py | 57 +++++++++++++++-------- tests/api/test_integration_hubspot_api.py | 53 ++++++++++++++------- tests/conftest.py | 33 +++++++------ 3 files changed, 92 insertions(+), 51 deletions(-) diff --git a/hs_api/api/hubspot_api.py b/hs_api/api/hubspot_api.py index 0ed885e..b4c28d0 100644 --- a/hs_api/api/hubspot_api.py +++ b/hs_api/api/hubspot_api.py @@ -1,5 +1,7 @@ -from datetime import datetime import time +from collections.abc import Generator +from datetime import datetime +from typing import Dict, Optional from hubspot import HubSpot from hubspot.auth.oauth import ApiException @@ -11,8 +13,6 @@ SimplePublicObjectInput, ) from requests.exceptions import HTTPError -from typing import Dict, Optional -from collections.abc import Generator from hs_api.settings.settings import HUBSPOT_ACCESS_TOKEN, HUBSPOT_PIPELINE_ID @@ -158,13 +158,14 @@ def _update(self, object_name, object_id, properties): print(f"Exception when updating {object_name}: {e}\n") def find_contact(self, property_name, value): - sort = [{"propertyName": "hs_object_id", "direction": "ASCENDING"}] response = self._find("contact", property_name, value, sort) return response.results - def find_contact_iter(self, property_name: str, value: str, limit: int = 20) -> Generator[Dict, None, None]: + def find_contact_iter( + self, property_name: str, value: str, limit: int = 20 + ) -> Generator[Dict, None, None]: """ Searches for a contact in Hubspot and returns results as a generator @@ -177,7 +178,9 @@ def find_contact_iter(self, property_name: str, value: str, limit: int = 20) -> after = 0 while True: - response = self._find("contact", property_name, value, sort, limit=limit, after=after) + response = self._find( + "contact", property_name, value, sort, limit=limit, after=after + ) if not response.results: break @@ -188,13 +191,14 @@ def find_contact_iter(self, property_name: str, value: str, limit: int = 20) -> after = response.paging.next.after def find_company(self, property_name, value): - sort = [{"propertyName": "hs_lastmodifieddate", "direction": "DESCENDING"}] response = self._find("company", property_name, value, sort) return response.results - def find_company_iter(self, property_name: str, value: str, limit: int = 20) -> Generator[Dict, None, None]: + def find_company_iter( + self, property_name: str, value: str, limit: int = 20 + ) -> Generator[Dict, None, None]: """ Searches for a company in Hubspot and returns results as a generator @@ -207,7 +211,9 @@ def find_company_iter(self, property_name: str, value: str, limit: int = 20) -> after = 0 while True: - response = self._find("company", property_name, value, sort, limit=limit, after=after) + response = self._find( + "company", property_name, value, sort, limit=limit, after=after + ) if not response.results: break @@ -270,7 +276,9 @@ def find_owner(self, property_name, value): if property_name == "email": return self._find_owner_by_email(email=value) - def find_all_email_events(self, filter_name=None, filter_value=None, limit=EMAIL_BATCH_LIMIT, **parameters): + def find_all_email_events( + self, filter_name=None, filter_value=None, limit=EMAIL_BATCH_LIMIT, **parameters + ): """ Finds and returns all email events, using the filter name and value as the high watermark for the events to return. If None are provided, it @@ -288,13 +296,14 @@ def find_all_email_events(self, filter_name=None, filter_value=None, limit=EMAIL if filter_name: parameters[filter_name] = filter_value - resp = self._find("email", - property_name=filter_name, - value=filter_value, - limit=limit, - after=after, - sort=sort, - ) + resp = self._find( + "email", + property_name=filter_name, + value=filter_value, + limit=limit, + after=after, + sort=sort, + ) if not resp.results: break @@ -490,8 +499,12 @@ def create_ticket(self, subject, **properties): response = self._create("ticket", properties) return response - def create_email(self, hs_timestamp: Optional[datetime] = None, hs_email_direction: Optional[str] = 'EMAIL', - **properties): + def create_email( + self, + hs_timestamp: Optional[datetime] = None, + hs_email_direction: Optional[str] = "EMAIL", + **properties, + ): """ See documentation at https://developers.hubspot.com/docs/api/crm/email @@ -512,7 +525,11 @@ def create_email(self, hs_timestamp: Optional[datetime] = None, hs_email_directi else: hs_timestamp = int(hs_timestamp.timestamp()) - properties = dict(hs_timestamp=hs_timestamp, hs_email_direction=hs_email_direction, **properties) + properties = dict( + hs_timestamp=hs_timestamp, + hs_email_direction=hs_email_direction, + **properties, + ) response = self._create("email", properties) return response diff --git a/tests/api/test_integration_hubspot_api.py b/tests/api/test_integration_hubspot_api.py index d4346b3..7a3e7fc 100644 --- a/tests/api/test_integration_hubspot_api.py +++ b/tests/api/test_integration_hubspot_api.py @@ -1,7 +1,7 @@ import pytest from tenacity import retry, stop_after_attempt, wait_fixed -from hs_api.api.hubspot_api import EMAIL_BATCH_LIMIT, BATCH_LIMITS, HubSpotClient +from hs_api.api.hubspot_api import BATCH_LIMITS, EMAIL_BATCH_LIMIT, HubSpotClient from hs_api.settings.settings import ( HUBSPOT_TEST_ACCESS_TOKEN, HUBSPOT_TEST_PIPELINE_ID, @@ -15,8 +15,9 @@ def test_pipeline_id_none_raises_value_error(): client.pipeline_stages -def test_create_and_find_contact(hubspot_client, first_name, last_name, email, phone, company_name): - +def test_create_and_find_contact( + hubspot_client, first_name, last_name, email, phone, company_name +): # Assert the contact doesn't already exist contact = hubspot_client.find_contact("email", email) assert not contact @@ -48,9 +49,7 @@ def test_create_and_find_company(hubspot_client, company_name, domain): assert not company # Create the company - company_result = hubspot_client.create_company( - name=company_name, domain=domain - ) + company_result = hubspot_client.create_company(name=company_name, domain=domain) assert company_result assert company_result.id @@ -72,16 +71,16 @@ def test_create_and_find_company_iter(hubspot_client, domain, unique_id): next(hubspot_client.find_company_iter("name", company_name)) # Create the company - company_result = hubspot_client.create_company( - name=company_name, domain=domain - ) + company_result = hubspot_client.create_company(name=company_name, domain=domain) assert company_result assert company_result.id @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) def _test(): - _company = next(hubspot_client.find_company_iter("hs_object_id", company_result.id)) + _company = next( + hubspot_client.find_company_iter("hs_object_id", company_result.id) + ) assert _company # Assert the company now exists based on previous creation @@ -90,7 +89,9 @@ def _test(): def test_create_and_find_ticket(hubspot_client: HubSpotClient, unique_id): ticket_name = f"{unique_id}0 ticket name" - ticket = hubspot_client.find_all_tickets(filter_name="subject", filter_value=ticket_name) + ticket = hubspot_client.find_all_tickets( + filter_name="subject", filter_value=ticket_name + ) assert ticket @@ -100,14 +101,21 @@ def test_create_and_find_email(hubspot_client: HubSpotClient): def test_create_contact_and_associated_company_with_auto_created_company( - hubspot_client, first_name, last_name, email_custom_domain, phone, unique_id, + hubspot_client, + first_name, + last_name, + email_custom_domain, + phone, + unique_id, ): company_name = f"{unique_id} test_create_contact_and_associated_company_with_auto_created_company" # Assert the company and contact don't already exist company = hubspot_client.find_company("name", company_name) assert not company - email_custom_domain = f"{unique_id}@testcreatecontactandassociatedcompanywithautocreatedcompany.com" + email_custom_domain = ( + f"{unique_id}@testcreatecontactandassociatedcompanywithautocreatedcompany.com" + ) contact = hubspot_client.find_contact("email", email_custom_domain) assert not contact @@ -172,7 +180,9 @@ def _test(): _contact = hubspot_client.find_contact("email", email) assert _contact - _association = hubspot_client.contact_associations(result["contact"].id, "company") + _association = hubspot_client.contact_associations( + result["contact"].id, "company" + ) assert _association assert _association[0].to_object_id == int(result["company"].id) @@ -247,7 +257,9 @@ def _test(): _test() -def test_create_deal_for_contact(hubspot_client, first_name, last_name, phone, company_name, unique_id): +def test_create_deal_for_contact( + hubspot_client, first_name, last_name, phone, company_name, unique_id +): test_amount = 99.99 # Assert the deal and company don't already exist @@ -337,6 +349,7 @@ def _test(): # Assert that the next batch follows on from the previous assert following_batch[0].updated_at > initial_batch[-1].updated_at + _test() @@ -375,7 +388,9 @@ def test_find_all_tickets_returns_given_properties(hubspot_client: HubSpotClient assert actual.keys() == expected.keys() -def test_find_all_tickets_returns_after_given_hs_lastmodifieddate(hubspot_client: HubSpotClient): +def test_find_all_tickets_returns_after_given_hs_lastmodifieddate( + hubspot_client: HubSpotClient, +): all_tickets = hubspot_client.find_all_tickets() filter_value = next(all_tickets)[0].updated_at filtered_tickets = hubspot_client.find_all_tickets( @@ -388,7 +403,9 @@ def test_find_all_tickets_returns_after_given_hs_lastmodifieddate(hubspot_client assert next(filtered_tickets)[0].updated_at > filter_value -def test_find_all_tickets_returns_after_given_hs_object_id(hubspot_client: HubSpotClient): +def test_find_all_tickets_returns_after_given_hs_object_id( + hubspot_client: HubSpotClient, +): all_tickets = hubspot_client.find_all_tickets() filter_value = next(all_tickets)[0].id filtered_tickets = hubspot_client.find_all_tickets( @@ -531,7 +548,7 @@ def test_find_all_email_events_returns_batches(hubspot_client): def test_find_all_email_events_returns_after_given_starttimestamp_epoch(hubspot_client): all_events = hubspot_client.find_all_email_events() filter_value = next(all_events)[-1].created_at - parameters = dict(created_after=filter_value.timestamp()+1) + parameters = dict(created_after=filter_value.timestamp() + 1) filtered_events = hubspot_client.find_all_email_events(**parameters) # Assert that the first record of the returned filtered list starts # after the original returned list diff --git a/tests/conftest.py b/tests/conftest.py index e4b9dc0..86bd204 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,20 @@ -import pytest -from hs_api.api.hubspot_api import HubSpotClient import datetime +import pytest + +from hs_api.api.hubspot_api import BATCH_LIMITS, EMAIL_BATCH_LIMIT, HubSpotClient from hs_api.settings.settings import ( HUBSPOT_TEST_ACCESS_TOKEN, HUBSPOT_TEST_PIPELINE_ID, HUBSPOT_TEST_TICKET_PIPELINE_ID, ) -from hs_api.api.hubspot_api import BATCH_LIMITS, EMAIL_BATCH_LIMIT @pytest.fixture(scope="session") def hubspot_client(deal_name, company_name, email, email_custom_domain, unique_id): - client = HubSpotClient(access_token=HUBSPOT_TEST_ACCESS_TOKEN, pipeline_id=HUBSPOT_TEST_PIPELINE_ID) + client = HubSpotClient( + access_token=HUBSPOT_TEST_ACCESS_TOKEN, pipeline_id=HUBSPOT_TEST_PIPELINE_ID + ) test_deal = None test_tickets = None test_emails = [] @@ -20,8 +22,8 @@ def hubspot_client(deal_name, company_name, email, email_custom_domain, unique_i try: # create Hubspot elements test_deal = client.create_deal(deal_name) - test_tickets = create_tickets(client, unique_id, BATCH_LIMITS+1) - test_emails = create_emails(client, EMAIL_BATCH_LIMIT+1) + test_tickets = create_tickets(client, unique_id, BATCH_LIMITS + 1) + test_emails = create_emails(client, EMAIL_BATCH_LIMIT + 1) yield client @@ -37,7 +39,9 @@ def hubspot_client(deal_name, company_name, email, email_custom_domain, unique_i for tid in test_emails: client.delete_email(tid) - clear_down_test_objects(client, company_name, deal_name, email, email_custom_domain) + clear_down_test_objects( + client, company_name, deal_name, email, email_custom_domain + ) def create_emails(client, quantity): @@ -51,11 +55,12 @@ def create_tickets(client, unique_id, quantity): ticket_ids = [] for i in range(quantity): test_ticket_name = f"{unique_id}{i} ticket name" - properties = dict(subject=test_ticket_name, - hs_pipeline=HUBSPOT_TEST_TICKET_PIPELINE_ID, - hs_pipeline_stage=1, - hs_ticket_priority="HIGH", - ) + properties = dict( + subject=test_ticket_name, + hs_pipeline=HUBSPOT_TEST_TICKET_PIPELINE_ID, + hs_pipeline_stage=1, + hs_ticket_priority="HIGH", + ) ticket_result = client.create_ticket(**properties) assert ticket_result @@ -63,7 +68,9 @@ def create_tickets(client, unique_id, quantity): return ticket_ids -def clear_down_test_objects(client, company_name, deal_name, email, email_custom_domain): +def clear_down_test_objects( + client, company_name, deal_name, email, email_custom_domain +): companies = client.find_company("name", company_name) for company in companies: client.delete_company(company.id) From 25d3dc295fbad870a4865d39de04239b32bb0ad9 Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Fri, 3 Mar 2023 15:46:54 -0500 Subject: [PATCH 05/12] Switching from makefile to tox --- .coveragerc | 29 +++++++ .github/workflows/pr_check.yml | 138 ++++++++++++++++++++++++--------- .pre-commit-config.yaml | 62 +++++++++++++++ requirements.txt | 9 --- setup.cfg | 133 ++++++++++++++++++++++++++++++- setup.py | 30 ++++--- tox.ini | 93 ++++++++++++++++++++++ 7 files changed, 435 insertions(+), 59 deletions(-) create mode 100644 .coveragerc create mode 100644 .pre-commit-config.yaml delete mode 100644 requirements.txt create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..17d9600 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,29 @@ +# .coveragerc to control coverage.py +[run] +branch = True +source = hubspot-api +# omit = bad_file.py + +[paths] +source = + src/ + */site-packages/ + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: +fail_under = 85 diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index 228ee87..b1b0a6a 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -1,49 +1,115 @@ -# This is a basic workflow to help you get started with Actions +# GitHub Actions configuration -name: Pull Request Check +name: tests -# Controls when the workflow will run on: - pull_request: - branches: [ "main" ] + push: + # Avoid using all the resources/limits available by checking only + # relevant branches and tags. Other branches can be checked via PRs. + branches: [main] + tags: ['v[0-9]*', '[0-9]+.[0-9]+*'] # Match tags that resemble a version + pull_request: # Run in every PR + workflow_dispatch: # Allow manually triggering the workflow + schedule: + # Run roughly every 15 days at 00:00 UTC + # (useful to check if updates on dependencies break the package) + - cron: '0 0 1,16 * *' - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: +concurrency: + group: >- + ${{ github.workflow }}-${{ github.ref_type }}- + ${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on + prepare: runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job + outputs: + wheel-distribution: ${{ steps.wheel-distribution.outputs.path }} steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 + with: {fetch-depth: 0} # deep clone for setuptools-scm + - uses: actions/setup-python@v4 + with: {python-version: "3.11"} + - name: Run static analysis and format checkers + run: pipx run pre-commit run --all-files --show-diff-on-failure + - name: Build package distribution files + run: pipx run tox -e clean,build + - name: Record the path of wheel distribution + id: wheel-distribution + run: echo "path=$(ls dist/*.whl)" >> $GITHUB_OUTPUT + - name: Store the distribution files for use in other stages + # `tests` and `publish` will use the same pre-built distributions, + # so we make sure to release the exact same package that was tested + uses: actions/upload-artifact@v3 + with: + name: python-distribution-files + path: dist/ + retention-days: 1 - # setup env - - name: setup env - run: | - pip install --upgrade pip - pip install -r requirements.txt - - # Run linting - - name: run linting - run: flake8 --config=setup.cfg - - # Run black - - name: run black - run: black ./ --verbose + test: + needs: prepare + strategy: + matrix: + python: + - "3.7" # oldest Python supported by PSF + - "3.11" # newest Python that is stable + platform: + - ubuntu-latest + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: Retrieve pre-built distribution files + uses: actions/download-artifact@v3 + with: {name: python-distribution-files, path: dist/} + - name: Run tests + env: + HUBSPOT_TOKEN: ${{ secrets.HUBSPOT_TOKEN }} + HUBSPOT_PIPELINE_ID: ${{ secrets.HUBSPOT_PIPELINE_ID }} + run: >- + pipx run tox + --installpkg '${{ needs.prepare.outputs.wheel-distribution }}' + -- -rFEx --durations 10 --color yes + - name: Generate coverage report + run: pipx run coverage lcov -o coverage.lcov + - name: Upload partial coverage report + uses: coverallsapp/github-action@master + with: + path-to-lcov: coverage.lcov + github-token: ${{ secrets.github_token }} + flag-name: ${{ matrix.platform }} - py${{ matrix.python }} + parallel: true - # Run isort - - name: run isort - run: isort + finalize: + needs: test + runs-on: ubuntu-latest + steps: + - name: Finalize coverage report + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true - # Runs a set of commands using the runners shell - - name: Run pytests + publish: + needs: finalize + if: ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags/') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: {python-version: "3.11"} + - name: Retrieve pre-built distribution files + uses: actions/download-artifact@v3 + with: {name: python-distribution-files, path: dist/} + - name: Publish Package env: - HUBSPOT_TEST_ACCESS_TOKEN: ${{ secrets.HUBSPOT_TEST_ACCESS_TOKEN }} - HUBSPOT_TEST_PIPELINE_ID: ${{ secrets.HUBSPOT_TEST_PIPELINE_ID }} - HUBSPOT_TEST_TICKET_PIPELINE_ID: ${{ secrets.HUBSPOT_TEST_TICKET_PIPELINE_ID }} - run: python -m pytest + # Set your PYPI_TOKEN as a secret using GitHub UI + # - https://pypi.org/help/#apitoken + # - https://docs.github.com/en/actions/security-guides/encrypted-secrets + TWINE_REPOSITORY: pypi + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: pipx run tox -e publish diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..807df46 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,62 @@ +exclude: '^docs/conf.py' + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + exclude: (districts1822.csv|races_full_tables_2018-2022.zip) + - id: check-ast + - id: check-json + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: mixed-line-ending + args: ['--fix=auto'] # replace 'auto' with 'lf' to enforce Linux/Mac line endings or 'crlf' for Windows + +# If you want to avoid flake8 errors due to unused vars or imports: +- repo: https://github.com/myint/autoflake + rev: v2.0.1 + hooks: + - id: autoflake + args: [ + --in-place, + --remove-all-unused-imports, + --remove-unused-variables, + ] + +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + +- repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black + language_version: python3 + +# If like to embrace black styles even in the docs: +- repo: https://github.com/asottile/blacken-docs + rev: v1.13.0 + hooks: + - id: blacken-docs + additional_dependencies: [black] + +- repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + additional_dependencies: [flake8-bugbear] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.0.1 + hooks: + - id: mypy + additional_dependencies: + - types-requests + - types-python-dateutil diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index be78bd4..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -requests -isort==5.12.0 -flake8==6.0.0 -black==23.1.0 -pytest==7.2.2 -python-dotenv==1.0.0 -hubspot-api-client==7.5.0 -tenacity==8.2.2 - diff --git a/setup.cfg b/setup.cfg index a53787b..fd100d0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,132 @@ +# This file is used to configure your project. +# Read more about the various options under: +# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html +# https://setuptools.pypa.io/en/latest/references/keywords.html + +[metadata] +name = hubspot-api +description = This hubspot api is a handy wrapper on top of the existing hubspot python api. +author = Superscript +author_email = paul.lucas@gosuperscript.com +license = MIT +license_files = LICENSE.txt +long_description = file: README.rst +long_description_content_type = text/x-rst; charset=UTF-8 +url = https://github.com/pyscaffold/pyscaffold/ +# Add here related links, for example: +project_urls = + Documentation = https://github.com/mannum/hubspot-api + Source = https://github.com/mannum/hubspot-api +# Changelog = https://pyscaffold.org/en/latest/changelog.html +# Tracker = https://github.com/pyscaffold/pyscaffold/issues +# Conda-Forge = https://anaconda.org/conda-forge/pyscaffold +# Download = https://pypi.org/project/PyScaffold/#files +# Twitter = https://twitter.com/PyScaffold + +# Change if running only on Windows, Mac or Linux (comma-separated) +platforms = any + +# Add here all kinds of additional classifiers as defined under +# https://pypi.org/classifiers/ +classifiers = + Development Status :: 4 - Beta + Programming Language :: Python + + +[options] +zip_safe = False +packages = find_namespace: +include_package_data = True +package_dir = + =src + +# Require a min/specific Python version (comma-separated conditions) +# python_requires = >=3.7 + +# Add here dependencies of your project (line-separated), e.g. requests>=2.2,<3.0. +# Version specifiers like >=2.2,<3.0 avoid problems due to API changes in +# new major versions. This works if the required packages follow Semantic Versioning. +# For more information, check out https://semver.org/. +install_requires = + requests + isort==5.12.0 + flake8==6.0.0 + black==23.1.0 + pytest==7.2.2 + python-dotenv==1.0.0 + hubspot-api-client==7.5.0 + tenacity==8.2.2 + +[options.packages.find] +where = src +exclude = + tests + +[options.extras_require] +# Add here additional requirements for extra features, to install with: +# `pip install hubspot-api[PDF]` like: +# PDF = ReportLab; RXP + +# Add here test requirements (semicolon/line-separated) +testing = + setuptools + pytest + pytest-cov + requests-mock + pytest-mock + typing_extensions + +[options.entry_points] +# Add here console scripts like: +# console_scripts = +# script_name = hubspot-api.module:function +# For example: +# console_scripts = +# fibonacci = hubspot-api.skeleton:run +# And any other entry points, for example: +# pyscaffold.cli = +# awesome = pyscaffoldext.awesome.extension:AwesomeExtension + +[tool:pytest] +# Specify command line options as you would do when invoking pytest directly. +# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml +# in order to write a coverage file that can be read by Jenkins. +# CAUTION: --cov flags may prohibit setting breakpoints while debugging. +# Comment those flags to avoid this pytest issue. +addopts = + --cov hubspot-api --cov-report term-missing + --verbose +norecursedirs = + dist + build + .tox +testpaths = tests +# Use pytest markers to select/deselect specific tests +# markers = +# slow: mark tests as slow (deselect with '-m "not slow"') +# system: mark end-to-end system tests + +[devpi:upload] +# Options for the devpi: PyPI server and packaging tool +# VCS export must be deactivated since we are using setuptools-scm +no_vcs = 1 +formats = bdist_wheel + [flake8] +# Some sane defaults for the code style checker flake8 max_line_length = 119 -ignore = W503 -exclude = .git,__pycache__, -per_file_ignores = +extend_ignore = E203, W503 +# ^ Black-compatible +# E203 and W503 have edge cases handled by black +exclude = + .tox + build + dist + .eggs + docs/conf.py + +[pyscaffold] +# PyScaffold's parameters when the project was created. +# This will be used when updating. Do not change! +version = 4.3.1 +package = diff --git a/setup.py b/setup.py index 3e230a1..a0da6e4 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,21 @@ -from distutils.core import setup +""" + Setup file for sterlingstrategies. + Use setup.cfg to configure your project. -from setuptools import find_packages + This file was generated with PyScaffold 4.3.1. + PyScaffold helps you to put up the scaffold of your new Python project. + Learn more under: https://pyscaffold.org/ +""" +from setuptools import setup -setup( - name="hubspot-api", - version="1.2.0", - description="Superscript Hubspot API", - author="Superscript", - author_email="paul.lucas@gosuperscript.com", - install_requires=["requests", "python-dotenv==0.19.2", "hubspot-api-client==7.5.0"], - packages=find_packages(include=["hs_api*"]), -) +if __name__ == "__main__": + try: + setup(use_scm_version={"version_scheme": "no-guess-dev"}) + except: # noqa + print( + "\n\nAn error occurred while building the project, " + "please ensure you have the most updated version of setuptools, " + "setuptools_scm and wheel with:\n" + " pip install -U setuptools setuptools_scm wheel\n\n" + ) + raise diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..02baa81 --- /dev/null +++ b/tox.ini @@ -0,0 +1,93 @@ +# Tox configuration file +# Read more under https://tox.wiki/ +# THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! + +[tox] +minversion = 3.24 +envlist = default +isolated_build = True + + +[testenv] +description = Invoke pytest to run automated tests +setenv = + TOXINIDIR = {toxinidir} +passenv = + HOME + SETUPTOOLS_* + HUBSPOT_PIPELINE_ID + HUBSPOT_TOKEN +extras = + testing +commands = + pytest {posargs} +allowlist_externals= + pytest + + +[testenv:lint] +description = Perform static analysis and style checks +skip_install = True +deps = pre-commit +passenv = + HOMEPATH + PROGRAMDATA + SETUPTOOLS_* +commands = + pre-commit run --all-files {posargs:--show-diff-on-failure} + + +[testenv:{build,clean}] +description = + build: Build the package in isolation according to PEP517, see https://github.com/pypa/build + clean: Remove old distribution files and temporary build artifacts (./build and ./dist) +# https://setuptools.pypa.io/en/stable/build_meta.html#how-to-use-it +skip_install = True +changedir = {toxinidir} +deps = + build: build[virtualenv] +passenv = + SETUPTOOLS_* +commands = + clean: python -c 'import shutil; [shutil.rmtree(p, True) for p in ("build", "dist", "docs/_build")]' + clean: python -c 'import pathlib, shutil; [shutil.rmtree(p, True) for p in pathlib.Path("src").glob("*.egg-info")]' + build: python -m build {posargs} + + +[testenv:{docs,doctests,linkcheck}] +description = + docs: Invoke sphinx-build to build the docs + doctests: Invoke sphinx-build to run doctests + linkcheck: Check for broken links in the documentation +passenv = + SETUPTOOLS_* +setenv = + DOCSDIR = {toxinidir}/docs + BUILDDIR = {toxinidir}/docs/_build + docs: BUILD = html + doctests: BUILD = doctest + linkcheck: BUILD = linkcheck +deps = + -r {toxinidir}/docs/requirements.txt + # ^ requirements.txt shared with Read The Docs +commands = + sphinx-build --color -b {env:BUILD} -d "{env:BUILDDIR}/doctrees" "{env:DOCSDIR}" "{env:BUILDDIR}/{env:BUILD}" {posargs} + + +[testenv:publish] +description = + Publish the package you have been developing to a package index server. + By default, it uses testpypi. If you really want to publish your package + to be publicly accessible in PyPI, use the `-- --repository pypi` option. +skip_install = True +changedir = {toxinidir} +passenv = + # See: https://twine.readthedocs.io/en/latest/ + TWINE_USERNAME + TWINE_PASSWORD + TWINE_REPOSITORY + TWINE_REPOSITORY_URL +deps = twine +commands = + python -m twine check dist/* + python -m twine upload {posargs:--repository {env:TWINE_REPOSITORY:testpypi}} dist/* From 0f777148da8a346771b81760f766808ab11f4c30 Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Mon, 6 Mar 2023 10:33:14 -0500 Subject: [PATCH 06/12] Fixing up a few things pre-commit caught --- .pre-commit-config.yaml | 2 +- README.md | 8 ++++---- hs_api/api/hubspot_api.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 807df46..98a0926 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: # If like to embrace black styles even in the docs: - repo: https://github.com/asottile/blacken-docs - rev: v1.13.0 + rev: 1.13.0 hooks: - id: blacken-docs additional_dependencies: [black] diff --git a/README.md b/README.md index 04a0b26..0e9bc01 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ id of the pipeline that you want to interact with as the default. ```python from hs_api.api.hubspot_api import HubSpotClient -access_token = 'my_access_token' -pipeline_id = 'my_pipeline_id' +access_token = "my_access_token" +pipeline_id = "my_pipeline_id" client = HubSpotClient( access_token=access_token, @@ -44,8 +44,8 @@ client = HubSpotClient( You can also set the environment variables `HUBSPOT_ACCESS_TOKEN` and `HUBS_PIPELINE_ID` which will be used as defaults if no access_token or pipeline_id are passed to the `HubSpotClient`. This can be done by copying -the .env.template file from `hs_api\.env.template` into the root of the -project and renaming it to .env. +the .env.template file from `hs_api\.env.template` into the root of the +project and renaming it to .env. More details on how to use the client can be found in the test cases that diff --git a/hs_api/api/hubspot_api.py b/hs_api/api/hubspot_api.py index b4c28d0..56b736b 100644 --- a/hs_api/api/hubspot_api.py +++ b/hs_api/api/hubspot_api.py @@ -521,12 +521,12 @@ def create_email( :return: """ if not hs_timestamp: - hs_timestamp = int(datetime.now().timestamp()) + hs_timestamp_int = int(datetime.now().timestamp()) else: - hs_timestamp = int(hs_timestamp.timestamp()) + hs_timestamp_int = int(hs_timestamp.timestamp()) properties = dict( - hs_timestamp=hs_timestamp, + hs_timestamp=hs_timestamp_int, hs_email_direction=hs_email_direction, **properties, ) From 0863936b3f3cda5eba524cc10e00659525b0e666 Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Mon, 6 Mar 2023 10:43:10 -0500 Subject: [PATCH 07/12] Fixing up a few things pre-commit caught --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index fd100d0..d5b6c85 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ author = Superscript author_email = paul.lucas@gosuperscript.com license = MIT license_files = LICENSE.txt -long_description = file: README.rst +long_description = file: README.md long_description_content_type = text/x-rst; charset=UTF-8 url = https://github.com/pyscaffold/pyscaffold/ # Add here related links, for example: From f8aef0cec18860333eb83cbb23d63ada3c25190d Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Mon, 6 Mar 2023 11:05:13 -0500 Subject: [PATCH 08/12] Fixing up some packaging and coverage issues --- .coveragerc | 3 +-- setup.cfg | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.coveragerc b/.coveragerc index 17d9600..4109689 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,7 +6,7 @@ source = hubspot-api [paths] source = - src/ + hs_api/ */site-packages/ [report] @@ -26,4 +26,3 @@ exclude_lines = # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: -fail_under = 85 diff --git a/setup.cfg b/setup.cfg index d5b6c85..3c594a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,7 @@ zip_safe = False packages = find_namespace: include_package_data = True package_dir = - =src + =hs_api # Require a min/specific Python version (comma-separated conditions) # python_requires = >=3.7 @@ -58,7 +58,7 @@ install_requires = tenacity==8.2.2 [options.packages.find] -where = src +where = hs_api exclude = tests @@ -94,7 +94,7 @@ testing = # CAUTION: --cov flags may prohibit setting breakpoints while debugging. # Comment those flags to avoid this pytest issue. addopts = - --cov hubspot-api --cov-report term-missing + --cov hs_api --cov-report term-missing --verbose norecursedirs = dist From ef6a0eb642d86a3e3524f4a2d968ac938e65330b Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Mon, 6 Mar 2023 11:15:29 -0500 Subject: [PATCH 09/12] Github action env vars --- .github/workflows/pr_check.yml | 5 +++-- tox.ini | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index b1b0a6a..e05f30f 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -67,8 +67,9 @@ jobs: with: {name: python-distribution-files, path: dist/} - name: Run tests env: - HUBSPOT_TOKEN: ${{ secrets.HUBSPOT_TOKEN }} - HUBSPOT_PIPELINE_ID: ${{ secrets.HUBSPOT_PIPELINE_ID }} + HUBSPOT_TEST_ACCESS_TOKEN: ${{ secrets.HUBSPOT_TEST_ACCESS_TOKEN }} + HUBSPOT_TEST_PIPELINE_ID: ${{ secrets.HUBSPOT_TEST_PIPELINE_ID }} + HUBSPOT_TEST_TICKET_PIPELINE_ID: ${{ secrets.HUBSPOT_TEST_TICKET_PIPELINE_ID }} run: >- pipx run tox --installpkg '${{ needs.prepare.outputs.wheel-distribution }}' diff --git a/tox.ini b/tox.ini index 02baa81..30d9e3a 100644 --- a/tox.ini +++ b/tox.ini @@ -15,8 +15,9 @@ setenv = passenv = HOME SETUPTOOLS_* - HUBSPOT_PIPELINE_ID - HUBSPOT_TOKEN + HUBSPOT_TEST_ACCESS_TOKEN + HUBSPOT_TEST_PIPELINE_ID + HUBSPOT_TEST_TICKET_PIPELINE_ID extras = testing commands = From d5bc8eed8d4237a9b9adfd0f712417ae67aa5213 Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Mon, 6 Mar 2023 11:23:50 -0500 Subject: [PATCH 10/12] Changing wait based on issues in github actions --- tests/api/test_integration_hubspot_api.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/api/test_integration_hubspot_api.py b/tests/api/test_integration_hubspot_api.py index 7a3e7fc..4f3dc27 100644 --- a/tests/api/test_integration_hubspot_api.py +++ b/tests/api/test_integration_hubspot_api.py @@ -1,5 +1,5 @@ import pytest -from tenacity import retry, stop_after_attempt, wait_fixed +from tenacity import retry, stop_after_attempt, wait_exponential from hs_api.api.hubspot_api import BATCH_LIMITS, EMAIL_BATCH_LIMIT, HubSpotClient from hs_api.settings.settings import ( @@ -34,7 +34,7 @@ def test_create_and_find_contact( assert contact_result assert contact_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry(stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10)) def _get_contact(): _contact = hubspot_client.find_contact("hs_object_id", contact_result.id) assert _contact @@ -54,7 +54,7 @@ def test_create_and_find_company(hubspot_client, company_name, domain): assert company_result assert company_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry(stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10)) def _test(): _company = hubspot_client.find_company("hs_object_id", company_result.id) assert _company @@ -76,7 +76,7 @@ def test_create_and_find_company_iter(hubspot_client, domain, unique_id): assert company_result assert company_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry(stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10)) def _test(): _company = next( hubspot_client.find_company_iter("hs_object_id", company_result.id) @@ -132,7 +132,7 @@ def test_create_contact_and_associated_company_with_auto_created_company( assert result["contact"].id assert result["company"].id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry(stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10)) def _test(): _company = hubspot_client.find_company("hs_object_id", result["company"].id) assert _company @@ -171,7 +171,7 @@ def test_create_contact_and_associated_company_without_auto_created_company( assert result["contact"].id assert result["company"].id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry(stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10)) def _test(): _company = hubspot_client.find_company("hs_object_id", result["company"].id) assert _company @@ -208,7 +208,7 @@ def test_create_and_find_deal(hubspot_client, unique_id): assert deal_result assert deal_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry(stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10)) def _test(): _deal = hubspot_client.find_deal("dealname", deal_name) assert _deal @@ -240,7 +240,7 @@ def test_create_deal_for_company(hubspot_client, unique_id): assert deal_result assert deal_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry(stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10)) def _test(): _deal = hubspot_client.find_deal("dealname", deal_name) assert _deal @@ -288,7 +288,7 @@ def test_create_deal_for_contact( assert deal_result assert deal_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry(stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10)) def _test(): _deal = hubspot_client.find_deal("dealname", deal_name) assert _deal @@ -336,7 +336,7 @@ def test_find_owner_without_id_or_email(hubspot_client): def test_find_all_tickets_returns_batches(hubspot_client: HubSpotClient): - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry(stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10)) def _test(): tickets = hubspot_client.find_all_tickets() From 8bdba02d8f7cc33353ea4d785831fa013cf681e4 Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Mon, 6 Mar 2023 11:23:50 -0500 Subject: [PATCH 11/12] Changing wait based on issues in github actions --- tests/api/test_integration_hubspot_api.py | 38 +++++++++++++++++------ 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/tests/api/test_integration_hubspot_api.py b/tests/api/test_integration_hubspot_api.py index 7a3e7fc..e60b27b 100644 --- a/tests/api/test_integration_hubspot_api.py +++ b/tests/api/test_integration_hubspot_api.py @@ -1,5 +1,5 @@ import pytest -from tenacity import retry, stop_after_attempt, wait_fixed +from tenacity import retry, stop_after_attempt, wait_exponential from hs_api.api.hubspot_api import BATCH_LIMITS, EMAIL_BATCH_LIMIT, HubSpotClient from hs_api.settings.settings import ( @@ -34,7 +34,9 @@ def test_create_and_find_contact( assert contact_result assert contact_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry( + stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10) + ) def _get_contact(): _contact = hubspot_client.find_contact("hs_object_id", contact_result.id) assert _contact @@ -54,7 +56,9 @@ def test_create_and_find_company(hubspot_client, company_name, domain): assert company_result assert company_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry( + stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10) + ) def _test(): _company = hubspot_client.find_company("hs_object_id", company_result.id) assert _company @@ -76,7 +80,9 @@ def test_create_and_find_company_iter(hubspot_client, domain, unique_id): assert company_result assert company_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry( + stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10) + ) def _test(): _company = next( hubspot_client.find_company_iter("hs_object_id", company_result.id) @@ -132,7 +138,9 @@ def test_create_contact_and_associated_company_with_auto_created_company( assert result["contact"].id assert result["company"].id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry( + stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10) + ) def _test(): _company = hubspot_client.find_company("hs_object_id", result["company"].id) assert _company @@ -171,7 +179,9 @@ def test_create_contact_and_associated_company_without_auto_created_company( assert result["contact"].id assert result["company"].id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry( + stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10) + ) def _test(): _company = hubspot_client.find_company("hs_object_id", result["company"].id) assert _company @@ -208,7 +218,9 @@ def test_create_and_find_deal(hubspot_client, unique_id): assert deal_result assert deal_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry( + stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10) + ) def _test(): _deal = hubspot_client.find_deal("dealname", deal_name) assert _deal @@ -240,7 +252,9 @@ def test_create_deal_for_company(hubspot_client, unique_id): assert deal_result assert deal_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry( + stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10) + ) def _test(): _deal = hubspot_client.find_deal("dealname", deal_name) assert _deal @@ -288,7 +302,9 @@ def test_create_deal_for_contact( assert deal_result assert deal_result.id - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry( + stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10) + ) def _test(): _deal = hubspot_client.find_deal("dealname", deal_name) assert _deal @@ -336,7 +352,9 @@ def test_find_owner_without_id_or_email(hubspot_client): def test_find_all_tickets_returns_batches(hubspot_client: HubSpotClient): - @retry(stop=stop_after_attempt(7), wait=wait_fixed(2)) + @retry( + stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=2, max=10) + ) def _test(): tickets = hubspot_client.find_all_tickets() From 563c8ac2c84b20970ec4e643ea97222bc40b910b Mon Sep 17 00:00:00 2001 From: Brian Seel Date: Mon, 6 Mar 2023 12:34:27 -0500 Subject: [PATCH 12/12] This is the more standard way to do imports with setuptools now https://setuptools.pypa.io/en/latest/userguide/package_discovery.html --- setup.cfg | 4 ++-- {hs_api => src/hs_api}/.env.template | 1 + {hs_api => src/hs_api}/__init__.py | 0 {hs_api => src/hs_api}/api/__init__.py | 0 {hs_api => src/hs_api}/api/hubspot_api.py | 0 {hs_api => src/hs_api}/settings/__init__.py | 0 {hs_api => src/hs_api}/settings/settings.py | 0 7 files changed, 3 insertions(+), 2 deletions(-) rename {hs_api => src/hs_api}/.env.template (75%) rename {hs_api => src/hs_api}/__init__.py (100%) rename {hs_api => src/hs_api}/api/__init__.py (100%) rename {hs_api => src/hs_api}/api/hubspot_api.py (100%) rename {hs_api => src/hs_api}/settings/__init__.py (100%) rename {hs_api => src/hs_api}/settings/settings.py (100%) diff --git a/setup.cfg b/setup.cfg index 3c594a3..739d4b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,7 @@ zip_safe = False packages = find_namespace: include_package_data = True package_dir = - =hs_api + =src # Require a min/specific Python version (comma-separated conditions) # python_requires = >=3.7 @@ -58,7 +58,7 @@ install_requires = tenacity==8.2.2 [options.packages.find] -where = hs_api +where = src exclude = tests diff --git a/hs_api/.env.template b/src/hs_api/.env.template similarity index 75% rename from hs_api/.env.template rename to src/hs_api/.env.template index 0087396..af55d42 100644 --- a/hs_api/.env.template +++ b/src/hs_api/.env.template @@ -3,3 +3,4 @@ HUBSPOT_TEST_PIPELINE_ID= HUBSPOT_TEST_ACCESS_TOKEN= HUBSPOT_TEST_PIPELINE_ID= +HUBSPOT_TEST_TICKET_PIPELINE_ID= diff --git a/hs_api/__init__.py b/src/hs_api/__init__.py similarity index 100% rename from hs_api/__init__.py rename to src/hs_api/__init__.py diff --git a/hs_api/api/__init__.py b/src/hs_api/api/__init__.py similarity index 100% rename from hs_api/api/__init__.py rename to src/hs_api/api/__init__.py diff --git a/hs_api/api/hubspot_api.py b/src/hs_api/api/hubspot_api.py similarity index 100% rename from hs_api/api/hubspot_api.py rename to src/hs_api/api/hubspot_api.py diff --git a/hs_api/settings/__init__.py b/src/hs_api/settings/__init__.py similarity index 100% rename from hs_api/settings/__init__.py rename to src/hs_api/settings/__init__.py diff --git a/hs_api/settings/settings.py b/src/hs_api/settings/settings.py similarity index 100% rename from hs_api/settings/settings.py rename to src/hs_api/settings/settings.py