89 lines
2.9 KiB
Python
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 ""
|