Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing up tests #23

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ pipeline_id = 'my_pipeline_id'

client = HubSpotClient(
access_token=access_token,
PIPELINE_ID=pipeline_id,
pipeline_id=pipeline_id,
)
```

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
Expand Down Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions hs_api/.env.template
Original file line number Diff line number Diff line change
@@ -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=
197 changes: 158 additions & 39 deletions hs_api/api/hubspot_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import time
from collections.abc import Generator
from datetime import datetime
from typing import Dict, Optional

import requests
from hubspot import HubSpot
from hubspot.auth.oauth import ApiException
from hubspot.crm.contacts import (
Expand All @@ -24,7 +26,7 @@
}

BATCH_LIMITS = 50
EMAIL_BATCH_LIMIT = 1000
EMAIL_BATCH_LIMIT = 10
RETRY_LIMIT = 3
RETRY_WAIT = 60

Expand Down Expand Up @@ -64,13 +66,16 @@ 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
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
Expand Down Expand Up @@ -112,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](
Expand Down Expand Up @@ -150,19 +158,71 @@ 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_company(self, property_name, value):
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"}]

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
Expand Down Expand Up @@ -195,6 +255,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(
Expand All @@ -206,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):
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
Expand All @@ -215,40 +287,32 @@ 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,
parameters[filter_name] = filter_value

resp = self._find(
"email",
property_name=filter_name,
value=filter_value,
limit=limit,
after=after,
sort=sort,
)
response.raise_for_status()

response_json = response.json()
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:
Expand Down Expand Up @@ -319,6 +383,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,
Expand Down Expand Up @@ -374,10 +441,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(
Expand Down Expand Up @@ -428,6 +494,45 @@ 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(
Expand Down Expand Up @@ -455,6 +560,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
Expand Down Expand Up @@ -488,7 +607,7 @@ def create_association(
from_object_id,
to_object_type,
to_object_id,
get_association_id(from_object_type, to_object_type),
[],
)
return result

Expand Down
13 changes: 7 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
description="Superscript Hubspot API",
author="Superscript",
author_email="[email protected]",
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*"]),
)
Loading