Initial commit
This commit is contained in:
commit
d71d560d29
8 changed files with 1325 additions and 0 deletions
108
README.md
Normal file
108
README.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue