Metadata-Version: 2.5
Name: ouroboros-jinja
Version: 1.2.0
Summary: Expand Jinja2 templates that back-reference values in the same document
Project-URL: Homepage, https://github.com/talamus/python-ouroboros-jinja
Project-URL: Repository, https://github.com/talamus/python-ouroboros-jinja
Project-URL: Issues, https://github.com/talamus/python-ouroboros-jinja/issues
Author-email: Tero Niemi <talamus@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: configuration,jinja2,templating,toml,yaml
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: jinja2>=3.1.6
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: tomli>=2.0; python_version < '3.11'
Description-Content-Type: text/markdown

# Ouroboros Jinja

Expand Jinja2 templates that back-reference other values in the same document
— the snake eats its own tail.

```yaml
# defaults.yaml
slug: ~
domain: "{{ slug }}.nimbus.fi"
volume-name: "{{ slug }}-volume"
permanent-ip-name: "{{ domain }}"
```

```toml
# server.toml
name = "Prominence 2: Hasturian Era"
slug = "hasturian"
```

```python
import ouroboros_jinja

config = ouroboros_jinja.load("defaults.yaml", "server.toml")
# {
#   "slug": "hasturian",
#   "domain": "hasturian.nimbus.fi",
#   "volume-name": "hasturian-volume",
#   "permanent-ip-name": "hasturian.nimbus.fi",
#   "name": "Prominence 2: Hasturian Era"
# }
```

## API

- `expand(*mappings)` — deep-merge mappings (later ones win) and expand
  every string value as a Jinja2 template.
- `loads(*yaml_texts)` — parse YAML texts, merge and expand.
- `load(*paths)` — read files (`.toml` via `tomllib`, everything else as
  YAML), merge and expand.

Templates may reference any top-level key, including nested content
(`{{ an_object.description }}`) and lists (`{% for port in ports %}`).
Values are expanded lazily and recursively, in dependency order.

A value inside a nested mapping may back-reference keys of the same
mapping, as long as those keys are defined above the reference:

```yaml
protocol: https
server:
    slug: production
    domain: "{{ server.slug }}.example.com"
    url: "{{ protocol }}://{{ server.domain }}/"
```

## Merging

When several documents are given, nested mappings are merged key by key, so
a later document can override one leaf without restating its siblings:

```yaml
# assets.yaml
assets:
    tractor:
        name: "Aurora"
    trailer:
        name: "Beatrice"
stack:
    engine: "{{ assets.tractor.name }}"
    payload: "{{ assets.trailer.name }}"
```

```yaml
# addon.yaml
assets:
    tractor:
        name: "Charlie"
```

`load("assets.yaml", "addon.yaml")` keeps `assets.trailer` and only replaces
`assets.tractor.name`, so `stack.engine` expands to `"Charlie"` and
`stack.payload` to `"Beatrice"`.

Anything that is not a mapping on *both* sides is replaced outright — a list
in a later document wins over an earlier list, it is not concatenated. The
input mappings are never mutated.

## Semantics

- Reference cycles raise `CircularReferenceError` with the chain spelled
  out: `Circular reference detected: item_1 -> item_2 -> item_3 -> item_1`.
- Unknown variables raise `ExpansionError` (Jinja2 `StrictUndefined`).
- A top-level key with a null value (`slug: ~`) is a *required override*:
  referencing it before it has been overridden raises `ExpansionError`
  instead of silently interpolating `"None"`.
- Only strings containing `{{` or `{%` are touched; other values
  (ints, lists, …) pass through unchanged.

## Testing

```sh
uv run pytest
```
