Renders Jinja2 templates from a declarative YAML config
Project description
jflow
Renders Jinja2 templates from a declarative YAML config. Data sources — Excel, CSV, and file directories — are declared once in config and injected into templates as named variables. Adding a new data source never requires touching code.
Installation
pip install jinja-flow
# or run directly without installing:
uvx --from jinja-flow jflow config.yaml
Config file
A config file declares sources and a list of render jobs.
sources:
products: { type: excel, file: data/products.xlsx, sheet: Sheet1 }
specs: { type: csv, file: data/specs.csv }
docs: { type: files, path: ./docs, recursive: true }
contracts: { type: files, path: ./contracts }
renders:
- output: "outputs/by-item/{{ item.name }}.txt"
for: { products: item }
template: prompts/analyze.j2
- output: outputs/joined/everything.txt
for: { products: item, join: "\n---\n" }
template: prompts/all_prompts.j2
# any iteration (if needed) done inside the template
# if no output:, stdout is used
- template: prompts/internal-for.j2
# or inline:
- template: |-
{% for item in products %}
{{ item.name }}: {{ item.category }}
---
{% endfor %}
sources
Each key under sources becomes a named variable injected into every
template. Three source types are supported.
type: excel
Loads a worksheet from an Excel file as an iterable of row dicts.
| Key | Required | Description |
|---|---|---|
file |
yes | Path to the .xlsx file |
sheet |
no | Sheet name. Defaults to the first sheet |
type: csv
Loads a CSV file as an iterable of row dicts. Behaves identically to
type: excel in templates — same iteration, same .lookup() method.
| Key | Required | Description |
|---|---|---|
file |
yes | Path to the .csv file |
delimiter |
no | Field delimiter. Default , |
encoding |
no | File encoding. Default utf-8 |
type: files
Wraps a directory for file lookup. Files are read as plain text.
| Key | Required | Description |
|---|---|---|
path |
yes | Directory path |
recursive |
no | Search subdirectories. Default false |
encoding |
no | File encoding. Default utf-8 |
renders
A list of render jobs.
Each render entry has the following keys:
| Key | Required | Description |
|---|---|---|
template |
yes | Path to a .j2 file, or an inline block scalar (|-) |
output |
no | Output path. Omit to write to stdout |
for |
no | Driver iteration config (see below). Omit for template iteration |
Inline templates
template accepts either a file path or an inline block scalar. Use
|- to strip the trailing newline, which is almost always what you want
for prompt content.
renders:
- output: "outputs/{{ item.name }}.txt"
for: { products: item }
template: |-
You are analyzing: {{ item.name }}
Category: {{ item.category }}
{% set spec = docs.find(item.name ~ "_spec.md") %}
{% if spec %}{{ spec.content }}{% endif %}
Inline templates can use {% include %} to pull in partials:
- for: { products: item }
template: |-
{% include "prompts/header.j2" %}
{{ item.name }}: {{ item.description }}
for:
When present, the driver iterates a source and renders the template once per row. All keys are inline in a flow mapping.
| Key | Required | Description |
|---|---|---|
{source}: {var} |
yes | Source name and the variable exposed per row |
join |
no | String used to join per-row outputs. Invalid without for: |
Iteration modes
Driver iteration (for: present) — the driver loops over the source
and renders the template once per row. Use when you need per-row output
files, or to join rows into a single file with a separator.
renders:
- output: "outputs/{{ item.name }}.txt"
for: { products: item }
template: prompts/item.j2
- output: outputs/all.txt
for: { products: item, join: "\n---\n" }
template: prompts/all.j2
Template iteration (no for:) — the driver renders once, the
template loops internally using {% for %} and {% include %}. Natural
when per-row output files are not needed.
renders:
- template: prompts/internal-for.j2
{# prompts/internal-for.j2 #}
{% for item in products %}
{% include "prompts/item.j2" %}
---
{% endfor %}
output
A plain path or Jinja2 expression. Always top-level. Omit to write to stdout.
Per-row files — Jinja2 expression evaluated against the current row.
Only meaningful with for:.
- output: "outputs/{{ item.name }}.txt"
for: { products: item }
template: prompts/item.j2
Single file — plain path. Use join inside for: to concatenate
per-row outputs. join without for: is a configuration error.
- output: outputs/all.txt
for: { products: item, join: "\n---\n" }
template: prompts/item.j2
Stdout — omit output entirely. Stdout is the default when no
output path is specified:
- for: { products: item, join: "\n---\n" }
template: prompts/item.j2
- template: prompts/internal-for.j2
Templates
Templates are standard Jinja2 (.j2 by convention). The following
variables are always available:
- Every source declared in
sources— as its source object (see below) - The iteration variable named in
for(e.g.item), if driver iteration is used — a dict of the current row's column values
Accessing row data
In driver iteration, the current row is accessed via the named variable:
{{ item.name }}
{{ item.category }}
In template iteration, the source is accessed directly:
{% for item in products %}
{{ item.name }}
{{ item.category }}
{% endfor %}
Column names come directly from the first row of the Excel or CSV file.
FileSource — type: files
File sources expose two methods.
.find(pattern) — search for a single file by glob pattern. Returns
a FileResult (see below). pattern accepts standard glob syntax:
* matches any sequence of characters, ? matches a single character,
[seq] matches any character in seq. Use * around a variable to
match regardless of surrounding text in the filename.
{# exact variable substitution #}
{% set spec = docs.find(item.name ~ "_spec.md") %}
{# variable is part of a longer filename #}
{% set brief = docs.find("brief " ~ item.name ~ " final.txt") %}
{# variable somewhere in the name — wildcards either side #}
{% set f = docs.find("*" ~ item.code ~ "*.txt") %}
{# variable in the middle, fixed prefix and suffix #}
{% set f = docs.find("report_" ~ item.year ~ "_summary.md") %}
Returns the first match if multiple files match the pattern.
.list(pattern) — return all matching files as a list of
FileResult objects. Pattern defaults to *. Accepts the same glob
syntax as .find().
{% for f in docs.list("*.md") %}
{{ f.name }}: {{ f.content }}
{% endfor %}
FileResult
Returned by .find() and .list(). Always safe to use — a missing file
does not raise an exception.
| Attribute | Type | Description |
|---|---|---|
.exists |
bool |
True if the file was found |
.content |
str |
File contents. Empty string if not found |
.data |
object|None |
Parsed object for .json and .yaml/.yml files. None for all other types. Raises on malformed content |
.name |
str |
Filename with extension (e.g. spec.md) |
.stem |
str |
Filename without extension (e.g. spec) |
.path |
Path|None |
Absolute Path object, or None |
FileResult is falsy when the file does not exist, so the idiomatic
pattern is:
{% set spec = docs.find(item.name ~ "_spec.md") %}
{% if spec %}
{{ spec.content }}
{% endif %}
.data gives parsed access to JSON and YAML files, with dot and bracket
notation:
{% set meta = docs.find("meta_" ~ item.name ~ ".json") %}
{% if meta %}
{{ meta.data.description }}
{{ meta.data.author.name }}
{{ meta.data.tags[0] }}
{% endif %}
{% set cfg = docs.find(item.name ~ ".yaml") %}
{% if cfg %}
{{ cfg.data.version }}
{% endif %}
str(result) returns .content, so a FileResult can be interpolated
directly:
{{ docs.find(item.name ~ "_spec.md") }}
Tabular sources — type: excel / type: csv
Non-primary tabular sources (i.e. not the one named in for) are
available for cross-referencing via .lookup().
.lookup(key, value) — return the first row where row[key] == value
as a dict. Returns an empty dict if not found.
{% set meta = specs.lookup("product_id", item.product_id) %}
{% if meta %}
Version: {{ meta.version }}
{% endif %}
.rows — list of all rows as dicts.
.headers — list of column names.
Example
Directory layout
project/
├── config.yaml
├── render.py
├── sources.py
├── data/
│ ├── products.xlsx
│ └── specs.csv
├── docs/
│ ├── WidgetA_spec.md
│ └── WidgetB_spec.md
├── contracts/
│ └── WidgetA_contract.txt
├── prompts/
│ └── analyze.j2
└── outputs/ ← created automatically
config.yaml
sources:
products: { type: excel, file: data/products.xlsx }
specs: { type: csv, file: data/specs.csv }
docs: { type: files, path: ./docs, recursive: true }
contracts: { type: files, path: ./contracts }
renders:
- output: "outputs/{{ item.name }}.txt"
for: { products: item }
template: prompts/analyze.j2
prompts/analyze.j2
You are a product analyst. Analyze the following product.
## Product
Name: {{ item.name }}
Category: {{ item.category }}
Region: {{ item.region }}
{% set spec = docs.find(item.name ~ "_spec.md") %}
{% if spec %}
## Specification
{{ spec.content }}
{% else %}
## Specification
No specification document found for {{ item.name }}.
{% endif %}
{% set contract = contracts.find(item.name ~ "*.txt") %}
{% if contract %}
## Contract Notes
{{ contract.content }}
{% endif %}
{% set meta = specs.lookup("product_id", item.product_id) %}
{% if meta %}
## Supplementary Data
Description: {{ meta.description }}
Version: {{ meta.version }}
{% endif %}
## Task
Summarize the product, highlight any risks, and suggest improvements.
Running
# write one file per product to outputs/
uvx jflow config.yaml
# preview without writing
Error handling
The environment uses StrictUndefined — referencing a variable that does
not exist in the current context raises an UndefinedError immediately,
rather than silently rendering an empty string. This catches typos in
column names early.
A FileResult for a missing file is always safe to use — .exists is
False, .content is "", and the object is falsy. No exception is
raised for missing files unless you explicitly access .content on a
None-path result outside of the normal attribute interface.
Config validation errors are raised at load time, before any rendering begins. The following are errors:
joinpresent in a render entry withoutfor—joinis only valid with driver iterationoutput.fileusing a Jinja expression (e.g.{{ item.name }}) withoutfor— per-row filenames require driver iterationforreferencing a source not declared insourcesiterate_over(legacy) orforpointing to atype: filessource — onlyexcelandcsvsources are iterable
Runtime errors raised during rendering:
- Accessing
.dataon a malformed.jsonor.yamlfile raises immediately — malformed structured files are never silently ignored
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
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 jinja_flow-0.1.0.tar.gz.
File metadata
- Download URL: jinja_flow-0.1.0.tar.gz
- Upload date:
- Size: 9.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c1b2437ac5db0b987df089739cf84c7120c41786857bf884d00b67587b3b3442
|
|
| MD5 |
38e87653b5ce8af17b98d89b789ecc66
|
|
| BLAKE2b-256 |
f7278990770ea712bce5809e398c45c7ed1d17ec6edf51faeabd6cecd10b3910
|
Provenance
The following attestation bundles were made for jinja_flow-0.1.0.tar.gz:
Publisher:
publish.yml on ogheorghies/jinja-flow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jinja_flow-0.1.0.tar.gz -
Subject digest:
c1b2437ac5db0b987df089739cf84c7120c41786857bf884d00b67587b3b3442 - Sigstore transparency entry: 1125535139
- Sigstore integration time:
-
Permalink:
ogheorghies/jinja-flow@b7bf2b5eed05cb72cb803109c26c0ae4d697670b -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ogheorghies
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b7bf2b5eed05cb72cb803109c26c0ae4d697670b -
Trigger Event:
release
-
Statement type:
File details
Details for the file jinja_flow-0.1.0-py3-none-any.whl.
File metadata
- Download URL: jinja_flow-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a8577deef2e6325143988a8fcfebfa79a95eef21454f0b33a1f913ad6f282e3c
|
|
| MD5 |
677a25e3d67ed91d057e751924766ae6
|
|
| BLAKE2b-256 |
53ac03e3f58763458c845180fbb8111cc8715a3e9111e1dba49431ee78619ed5
|
Provenance
The following attestation bundles were made for jinja_flow-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on ogheorghies/jinja-flow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jinja_flow-0.1.0-py3-none-any.whl -
Subject digest:
a8577deef2e6325143988a8fcfebfa79a95eef21454f0b33a1f913ad6f282e3c - Sigstore transparency entry: 1125535203
- Sigstore integration time:
-
Permalink:
ogheorghies/jinja-flow@b7bf2b5eed05cb72cb803109c26c0ae4d697670b -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ogheorghies
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b7bf2b5eed05cb72cb803109c26c0ae4d697670b -
Trigger Event:
release
-
Statement type: