"""Exceptions raised by the ODMS client. The split is by *what the caller should do*, not by HTTP status. Everything inherits :class:`OdmsError`, so a service that only wants "ODMS did not answer" can catch that one and be done. """ from __future__ import annotations class OdmsError(Exception): """Base class for every failure this client raises.""" class TransportError(OdmsError): """The request never got an answer: DNS, TCP, TLS, or a read timeout. Distinguished from an HTTP error because a request that never arrived can be retried safely even when it is not idempotent -- and one that may have arrived cannot. The client only auto-retries these for reads. """ class HTTPError(OdmsError): """ODMS answered with a status the caller did not ask for.""" def __init__(self, status: int, url: str, body: str = "", detail: str = ""): self.status = status self.url = url self.body = body self.detail = detail or _detail_from(body) or body[:200] super().__init__(f"HTTP {status} from {url}" + (f": {self.detail}" if self.detail else "")) class NotFound(HTTPError): """No such satellite, run or message. ODMS returns this for a satellite that exists but is not public, too -- an anonymous caller cannot tell the two apart, which is the intent. If you expected a private object, check the token. """ class Unauthorized(HTTPError): """The endpoint needs a token, and the one presented was missing or invalid. ODMS's read surface is anonymous; only the expensive and the writing endpoints (OEM generation, message push, transform, TLE fitting, bulk propagation) are gated. """ class RateLimited(HTTPError): """The per-IP hourly budget is spent. ``retry_after`` is the server's own advice in seconds when it gave one. The client retries these automatically up to ``max_retries``; seeing this exception means the budget is still spent after those attempts, so the fix is to slow down or hold a token, not to retry harder. """ def __init__(self, status: int, url: str, body: str = "", detail: str = "", retry_after: float | None = None): self.retry_after = retry_after super().__init__(status, url, body, detail) class ServiceUnavailable(HTTPError): """ODMS (or something it depends on) is busy or down. Retried automatically. A persistent one usually means the propagation sidecar behind ODMS is saturated rather than ODMS itself being unwell. """ def _detail_from(body: str) -> str: """The error message out of a JSON body, if that is what this is.""" import json try: parsed = json.loads(body) except (ValueError, TypeError): return "" if isinstance(parsed, dict): for key in ("detail", "error", "message"): value = parsed.get(key) if isinstance(value, str) and value: return value return ""