mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
HubSpot failures surface without the response body that echoes tenant PII 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2e19085875
commit
e3894b0309
2 changed files with 76 additions and 17 deletions
|
|
@ -1,13 +1,40 @@
|
|||
from typing import Dict
|
||||
import json
|
||||
from typing import Dict, Optional
|
||||
|
||||
from hubspot.client import Client # type: ignore[reportMissingTypeStubs]
|
||||
from hubspot.crm.associations.v4 import AssociationSpec # type: ignore[reportMissingTypeStubs]
|
||||
from hubspot.crm.contacts import SimplePublicObjectInputForCreate # type: ignore[reportMissingTypeStubs]
|
||||
|
||||
from etl.hubspot.errors import HubspotRequestError
|
||||
|
||||
# HubSpot-defined deal -> contact association ("deal_to_contact" in v3 terms).
|
||||
DEAL_TO_CONTACT_ASSOCIATION_TYPE_ID = 3
|
||||
|
||||
|
||||
def _scrubbed(error: Exception) -> HubspotRequestError:
|
||||
"""Reduce an SDK error to non-PII diagnostics, dropping the body.
|
||||
|
||||
Each HubSpot sub-module ships its own ApiException class with no shared
|
||||
base beyond Exception, so the fields are read by duck-typing.
|
||||
"""
|
||||
status = getattr(error, "status", None)
|
||||
status_code = status if isinstance(status, int) else None
|
||||
|
||||
category: Optional[str] = None
|
||||
correlation_id: Optional[str] = None
|
||||
try:
|
||||
details = json.loads(getattr(error, "body", None) or "")
|
||||
if isinstance(details, dict):
|
||||
category = details.get("category") # type: ignore[reportUnknownMemberType]
|
||||
correlation_id = details.get("correlationId") # type: ignore[reportUnknownMemberType]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return HubspotRequestError(
|
||||
status_code=status_code, category=category, correlation_id=correlation_id
|
||||
)
|
||||
|
||||
|
||||
class HubspotDealContactsClient:
|
||||
"""Focused HubSpot client for the tenant-data sync flow.
|
||||
|
||||
|
|
@ -21,23 +48,30 @@ class HubspotDealContactsClient:
|
|||
self._client: Client = sdk_client
|
||||
|
||||
def create_contact(self, properties: Dict[str, str]) -> str:
|
||||
contact = self._client.crm.contacts.basic_api.create( # type: ignore[reportUnknownMemberType]
|
||||
simple_public_object_input_for_create=SimplePublicObjectInputForCreate(
|
||||
properties=properties
|
||||
try:
|
||||
contact = self._client.crm.contacts.basic_api.create( # type: ignore[reportUnknownMemberType]
|
||||
simple_public_object_input_for_create=SimplePublicObjectInputForCreate(
|
||||
properties=properties
|
||||
)
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
# `from None` keeps the body-carrying SDK error out of tracebacks.
|
||||
raise _scrubbed(error) from None
|
||||
return str(contact.id) # type: ignore[reportUnknownMemberType]
|
||||
|
||||
def associate_contact_to_deal(self, deal_id: str, contact_id: str) -> None:
|
||||
self._client.crm.associations.v4.basic_api.create( # type: ignore[reportUnknownMemberType]
|
||||
object_type="deals",
|
||||
object_id=deal_id,
|
||||
to_object_type="contacts",
|
||||
to_object_id=contact_id,
|
||||
association_spec=[
|
||||
AssociationSpec(
|
||||
association_category="HUBSPOT_DEFINED",
|
||||
association_type_id=DEAL_TO_CONTACT_ASSOCIATION_TYPE_ID,
|
||||
)
|
||||
],
|
||||
)
|
||||
try:
|
||||
self._client.crm.associations.v4.basic_api.create( # type: ignore[reportUnknownMemberType]
|
||||
object_type="deals",
|
||||
object_id=deal_id,
|
||||
to_object_type="contacts",
|
||||
to_object_id=contact_id,
|
||||
association_spec=[
|
||||
AssociationSpec(
|
||||
association_category="HUBSPOT_DEFINED",
|
||||
association_type_id=DEAL_TO_CONTACT_ASSOCIATION_TYPE_ID,
|
||||
)
|
||||
],
|
||||
)
|
||||
except Exception as error:
|
||||
raise _scrubbed(error) from None
|
||||
|
|
|
|||
25
etl/hubspot/errors.py
Normal file
25
etl/hubspot/errors.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from typing import Optional
|
||||
|
||||
|
||||
class HubspotRequestError(Exception):
|
||||
"""A HubSpot API failure, scrubbed of the response body.
|
||||
|
||||
HubSpot validation errors echo submitted property values back in the
|
||||
body, so carrying (or exception-chaining) the SDK error would leak the
|
||||
submitted PII into tracebacks and logs. Only non-PII diagnostics are
|
||||
kept: status code, HubSpot error category and correlation id.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status_code: Optional[int],
|
||||
category: Optional[str],
|
||||
correlation_id: Optional[str],
|
||||
) -> None:
|
||||
self.status_code = status_code
|
||||
self.category = category
|
||||
self.correlation_id = correlation_id
|
||||
super().__init__(
|
||||
f"HubSpot request failed (status={status_code}, "
|
||||
f"category={category}, correlation_id={correlation_id})"
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue