Metadata-Version: 2.4
Name: modwire-siren
Version: 3.1.0
Summary: Compile OpenAPI API graphs into Siren hypermedia documents.
Project-URL: Repository, https://github.com/modwire/modwire-siren
Project-URL: Issues, https://github.com/modwire/modwire-siren/issues
Author: 9orky
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: gherkin-official>=29
Requires-Dist: jsonschema[format-nongpl]>=4.26
Requires-Dist: openapi-spec-validator<1,>=0.9
Requires-Dist: pydantic>=2.12
Requires-Dist: wireup>=2.12.0
Provides-Extra: dev
Requires-Dist: build>=1.3; extra == 'dev'
Requires-Dist: django-ninja-extra<1,>=0.30; extra == 'dev'
Requires-Dist: django<6.1,>=5.1; extra == 'dev'
Requires-Dist: fastapi<1,>=0.115; extra == 'dev'
Requires-Dist: modwire-cli>=2.0.0; extra == 'dev'
Requires-Dist: packaging>=25; extra == 'dev'
Requires-Dist: pytest-bdd<9,>=8.1; extra == 'dev'
Requires-Dist: pytest>=9; extra == 'dev'
Requires-Dist: ruff>=0.14; extra == 'dev'
Requires-Dist: twine>=6; extra == 'dev'
Description-Content-Type: text/markdown

# 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

```bash
python -m pip install modwire-siren
```

For local development, install `uv` and use the locked environment:

```bash
UV_CACHE_DIR=.dump/uv-cache uv sync --locked --all-groups
make verify
```

Version 2 is a breaking rewrite. See [MIGRATION.md](MIGRATION.md) when upgrading from version 1.

<!-- generated:public-api:start -->
## 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

```python
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",
                                "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"}),
    )
)

payload = document.model_dump(by_alias=True, mode="json", exclude_none=True)

assert payload["actions"][0] == {
    "name": "get_record",
    "href": "https://api.example.com/records/42",
    "method": "GET",
}
```

#### 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. Every non-root HTTP operation
needs a unique `operationId`. Local `#/components/parameters`, `#/components/requestBodies`,
and `#/components/schemas` references are resolved; external and path-item references are not.

#### Action field support matrix

Path parameters substitute into action URLs and never become fields. Query parameters and
properties of an `application/json` object body become fields:

| OpenAPI schema | Siren field type |
| --- | --- |
| `string`, including `uuid` | `text` |
| formatted `string` | matching Siren field type |
| `integer` or `number` | `number` |
| `boolean` | `checkbox` |
| flat primitive array or repeated query parameter | `text` |
| scalar `enum` | `radio` with selectable values |
| flat array with an item `enum` | `checkbox` with selectable values |
| object, map, or nested array | delegated; no synthetic field |
| header or cookie parameter | delegated; no synthetic field |
| one non-JSON request media type | delegated action with that media type |

`email`, `uri`, `date`, `date-time`, and `time` map to `email`, `url`, `date`,
`datetime-local`, and `time`, respectively.

Required and nullable controls compile as ordinary standard Siren fields: validation remains
server-enforced because official Siren has no `required` or `nullable` members. A flat array
has one named `text` field; the OpenAPI serialization contract remains authoritative for
submission. `allOf` scalar fragments and a `oneOf` or `anyOf` containing one scalar plus
`null` are accepted when they normalize unambiguously.

Structured values, header and cookie parameters, and one non-JSON request body are delegated
to the API contract and client transport; official Siren has no standard members for their
paths, serialization, or placement. Multiple non-JSON media types, ambiguous compositions,
unsupported string formats, and `HEAD`, `OPTIONS`, or `TRACE` operations are rejected during
this startup call.

Call `audit(openapi)` first when a consumer needs a deterministic list of every current
incompatibility before using this strict fail-fast entry point.

#### Framework integration is one startup call

Give the framework-generated document directly to `siren()` after routes are registered:

```python
engine = siren(app.openapi())  # FastAPI
engine = siren(api.get_openapi_schema())  # Django Ninja / Django Ninja Extra
```

