108 lines
4.6 KiB
Markdown
108 lines
4.6 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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.
|