"""CMDB provider exceptions and credential-safe diagnostic helpers.""" from app.core.credential_redaction import redact_credentials class ProviderError(Exception): """Base — Temporal retries on this.""" class ProviderRateLimitError(ProviderError): """HTTP 429 — retryable.""" def __init__(self, retry_after: int | None = None): self.retry_after = retry_after class ProviderAuthError(ProviderError): """HTTP 401/403 — credential wrong/expired. Non-retryable.""" class ProviderTransientError(ProviderError): """HTTP 5xx / timeout — retryable.""" class CapabilityNotSupported(ProviderError): """Provider does not support this resource type. Non-retryable.""" def redact_provider_error(text: object, *, max_len: int = 300) -> str: """Return a credential-safe, length-bounded provider error. The implementation lives in :mod:`app.core.credential_redaction` so cloud providers and non-cloud integration diagnostics cannot drift into different credential-recognition rules. """ return redact_credentials(text, max_len=max_len) def redact_leg_entry(entry: dict) -> dict: """Return a redacted copy of a provider ``partial_failures`` entry. Both cloud connection tests and the sync engine persist these entries in admin-readable surfaces. Current providers normally emit bounded leg labels and reasons, but this boundary also protects future drivers that construct either field from raw SDK errors. """ if not isinstance(entry, dict): return entry out = dict(entry) if "leg" in out: out["leg"] = redact_provider_error(out.get("leg") or "") if "reason" in out: out["reason"] = redact_provider_error(out.get("reason") or "") if "error_message" in out: out["error_message"] = redact_provider_error( out.get("error_message") or "", ) return out