4.6 KiB
odms-client
Python client for the ODMS orbital-data API.
No dependencies. It is written against urllib because every service in the
estate depends on ODMS, and a shared client that drags a dependency tree into
each of their resolvers does not get shared — it gets copy-pasted, and then
there are five subtly different clients. If httpx is already installed it is
used instead, purely for connection pooling; nothing else changes and nothing
requires it.
Install
pip install ./clients/odms # or: pip install ./clients/odms[pooled]
Or vendor it: the odms/ package is four files and imports nothing outside the
standard library.
Use
from odms import OdmsClient
with OdmsClient("https://odms.tmtc.yksa.space", token=TOKEN) as odms:
element = odms.latest("iss") # newest OMM, by epoch
print(element["tle"])
past = odms.at("iss", "2026-04-01T00:00:00Z")
span = odms.history("iss", start, end, limit=500)
xml = odms.download(internal_id="iss", format="omm_xml")
oem = odms.oem("iss", start, stop, step_s=60) # token
base_url is the site root, not the API prefix. A token is optional: without
one you get the public catalogue and every read-only endpoint, which is what
most consumers need. With one you also see non-public satellites and can reach
the gated endpoints — OEM generation, message push, frame transforms, TLE
fitting, bulk propagation.
Methods return parsed JSON (dict/list) or, for the rendered formats, the
body as text. Deliberately not model classes: ODMS adds fields as the catalogue
grows, and mapping them onto fixed classes turns each addition into a client
release.
Endpoints
| Method | What |
|---|---|
query(format=…, **filters) |
The one element-search endpoint. Everything else below is a shortcut over it. |
latest(key) / at(key, when) / history(key, start, end) |
The three questions worth asking of an element history. None / [] when there is nothing. |
download(format=…, **filters) |
TLE text, OMM XML, CSV, KVN, or a converted element set. |
satellites() / satellite(key) / find(key) |
The catalogue. find returns None where satellite raises. |
sources() |
Configured public sources with their last-run status. |
opm(key) / oem(key, start, stop) |
Generate CCSDS messages. OEM needs a token. |
element_sets(key) / state(key, frame) |
Converted element sets, and a state in a named frame. |
messages(key) / message(id) / push_message(text) |
Stored external OPM/OEM. Push needs a token. |
transform(frame, …) / tle_from_oem(…) / propagate(key, timestamps) |
The gated compute endpoints. |
decay_runs(key) / decay_run(id) / decay_latest(key) |
Re-entry forecasts. decay_latest returns None when there is no forecast — a normal state, not an error. |
key is resolved the way ODMS resolves it: internal id, then NORAD catalog
number, then COSPAR designator.
Errors
Everything inherits OdmsError, so a caller that only cares whether ODMS
answered can catch that one.
| Exception | Meaning |
|---|---|
TransportError |
Never reached the server: DNS, TCP, TLS, read timeout. |
NotFound |
No such object — or a non-public one seen anonymously. Indistinguishable on purpose. |
Unauthorized |
The endpoint is token-gated and the token was missing or invalid. |
RateLimited |
Per-IP hourly budget spent, after the automatic retries. Carries retry_after. |
ServiceUnavailable |
ODMS or its propagation sidecar is busy or down. |
HTTPError |
Anything else 4xx/5xx. Carries status, body, detail. |
Retries
429, 502, 503 and 504 are retried with exponential backoff and jitter, honouring
Retry-After. 500 is not: it means ODMS took the request and broke on it, and
repeating it breaks it again.
Transport failures are retried for reads only. A POST whose outcome is
unknown is not repeated — push_message that timed out may well have stored the
message, and a retry would store a second copy. If you need to recover one,
check messages() and push again yourself.
Configuration
OdmsClient(
base_url,
token=None, # bearer token, from Core > API tokens in the admin
timeout_s=30.0,
max_retries=3,
user_agent="odms-client/1.0",
session=None, # pass your own httpx.Client to share a pool
)
The API prefix and the element-query path are class attributes
(api_prefix, query_path), so a deployment mounted elsewhere — or one still
serving the older tle/query/ alias — is reachable without subclassing.
Thread-safe. Not fork-safe: build one client per process.