Initial commit

This commit is contained in:
ThePetrovich 2026-08-18 22:04:58 +08:00
commit d71d560d29
8 changed files with 1325 additions and 0 deletions

108
README.md Normal file
View file

@ -0,0 +1,108 @@
# 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.