mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
HubSpot clients share one scrubbed-retry call policy 🟪
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2ea9257036
commit
ed08a6adfe
3 changed files with 56 additions and 84 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
45
infrastructure/hubspot/scrubbed_calls.py
Normal file
45
infrastructure/hubspot/scrubbed_calls.py
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue