From ed08a6adfe1843f41287c4fbda340a31e6906b12 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 6 Jul 2026 16:02:22 +0000 Subject: [PATCH] =?UTF-8?q?HubSpot=20clients=20share=20one=20scrubbed-retr?= =?UTF-8?q?y=20call=20policy=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../hubspot/deal_contacts_client.py | 42 ++------------- .../hubspot/deal_properties_client.py | 53 +++---------------- infrastructure/hubspot/scrubbed_calls.py | 45 ++++++++++++++++ 3 files changed, 56 insertions(+), 84 deletions(-) create mode 100644 infrastructure/hubspot/scrubbed_calls.py diff --git a/infrastructure/hubspot/deal_contacts_client.py b/infrastructure/hubspot/deal_contacts_client.py index 2c0eb0627..948a2d953 100644 --- a/infrastructure/hubspot/deal_contacts_client.py +++ b/infrastructure/hubspot/deal_contacts_client.py @@ -1,46 +1,15 @@ -import json -from typing import Any, Callable, Dict, Optional, cast +from typing import Any, Callable, Dict 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 infrastructure.hubspot.errors import HubspotRequestError -from infrastructure.hubspot.retry import call_with_retry +from infrastructure.hubspot.scrubbed_calls import scrubbed_call_with_retry # 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: - parsed: Any = json.loads(getattr(error, "body", None) or "") - if isinstance(parsed, dict): - details = cast(Dict[str, Any], parsed) - raw_category = details.get("category") - raw_correlation_id = details.get("correlationId") - category = raw_category if isinstance(raw_category, str) else None - correlation_id = ( - raw_correlation_id if isinstance(raw_correlation_id, str) else None - ) - 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. @@ -81,9 +50,4 @@ class HubspotDealContactsClient: @staticmethod def _call(fn: Callable[[], Any]) -> Any: - """Run one SDK call under the shared retry policy, scrubbed on failure.""" - try: - return call_with_retry(fn) - except Exception as error: - # `from None` keeps the body-carrying SDK error out of tracebacks. - raise _scrubbed(error) from None + return scrubbed_call_with_retry(fn) diff --git a/infrastructure/hubspot/deal_properties_client.py b/infrastructure/hubspot/deal_properties_client.py index 3893c795b..dda4813a4 100644 --- a/infrastructure/hubspot/deal_properties_client.py +++ b/infrastructure/hubspot/deal_properties_client.py @@ -1,40 +1,7 @@ -import json -from typing import Any, Dict, Optional, cast - from hubspot.client import Client # type: ignore[reportMissingTypeStubs] from hubspot.crm.deals import SimplePublicObjectInput # type: ignore[reportMissingTypeStubs] -from infrastructure.hubspot.errors import HubspotRequestError -from infrastructure.hubspot.retry import call_with_retry - - -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: - parsed: Any = json.loads(getattr(error, "body", None) or "") - if isinstance(parsed, dict): - details = cast(Dict[str, Any], parsed) - raw_category = details.get("category") - raw_correlation_id = details.get("correlationId") - category = raw_category if isinstance(raw_category, str) else None - correlation_id = ( - raw_correlation_id if isinstance(raw_correlation_id, str) else None - ) - except ValueError: - pass - - return HubspotRequestError( - status_code=status_code, category=category, correlation_id=correlation_id - ) +from infrastructure.hubspot.scrubbed_calls import scrubbed_call_with_retry class HubspotDealPropertiesClient: @@ -51,15 +18,11 @@ class HubspotDealPropertiesClient: def write_deal_property( self, deal_id: str, property_name: str, value: str ) -> None: - try: - call_with_retry( - lambda: self._client.crm.deals.basic_api.update( # type: ignore[reportUnknownMemberType] - deal_id, - simple_public_object_input=SimplePublicObjectInput( - properties={property_name: value} - ), - ) + scrubbed_call_with_retry( + lambda: self._client.crm.deals.basic_api.update( # type: ignore[reportUnknownMemberType] + deal_id, + simple_public_object_input=SimplePublicObjectInput( + properties={property_name: value} + ), ) - except Exception as error: - # `from None` keeps the body-carrying SDK error out of tracebacks. - raise _scrubbed(error) from None + ) diff --git a/infrastructure/hubspot/scrubbed_calls.py b/infrastructure/hubspot/scrubbed_calls.py new file mode 100644 index 000000000..886358f01 --- /dev/null +++ b/infrastructure/hubspot/scrubbed_calls.py @@ -0,0 +1,45 @@ +import json +from typing import Any, Callable, Dict, Optional, TypeVar, cast + +from infrastructure.hubspot.errors import HubspotRequestError +from infrastructure.hubspot.retry import call_with_retry + +T = TypeVar("T") + + +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: + parsed: Any = json.loads(getattr(error, "body", None) or "") + if isinstance(parsed, dict): + details = cast(Dict[str, Any], parsed) + raw_category = details.get("category") + raw_correlation_id = details.get("correlationId") + category = raw_category if isinstance(raw_category, str) else None + correlation_id = ( + raw_correlation_id if isinstance(raw_correlation_id, str) else None + ) + except ValueError: + pass + + return HubspotRequestError( + status_code=status_code, category=category, correlation_id=correlation_id + ) + + +def scrubbed_call_with_retry(fn: Callable[[], T]) -> T: + """Run one SDK call under the shared retry policy, scrubbed on failure.""" + try: + return call_with_retry(fn) + except Exception as error: + # `from None` keeps the body-carrying SDK error out of tracebacks. + raise scrubbed(error) from None