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