Skip to main content

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",
        "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.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

modwire_siren-2.0.1.tar.gz (22.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

modwire_siren-2.0.1-py3-none-any.whl (21.8 kB view details)

Uploaded Python 3

File details

Details for the file modwire_siren-2.0.1.tar.gz.

File metadata

  • Download URL: modwire_siren-2.0.1.tar.gz
  • Upload date:
  • Size: 22.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for modwire_siren-2.0.1.tar.gz
Algorithm Hash digest
SHA256 766f9fd8305b50bd672325ae35258d6a803f01bcc26200507f314f8aec4d67d5
MD5 4270f36c25b3756057a01d1049ee2ed7
BLAKE2b-256 c348b4f34a131fe631dc335497908f94a11a5d944f5d9fa6a1d8ea5768f3d14d

See more details on using hashes here.

Provenance

The following attestation bundles were made for modwire_siren-2.0.1.tar.gz:

Publisher: release.yml on modwire/modwire-siren

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file modwire_siren-2.0.1-py3-none-any.whl.

File metadata

  • Download URL: modwire_siren-2.0.1-py3-none-any.whl
  • Upload date:
  • Size: 21.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for modwire_siren-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 13ea7f8978bdc1940775ea494af35e9a782546d86543fc3c36d98606068bdb1c
MD5 ec1d321a02cae6f8e7ba6f6ab9053b78
BLAKE2b-256 83449c621b59174d4e1d9d5c91e7d34bd724f43340aa9593e894a6c6adaab8e2

See more details on using hashes here.

Provenance

The following attestation bundles were made for modwire_siren-2.0.1-py3-none-any.whl:

Publisher: release.yml on modwire/modwire-siren

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page