#### HTTP response contract

`engine.project(context)` returns a `SirenDocument`, not a dictionary. Serialize it with
`document.model_dump(by_alias=True, mode="json", exclude_none=True)` and send that payload as
`application/vnd.siren+json`. The document contains only official Siren members; action fields
never include the non-standard `required` member.

Set `root_path` when the Siren entry point is mounted away from `/`.

### `audit`

Inspect a valid OpenAPI document against the current official-Siren support boundary.

Call this during startup before `siren(openapi)` when a consumer needs every currently
unsupported construct at once. The report exposes typed findings and `render()` for terminal
or CI output; `siren(openapi)` remains the strict fail-fast compilation entry point.

### `SirenRelationship`

Describe a runtime relationship to another OpenAPI resource.

A relationship projects as a navigational link by default. Set `embedded` when the related
resource values should be included as a Siren embedded representation instead.

### `SirenLink`

Describe a navigational Siren link.

### `SirenFieldValue`

Describe a selectable Siren action field value.

### `SirenField`

Describe an official Siren action field.

### `SirenEmbeddedRepresentation`

Represent a Siren sub-entity embedded in full.

### `SirenEmbeddedLink`

Represent a Siren sub-entity linked by URI.

### `SirenDocument`

Represent an official Siren entity document.

Project an engine request into this immutable public value, then serialize it with
`model_dump(by_alias=True, mode="json", exclude_none=True)` for an
`application/vnd.siren+json` response. Navigation belongs in `links`; embedded sub-entities
belong in `entities`.

### `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.

| Field | Purpose |
| --- | --- |
| `base_url` | Public origin joined with OpenAPI paths. |
| `scope` | `"root"`, `"collection"`, or `"entity"`. |
| `resource` | Derived singular resource name; required outside root. |
| `value` | Entity or collection properties and entity path parameters. |
| `items` | Entity mappings for a collection. |
| `item_capabilities` | Optional permitted operation IDs for each collection item. |
| `relationships` | Linked or embedded related resources for this document. |
| `path_values` | Missing path parameters, such as a parent resource ID or a root command target. |
| `query` | Ordered query pairs for self and action links. |
| `capabilities` | Permitted OpenAPI `operationId` values. |

### `SirenCompatibilityReport`

Expose deterministic OpenAPI-to-Siren compatibility findings.

### `SirenCompatibilityFinding`

Describe one OpenAPI construct outside the current official-Siren boundary.

### `SirenAction`

Describe an available Siren action.

### `ModwireSirenError`

Indicate a Modwire Siren operation failure.

## Public API

The supported root imports below are generated from `modwire_siren.__all__`.

| Symbol | Purpose | Primary API |
| --- | --- | --- |
| `ModwireSirenError` | Indicate a Modwire Siren operation failure. | — |
| `SirenAction` | Describe an available Siren action. | — |
| `SirenCompatibilityFinding` | Describe one OpenAPI construct outside the current official-Siren boundary. | — |
| `SirenCompatibilityReport` | Expose deterministic OpenAPI-to-Siren compatibility findings. | `compatible: <class 'bool'>`<br>`render() -> <class 'str'>` |
| `SirenContext` | Supply runtime state used to project a Siren document. | — |
| `SirenDocument` | Represent an official Siren entity document. | — |
| `SirenEmbeddedLink` | Represent a Siren sub-entity linked by URI. | — |
| `SirenEmbeddedRepresentation` | Represent a Siren sub-entity embedded in full. | — |
| `SirenField` | Describe an official Siren action field. | — |
| `SirenFieldValue` | Describe a selectable Siren action field value. | — |
| `SirenLink` | Describe a navigational Siren link. | — |
| `SirenRelationship` | Describe a runtime relationship to another OpenAPI resource. | — |
| `audit` | Inspect a valid OpenAPI document against the current official-Siren support boundary. | — |
| `siren` | Compile a complete OpenAPI 3.1 document into a reusable Siren engine. | — |
<!-- generated:public-api:end -->
