The runtime for the InOrbit API, and its public surface, in a blocking and an asyncio
form. On PyPI since 0.1.0, for Python 3.11 or later:
pip install inorbithr # or: uv add inorbithrfrom inorbithr import Public
api = Public.load() # code, then INORBIT_*, then the iohr config file, then `iohr login`
me = api.me().value
digests = api.radar.list_digests(limit=3).valueload takes each setting from the first place that sets it: an option in code
(Public.load(timeout=10)), the environment (INORBIT_TIMEOUT=10s), your profile in
the command line's config file, then the default. Credentials come from the first source
of the chain that has any: code, INORBIT_TOKEN / INORBIT_TOKEN_FILE /
INORBIT_KEY_ID with INORBIT_KEY_SECRET (or _SECRET_FILE) and INORBIT_SCOPES, the
config file's profile, and last the developer's iohr login. Nothing is contacted until
the first call. api.client.config().describe() (or iohr sdk config) shows what was
used and where it came from, secrets redacted. Settings, sources and the chain are in
docs/config.md; ready-made setups for CI, Kubernetes, proxies,
private CAs and serverless in docs/recipes.md.
AsyncPublic is the same on asyncio (await api.me()). Public.from_env() and
Client(...) keep working: from_env reads only the credential and the URLs from the
environment, and explicit options read nothing but code. examples/python
holds programs CI type-checks.
Every call goes through a named pipeline of middlewares, which you can add to, reorder,
replace or trim. A per-retry middleware sees every attempt, Authorization included; a
per-call one sees the call once (a circuit breaker goes there):
import time
from inorbithr import CallNext, Middleware, Pipeline, Public, SdkRequest, SdkResponse
class Timing:
name = "timing"
def __call__(self, request: SdkRequest, call_next: CallNext, /) -> SdkResponse:
started = time.monotonic()
response = call_next(request)
print(request.info.operation, request.info.attempt, time.monotonic() - started)
return response
def pipeline(p: Pipeline[Middleware]) -> None:
p.add_per_retry(Timing()).remove("rate_limit")
api = Public.load(pipeline=pipeline)AsyncClient takes AsyncMiddleware, whose __call__ is async and awaits
call_next. A middleware must not log secrets or bodies; the built-ins do not.
A surface cut to what your own credentials may call comes from the command line:
iohr sdk generate --lang python --for default --out src/iohrIt writes one class per profile, blocking and asyncio, each holding only the operations
its cut holds; pyright and mypy refuse a call a profile may not make.
-
Errors are one tree under
InOrbitError;ApiErrorcarries the API'scode,statusand typeddetails. -
Idempotent calls (
GET,PUT,DELETE) and writes that takeIdempotency-Keyare retried on 429, 503, 504 and connection failures, at mostmax_retries(2) times, honouringRetry-Afterup toretry_after_max(60 s). A write that takes the header gets one key per call, sent on every attempt (pass your own withidempotency_key=); other writes are not retried. Retries draw from a per-client budget, so a real outage fails fast instead of multiplying traffic. -
Every attempt is held to
timeout(30 s), body included, and every call tototal_timeout(120 s since 0.2.2), retries and waits included. Atimeout=on a call, orinorbithr.call_options(timeout=...), replacestimeoutand capstotal_timeout. -
Results and errors carry
request_id,server_request_id,idempotency_keyand therate_limitsnapshot;client.rate_limit()is the latest.rate_limit="wait"holds a call until an exhausted window resets. -
Logging is off until
logis set (INORBIT_LOG=info); records go tologging.getLogger("inorbithr")with the field names of docs/config.md section 7.9, never a body, a query value, a token or a secret.redact=sees every record last. -
With
pip install "inorbithr[otel]", each call is anINTERNALspan and each attempt aCLIENTspan on your OpenTelemetry providers, andtraceparentis sent; without it,inorbithr.call_options(traceparent=...)passes yours through unchanged. -
TLS is verified against the operating system's trust store (
truststore; on Linux, OpenSSL's, which also readsSSL_CERT_FILE);ca_bundleadds a private CA, andclient_certwithclient_keypresent a certificate.proxy,no_proxyand the standardhttps_proxyvariables apply to calls, the token exchange and the socket. After a certificate rotation, build a new client. -
64-bit integers are
int, sent as decimal strings; a message field the API left out isNone; fields this version does not know are kept. -
Every request field is optional (
Noneby default) and left out when unset; an answer's field is optional unless the API always sends it. Timestamps stay the strings the API sent;parse_timestampreads one,""(unset) asNone. -
A paged list has an
all_<operation>beside its page method that follows the next-page token:for d in api.radar.all_list_digests(): ..., orasync foron theasyncioclass; stopping the loop fetches nothing more. -
A streaming operation hands back a
Streamthat opens on the first step of the loop and yields one model per event;AsyncStreamon theasyncioclass. Leaving thewith(orclose()) stops it:with api.events.stream_events(types="key.created") as events: for event in events: print(event.type, event.id)
Streams open over server-sent events, or with
Client(streams="socket")every stream of the client shares one/v1/wsconnection, opened again when the server ends it. A stream silent forstream_idle_timeout(45 s) ends withApiTimeoutError; anerrorevent or frame is anApiError; a revoked key ends the stream withunauthenticated. -
Python 3.11 or newer; the dependencies are
httpx,pydantic2,websockets(for the socket) andtruststore(the system's trust store); the config file is read with the standard library'stomllib, logs go throughlogging.
How the SDKs behave in every language: docs/design.md.