From a0984688420098bd80b0c66adeca76c62cff69b0 Mon Sep 17 00:00:00 2001 From: Justin Smith Date: Sun, 9 Mar 2025 13:48:26 -0500 Subject: [PATCH] source-hubspot-native: Expand HubSpot Engagement associations to support multiple object types Update the Engagement model and schema to include associations for contacts, companies, tickets, content, quotes, orders, emails, meetings, notes, tasks, carts, partner_clients, and marketing_event. --- .../acmeCo/engagements.schema.yaml | 78 +++++++++++++ .../source_hubspot_native/models.py | 91 ++++++++++----- .../snapshots/snapshots__capture__stdout.json | 3 + .../snapshots__discover__stdout.json | 104 ++++++++++++++++++ 4 files changed, 250 insertions(+), 26 deletions(-) diff --git a/source-hubspot-native/acmeCo/engagements.schema.yaml b/source-hubspot-native/acmeCo/engagements.schema.yaml index 9756e742dc..a204e24786 100644 --- a/source-hubspot-native/acmeCo/engagements.schema.yaml +++ b/source-hubspot-native/acmeCo/engagements.schema.yaml @@ -100,12 +100,90 @@ properties: default: {} title: Associations type: object + contacts: + default: [] + items: + type: integer + title: Contacts + type: array + companies: + default: [] + items: + type: integer + title: Companies + type: array deals: default: [] items: type: integer title: Deals type: array + tickets: + default: [] + items: + type: integer + title: Tickets + type: array + content: + default: [] + items: + type: integer + title: Content + type: array + quotes: + default: [] + items: + type: integer + title: Quotes + type: array + orders: + default: [] + items: + type: integer + title: Orders + type: array + emails: + default: [] + items: + type: integer + title: Emails + type: array + meetings: + default: [] + items: + type: integer + title: Meetings + type: array + notes: + default: [] + items: + type: integer + title: Notes + type: array + tasks: + default: [] + items: + type: integer + title: Tasks + type: array + carts: + default: [] + items: + type: integer + title: Carts + type: array + partner_clients: + default: [] + items: + type: integer + title: Partner Clients + type: array + marketing_event: + default: [] + items: + type: integer + title: Marketing Event + type: array required: - id - createdAt diff --git a/source-hubspot-native/source_hubspot_native/models.py b/source-hubspot-native/source_hubspot_native/models.py index 6e4a2f3545..cedd978555 100644 --- a/source-hubspot-native/source_hubspot_native/models.py +++ b/source-hubspot-native/source_hubspot_native/models.py @@ -110,12 +110,20 @@ class Names(StrEnum): quotes = auto() subscriptions = auto() tickets = auto() + emails = auto() + meetings = auto() + notes = auto() + tasks = auto() + content = auto() + orders = auto() + carts = auto() + partner_clients = auto() + marketing_event = auto() # A Property is a HubSpot or HubSpot-user defined attribute that's # attached to a HubSpot CRM object. class Property(BaseDocument, extra="allow"): - name: str = "" calculated: bool = False hubspotObject: str = "unknown" # Added by us. @@ -126,7 +134,6 @@ class Properties(BaseDocument, extra="forbid"): class DealPipeline(BaseDocument, extra="allow"): - createdAt: AwareDatetime | None updatedAt: AwareDatetime | None @@ -136,7 +143,6 @@ class DealPipelines(BaseDocument, extra="forbid"): class Owner(BaseDocument, extra="allow"): - createdAt: AwareDatetime | None updatedAt: AwareDatetime | None @@ -185,7 +191,6 @@ def _post_init(self) -> Self: k: v for k, v in self.propertiesWithHistory.items() if len(v) } - # If the model has attached inline associations, # hoist them to corresponding arrays. Then clear associations. for ae in self.ASSOCIATED_ENTITIES: @@ -222,9 +227,40 @@ class Deal(BaseCRMObject): class Engagement(BaseCRMObject): - ASSOCIATED_ENTITIES = [Names.deals] + ASSOCIATED_ENTITIES = [ + Names.contacts, + Names.companies, + Names.deals, + Names.tickets, + Names.quotes, + Names.orders, + Names.emails, + Names.meetings, + Names.notes, + Names.tasks, + Names.content, + Names.orders, + Names.carts, + Names.partner_clients, + Names.marketing_event, + ] + contacts: list[int] = [] + companies: list[int] = [] deals: list[int] = [] + tickets: list[int] = [] + content: list[int] = [] + quotes: list[int] = [] + orders: list[int] = [] + emails: list[int] = [] + meetings: list[int] = [] + notes: list[int] = [] + tasks: list[int] = [] + content: list[int] = [] + orders: list[int] = [] + carts: list[int] = [] + partner_clients: list[int] = [] + marketing_event: list[int] = [] class Ticket(BaseCRMObject): @@ -241,7 +277,14 @@ class Product(BaseCRMObject): class LineItem(BaseCRMObject): - ASSOCIATED_ENTITIES = [Names.commerce_payments, Names.products, Names.deals, Names.invoices, Names.quotes, Names.subscriptions] + ASSOCIATED_ENTITIES = [ + Names.commerce_payments, + Names.products, + Names.deals, + Names.invoices, + Names.quotes, + Names.subscriptions, + ] commerce_payments: list[int] = [] products: list[int] = [] @@ -253,7 +296,6 @@ class LineItem(BaseCRMObject): # An Association, as returned by the v4 associations API. class Association(BaseModel, extra="forbid"): - class Type(BaseModel, extra="forbid"): category: Literal["HUBSPOT_DEFINED", "USER_DEFINED"] # Type IDs are defined here: https://developers.hubspot.com/docs/api/crm/associations @@ -296,7 +338,6 @@ class Paging(BaseModel, extra="forbid"): # Common shape of a v3 API paged listing for a GET request to the objects endpoint for a particular # object. class PageResult(BaseModel, Generic[Item], extra="forbid"): - class Cursor(BaseModel, extra="forbid"): after: str link: str @@ -311,7 +352,6 @@ class Paging(BaseModel, extra="forbid"): # Common shape of a v3 search API listing, which is the same as PageResult but includes a field for # the total number of records returned, and doesn't have a "link" in the paging.next object. class SearchPageResult(BaseModel, Generic[Item], extra="forbid"): - class Cursor(BaseModel, extra="forbid"): after: str @@ -325,7 +365,6 @@ class Paging(BaseModel, extra="forbid"): # Common shape of a v3 API batch read. class BatchResult(BaseModel, Generic[Item], extra="forbid"): - class Error(BaseModel, extra="forbid"): status: Literal["error"] category: Literal["OBJECT_NOT_FOUND"] @@ -349,7 +388,6 @@ class Error(BaseModel, extra="forbid"): class OldRecentCompanies(BaseModel): - class Item(BaseModel): class Properties(BaseModel): class Timestamp(BaseModel): @@ -367,7 +405,6 @@ class Timestamp(BaseModel): class OldRecentContacts(BaseModel): - class Item(BaseModel): class Properties(BaseModel): class Timestamp(BaseModel): @@ -379,13 +416,12 @@ class Timestamp(BaseModel): properties: Properties contacts: list[Item] - has_more: bool = Field(alias="has-more") # type: ignore - time_offset: int = Field(alias="time-offset") # type: ignore - vid_offset: int = Field(alias="vid-offset") # type: ignore + has_more: bool = Field(alias="has-more") # type: ignore + time_offset: int = Field(alias="time-offset") # type: ignore + vid_offset: int = Field(alias="vid-offset") # type: ignore class OldRecentDeals(BaseModel): - class Item(BaseModel): class Properties(BaseModel): class Timestamp(BaseModel): @@ -403,7 +439,6 @@ class Timestamp(BaseModel): class OldRecentEngagements(BaseModel): - class Item(BaseModel): class Engagement(BaseModel): id: int @@ -425,6 +460,7 @@ class OldRecentTicket(BaseModel): # EmailEvent and EmailEventsResponse represent an email event and the shape of the email events API # response, respectively. + class EmailEvent(BaseDocument, extra="allow"): id: str created: AwareDatetime @@ -442,12 +478,12 @@ class EmailEvent(BaseDocument, extra="allow"): "STATUSCHANGE", "SPAMREPORT", "SUPPRESSED", - "SUPPRESSION", # "SUPPRESSION" is documented in HubSpot's docs, but "SUPPRESSED" isn't. We've seen "SUPPRESSED" events, so "SUPPRESSION" events might not actually occur. - "UNBOUNCE", # This is not actually a type reported by HubSpot, but the absence of the "type" field means its an UNBOUNCE type. + "SUPPRESSION", # "SUPPRESSION" is documented in HubSpot's docs, but "SUPPRESSED" isn't. We've seen "SUPPRESSED" events, so "SUPPRESSION" events might not actually occur. + "UNBOUNCE", # This is not actually a type reported by HubSpot, but the absence of the "type" field means its an UNBOUNCE type. ] = Field( default="UNBOUNCE", - # Don't schematize the default value. - json_schema_extra=lambda x: x.pop('default'), # type: ignore + # Don't schematize the default value. + json_schema_extra=lambda x: x.pop("default"), # type: ignore ) @@ -478,7 +514,6 @@ class CustomObjectSchema(BaseDocument, extra="allow"): # This is the shape of a response from the V3 search API for custom objects. As above, we are # modeling only the minimum needed to get the IDs and modification time. class CustomObjectSearchResult(BaseModel): - class Properties(BaseModel): hs_lastmodifieddate: AwareDatetime @@ -488,10 +523,14 @@ def set_lastmodifieddate(cls, values): # The "Contacts" object uses `lastmodifieddate`, while everything # else uses `hs_lastmodifieddate`. - if 'lastmodifieddate' in values: - values['hs_lastmodifieddate'] = datetime.fromisoformat(values.pop('lastmodifieddate')) - elif 'hs_lastmodifieddate' in values: - values['hs_lastmodifieddate'] = datetime.fromisoformat(values.pop('hs_lastmodifieddate')) + if "lastmodifieddate" in values: + values["hs_lastmodifieddate"] = datetime.fromisoformat( + values.pop("lastmodifieddate") + ) + elif "hs_lastmodifieddate" in values: + values["hs_lastmodifieddate"] = datetime.fromisoformat( + values.pop("hs_lastmodifieddate") + ) return values id: int diff --git a/source-hubspot-native/tests/snapshots/snapshots__capture__stdout.json b/source-hubspot-native/tests/snapshots/snapshots__capture__stdout.json index 96d5627a8a..105ff03b27 100644 --- a/source-hubspot-native/tests/snapshots/snapshots__capture__stdout.json +++ b/source-hubspot-native/tests/snapshots/snapshots__capture__stdout.json @@ -4993,6 +4993,9 @@ "uuid": "DocUUIDPlaceholder-329Bb50aa48EAa9ef" }, "archived": false, + "companies": [ + 19015593502 + ], "createdAt": "2024-02-15T17:25:55.752000Z", "id": 47494434080, "properties": { diff --git a/source-hubspot-native/tests/snapshots/snapshots__discover__stdout.json b/source-hubspot-native/tests/snapshots/snapshots__discover__stdout.json index cc49836614..78bd2a3098 100644 --- a/source-hubspot-native/tests/snapshots/snapshots__discover__stdout.json +++ b/source-hubspot-native/tests/snapshots/snapshots__discover__stdout.json @@ -743,6 +743,22 @@ "title": "Associations", "type": "object" }, + "contacts": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Contacts", + "type": "array" + }, + "companies": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Companies", + "type": "array" + }, "deals": { "default": [], "items": { @@ -750,6 +766,94 @@ }, "title": "Deals", "type": "array" + }, + "tickets": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Tickets", + "type": "array" + }, + "content": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Content", + "type": "array" + }, + "quotes": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Quotes", + "type": "array" + }, + "orders": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Orders", + "type": "array" + }, + "emails": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Emails", + "type": "array" + }, + "meetings": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Meetings", + "type": "array" + }, + "notes": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Notes", + "type": "array" + }, + "tasks": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Tasks", + "type": "array" + }, + "carts": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Carts", + "type": "array" + }, + "partner_clients": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Partner Clients", + "type": "array" + }, + "marketing_event": { + "default": [], + "items": { + "type": "integer" + }, + "title": "Marketing Event", + "type": "array" } }, "required": [