Compile OpenAPI API graphs into Siren hypermedia documents.
Project description
modwire-siren
modwire-siren compiles a complete OpenAPI 3.1 document into a reusable Siren engine. At request
time, the engine turns application data and permissions into a Siren response with concrete links
and authorized actions.
Requires Python 3.12 or later.
Install
python -m pip install modwire-siren
For local development:
uv sync --all-groups --frozen
make verify
Version 2 is a breaking rewrite. See MIGRATION.md when upgrading from version 1.
Usage
This section is generated from the docstrings of the supported root imports. Run make docs after changing a public API example or its guidance.
siren
Compile a complete OpenAPI 3.1 document into a reusable Siren engine.
Call this once during application startup, then call engine.project(context) for each
negotiated Siren response. OpenAPI defines links, methods, and candidate fields; the context's
capabilities decide which candidate actions are present in that response.
Example
from modwire_siren import SirenContext, siren
openapi = {
"openapi": "3.1.1",
"info": {"title": "Records API", "version": "1.0"},
"paths": {
"/records": {
"get": {
"operationId": "list_records",
"responses": {"200": {"description": "OK"}},
}
},
"/records/{record_id}": {
"parameters": [
{
"name": "record_id",
"in": "path",
"required": True,
"schema": {"type": "string"},
}
],
"get": {
"operationId": "get_record",
"responses": {"200": {"description": "OK"}},
},
"patch": {
"operationId": "rename_record",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["title"],
"properties": {"title": {"type": "string"}},
}
}
}
},
"responses": {"200": {"description": "OK"}},
},
},
},
}
engine = siren(openapi)
document = engine.project(
SirenContext(
base_url="https://api.example.com",
resource="record",
value={"id": "42", "title": "Architecture"},
capabilities=frozenset({"get_record", "rename_record"}),
)
)
assert document["actions"] == [
{"name": "get_record", "href": "https://api.example.com/records/42", "method": "GET"},
{
"name": "rename_record",
"href": "https://api.example.com/records/42",
"method": "PATCH",
"type": "application/json",
"fields": [{"name": "title", "type": "string", "required": True}],
},
]
OpenAPI requirements
The final plural static segment of a route is a collection; adding one path parameter forms its entity route. Prefixes and nested collections are supported, including:
/api/v1/records
/accounts/{account}/records
/accounts/{account}/records/{record}
Every non-root HTTP operation needs a unique operationId. Operations on collection or
entity paths, including their static subpaths, belong to that resource. The longest matching
route wins, so a nested resource owns /accounts/{account}/records rather than account.
Standalone endpoints whose final segment is static, such as
/scaffoldings/converge or /scaffoldings/{scaffolding_id}/preview, are accepted as
documented commands when no resource owns them. They are outside the Siren resource graph,
so they are not projected as actions or accepted as capabilities. Parameters must be unchanged
from the owning route: adding, removing, renaming, or reordering them is unsupported.
Ambiguous routes, unowned routes ending in a parameter, duplicate resource names, missing
operation IDs, and invalid OpenAPI fail compilation explicitly.
Local #/components/parameters, #/components/requestBodies, and #/components/schemas
references are resolved for actions. External and path-item references
are unsupported. Action fields come from query parameters and JSON request-body properties;
path parameters remain routing values. Header and cookie parameters are unsupported. If a
request body declares content, it must include application/json; that media type is projected
as the Siren action's type.
Framework integration is one startup call
Do not recreate your framework's routes, actions, or OpenAPI document in Siren-specific code.
Give the framework-generated document directly to siren() once, after routes are registered:
# FastAPI
engine = siren(app.openapi())
# Django Ninja / Django Ninja Extra
engine = siren(api.get_openapi_schema())
That is the integration point: siren() derives the graph from the same contract your
framework already exposes. At response time, only supply the request-specific data and allowed
operation IDs in SirenContext, then return engine.project(context) as
application/vnd.siren+json. No builder, resource registration, route duplication, or
framework adapter is required.
Mounted entry point
Set root_path when the Siren entry point is mounted away from /:
engine = siren(app.openapi(), root_path="/siren/")
Framework adapters are intentionally outside this package because the integration is already the small startup call shown above.
SirenContext
Supply runtime state used to project a Siren document.
Use the default "entity" scope for one resource, "collection" for a list, and "root"
for an API entry point. A resource is required outside root scope and is the singular name
derived from the collection route: "record" for /records. If the same resource appears
in multiple nested routes, path_values selects the route with matching parent parameters.
Collection example
context = SirenContext(
base_url="https://api.example.com",
scope="collection",
resource="record",
items=(
{"id": "42", "title": "Architecture"},
{"id": "43", "title": "Systems"},
),
query=(("page", 2),),
capabilities=frozenset({"list_records"}),
)
document = engine.project(context)
assert document["links"] == [
{"rel": ["self"], "href": "https://api.example.com/records?page=2"}
]
| Field | Purpose |
|---|---|
base_url |
Public origin joined with OpenAPI paths, for example https://api.example.com. |
scope |
"root", "collection", or "entity". Root contexts have no resource. |
resource |
Derived singular resource name. Required outside the root. |
value |
Entity properties or collection-level properties. Also supplies entity path parameters. |
items |
Tuple of entity mappings for a collection. |
path_values |
Path parameters missing from value, such as a parent resource ID. |
query |
Ordered (name, value) pairs added to self and action links. |
capabilities |
Permitted OpenAPI operationId values to advertise as Siren actions. |
Nested routes and queries
For /accounts/{account}/records/{record}, supply the parent parameter separately:
context = SirenContext(
base_url="https://api.example.com",
resource="record",
path_values={"account": "acme"},
value={"record": "42", "title": "Architecture"},
)
Path values are percent-encoded. Query pairs retain their order and repeated keys. Query
values must be scalar; booleans become lowercase true or false, and None becomes
an empty value. The root self link receives its query pairs, while root resource links do not.
Public API
The supported root imports below are generated from modwire_siren.__all__.
| Symbol | Purpose | Primary API |
|---|---|---|
SirenContext |
Supply runtime state used to project a Siren document. | — |
siren |
Compile a complete OpenAPI 3.1 document into a reusable Siren engine. | — |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file modwire_siren-2.0.2.tar.gz.
File metadata
- Download URL: modwire_siren-2.0.2.tar.gz
- Upload date:
- Size: 22.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
879da56166fee143da267696ed38b3f7697cb3d820efd2efa333b95d88e70626
|
|
| MD5 |
e405232fa36b759db2fc2eba21d6b447
|
|
| BLAKE2b-256 |
b6fd7104c9174ece57509e0183f85defa6fa5a0a3b9e06a4f71c685c17b0395e
|
Provenance
The following attestation bundles were made for modwire_siren-2.0.2.tar.gz:
Publisher:
release.yml on modwire/modwire-siren
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
modwire_siren-2.0.2.tar.gz -
Subject digest:
879da56166fee143da267696ed38b3f7697cb3d820efd2efa333b95d88e70626 - Sigstore transparency entry: 2227427026
- Sigstore integration time:
-
Permalink:
modwire/modwire-siren@9bc0434f84867a5a8b0a9441d8e26046629ed0d0 -
Branch / Tag:
refs/tags/v2.0.2 - Owner: https://github.com/modwire
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9bc0434f84867a5a8b0a9441d8e26046629ed0d0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file modwire_siren-2.0.2-py3-none-any.whl.
File metadata
- Download URL: modwire_siren-2.0.2-py3-none-any.whl
- Upload date:
- Size: 22.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
49413665ccdeee0c032505793859934b4361aef7ad33a102e2a696e1e026c7ca
|
|
| MD5 |
d8b21a39abe3ba132b4b3f1bf741e97f
|
|
| BLAKE2b-256 |
5e93ba123f1323128b9520640f2d081a01af78759d0014dcfacbe733e8e4c893
|
Provenance
The following attestation bundles were made for modwire_siren-2.0.2-py3-none-any.whl:
Publisher:
release.yml on modwire/modwire-siren
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
modwire_siren-2.0.2-py3-none-any.whl -
Subject digest:
49413665ccdeee0c032505793859934b4361aef7ad33a102e2a696e1e026c7ca - Sigstore transparency entry: 2227427180
- Sigstore integration time:
-
Permalink:
modwire/modwire-siren@9bc0434f84867a5a8b0a9441d8e26046629ed0d0 -
Branch / Tag:
refs/tags/v2.0.2 - Owner: https://github.com/modwire
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9bc0434f84867a5a8b0a9441d8e26046629ed0d0 -
Trigger Event:
release
-
Statement type: