odms-client/odms/errors.py
2026-08-18 22:04:58 +08:00

89 lines
2.9 KiB
Python

"""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 ""