scorm-dispatch-client
LLM? Read
llms.txtfirst: a concise manual of every method, error and a full example.
Async Python client for scorm-dispatch, the service that
builds lightweight SCORM packages from a neutral JSON course and, at run time, validates partners by id and
Origin and hands out short-lived signed URLs.
- Async only, on
httpx2. Python 3.11+. - Fully typed (
py.typed, mypy strict). Requests areTypedDicts, responses are frozen pydantic models. - Covers the admin API, the public runtime endpoint and the health routes.
- One exception,
ScormDispatchError, carrying the HTTP status and the RFC 9457 problem body.
Install
pip install scorm-dispatch-client
# or straight from git
pip install "scorm-dispatch-client @ git+https://github.com/cr0hn/scorm-dispatch-python"
Quickstart
import asyncio
from scorm_dispatch import Client
async def main() -> None:
async with Client("https://scorm.example.com", "ADMIN_API_KEY") as sd:
await sd.put_partner(
"northwind",
{
"name": "Northwind Academy",
"contactEmail": "learning@northwind.example",
"seatCap": 500,
"allowedOrigins": ["https://lms.northwind.example"],
"active": True,
},
)
await sd.put_asset(
"vid-walkthrough", {"kind": "video", "uri": "stream-bunny://6f1c2a9e-..."}
)
zip_bytes = await sd.create_package(
"northwind",
"2004",
{
"id": "intro",
"title": "Intro",
"version": "1",
"language": "en",
"modules": [
{
"title": "Start",
"lessons": [
{
"id": "l1",
"title": "Welcome",
"type": "text",
"html": "<p>Hello</p>",
},
{
"id": "l2",
"title": "Video",
"type": "video",
"assetKey": "vid-walkthrough",
},
],
}
],
},
)
open("intro.zip", "wb").write(zip_bytes)
asyncio.run(main())
Methods
Client(base_url, admin_key, *, http_client=None, timeout=30.0). Use it as async with, or call aclose().
| Method | Route | Returns |
|---|---|---|
put_partner(partner_id, partner) |
PUT /v1/partners/{id} |
Partner |
get_partner(partner_id) |
GET /v1/partners/{id} |
Partner |
list_partners(*, limit=50, offset=0) |
GET /v1/partners |
PartnerList |
put_asset(asset_key, asset) |
PUT /v1/assets/{key} |
Asset (has asset_id) |
get_asset(asset_key) |
GET /v1/assets/{key} |
Asset |
create_package(partner_id, scorm_version, course) |
POST /v1/packages |
bytes (the zip) |
get_usage_report(month) |
GET /v1/reports/usage |
UsageReport |
get_usage_report_csv(month) |
same, Accept: text/csv |
str |
get_runtime_asset(asset_id, *, partner, learner, origin) |
GET /v1/runtime/assets/{id} |
RuntimeAsset |
healthz() / readyz() |
GET /healthz, /readyz |
None (raises if not ok) |
Examples of the less obvious ones:
page = await sd.list_partners(limit=100)
for p in page.items:
print(p.partner_id, p.seat_cap, p.active)
report = await sd.get_usage_report("2026-11") # month is YYYY-MM
csv_text = await sd.get_usage_report_csv("2026-11")
# What a package does in the browser; useful to check a partner's setup.
# It sends the Origin you pass and never the admin key.
asset = await sd.get_runtime_asset(
"Zk3p9QwL7xYb2NcVd8TfRa",
partner="northwind",
learner="student-1",
origin="https://lms.northwind.example",
)
print(asset.kind, asset.url, asset.expires_at)
Error handling
Every non-2xx answer raises ScormDispatchError:
from scorm_dispatch import ScormDispatchError
try:
await sd.create_package("northwind", "2004", course)
except ScormDispatchError as e:
e.status # int: 400, 401, 403, 404, 409, 413, 422, 429, 5xx
e.problem # Problem: type, title, status, detail, errors
e.field_errors # 422 only: [FieldError(path="course.modules[0]...", message="...")]
e.retry_after # 429 only: seconds, from Retry-After (seconds or HTTP-date form)
| Status | Meaning |
|---|---|
| 400 | malformed request or parameter |
| 401 | wrong admin key |
| 403 | runtime: partner unknown/inactive or Origin not allowed |
| 404 | partner, asset or runtime asset not found |
| 409 | asset kind cannot change |
| 413 | package body above the server's limit |
| 422 | validation failed; see field_errors |
| 429 | rate limit; wait retry_after seconds |
A non-problem body (for example a proxy's HTML page) still raises, with the text in problem.detail.
A 2xx body that does not parse also raises ScormDispatchError (title Unparseable response body).
Unknown enum values in responses (for example a new asset kind) are accepted as plain strings.
Network failures are httpx2 exceptions and are not wrapped.
Redirects are never followed: a 3xx answer raises ScormDispatchError naming the redirect target, even if
you pass an http_client with follow_redirects=True. This keeps the Bearer key on the configured host.
Fix base_url instead.
Timeouts and retries
timeout (seconds) applies to every request. If you omit it, an owned client uses 30 and an injected
http_client keeps its own timeout; pass timeout explicitly to override it. The client never retries: PUT upserts are
idempotent and safe for you to retry; POST /v1/packages is a pure function of its body, also safe.
Back off on 429 using retry_after. To tune pooling or add transport-level retries, pass your own
httpx2.AsyncClient as http_client (you then own and close it).
Typing
Request bodies are TypedDicts with the wire (camelCase) keys: your type checker catches typos and wrong
lesson shapes. Responses are frozen pydantic models with snake_case attributes and parsed datetimes.
Unknown response fields are ignored, so a newer server does not break an older client.
Versioning and compatibility
The client follows semantic versioning. It is built and tested against the contract in
spec/openapi.yaml (API 1.x); a 1.x server is expected to work with any 0.x/1.x client.
What each suite checks:
- Unit (
tests/test_unit.py): the exact requests the client builds and how it maps answers and errors, against an in-memory transport; 100% line and branch coverage is enforced. - Contract (
tests/test_contract.py): the requests the client builds, and hand-written sample responses, are validated againstspec/openapi.yaml. The samples are written by hand, so this proves the client agrees with the spec, not that the server does. - Integration (
tests/test_integration.py): the real server (built from source, with Postgres behind PgBouncer and MinIO) answers every public method and error status. This is what covers real responses.
Development
make check # lint, mypy, unit+contract on Python 3.11-3.14, integration
make matrix # only unit+contract on every supported Python (each in its own .venv-3.X)
make integration # needs Docker, Go, and the server source: SCORM_DISPATCH_SRC=../scorm-dispatch
The integration suite builds the real server, starts Postgres behind PgBouncer and MinIO with testcontainers, and runs every public method and documented error status against it. It fails, not skips, if the server source is missing.
Contributing
Issues and pull requests welcome. CI (.github/workflows/ci.yml) runs lint, mypy and unit+contract on
Python 3.11-3.14. It cannot run the integration suite, which needs the private server source: run
make integration locally before sending a change. Keep make check green; a change in behaviour needs a unit test and an
integration test. When the server API changes, copy its api/openapi.yaml to spec/openapi.yaml.
Security
Report vulnerabilities privately to me@danielalfocea.com. Never put the admin key in a browser: this client is for server-side use. Runtime calls are made by packages in browsers, not by this client in production.
License
MIT, see LICENSE.
Metadata
Release files for scorm-dispatch-client 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| scorm_dispatch_client-0.1.0.tar.gz | 9.9 kB | Without provenance | Details for scorm_dispatch_client-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| scorm_dispatch_client-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for scorm_dispatch_client-0.1.0-py3-none-any.whl |
Total release size: 21.9 kB
Release files / scorm_dispatch_client-0.1.0.tar.gz
| Download URL | scorm_dispatch_client-0.1.0.tar.gz |
|---|---|
| Size | 9.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
42a5e40c8fa8c333187dda1467ca1ea0a070c428ff9967d9a1ce3196cd07198d
|
|
BLAKE2b-256 checksum How to use checksums |
5d5f7982f1c5f035e7521a6eb4ef775fe402a97a6513bba3ac01d44abd3f25fa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|
Release files / scorm_dispatch_client-0.1.0-py3-none-any.whl
| Download URL | scorm_dispatch_client-0.1.0-py3-none-any.whl |
|---|---|
| Size | 12.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d6bbcf926c2ee3503aad4b72cb5add5ef6fea3c9415f1b14804118fe46edcd00
|
|
BLAKE2b-256 checksum How to use checksums |
9904741b27fa3715b73c6a92ce30b672ddb69b5bcf5d7e6e3eb441983fbaa68a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|
Security for scorm-dispatch-client 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.