datadocs
Generate documents from structured data.
datadocs generates Markdown, HTML, or text documents from structured data using
Jinja templates.
The philosophy is simple:
Bring your own data. Bring your own template. Generate documents deterministically.
What it is
datadocs is a small Python package for rendering documents from data you already
prepared.
Templates receive exactly two variables:
data
params
What it is not
datadocs is not a workflow engine, reporting platform, data transformation
framework, query builder, or AI framework.
Installation
pip install datadocs
Optional integrations:
pip install "datadocs[http]"
pip install "datadocs[bigquery]"
CLI
Render from JSON, YAML, or NDJSON:
datadocs render \
--template report.md.j2 \
--data report.json
Write to a file:
datadocs render \
--template report.md.j2 \
--data report.yaml \
--output report.md
Pass template parameters:
datadocs render \
--template report.md.j2 \
--data report.ndjson \
--param period="Q2 2026"
SDK
from datadocs import render
markdown = render(
template_path="templates/report.md.j2",
data={"epics": [{"name": "Search"}]},
params={"period": "Q2 2026"},
)
Use Document with data:
from datadocs import Document
doc = Document(
template_path="templates/report.md.j2",
data={"epics": [{"name": "Search"}]},
params={"period": "Q2 2026"},
)
doc.render_to("report.md")
Use Document with a source:
doc = Document(
template_path="templates/report.md.j2",
source=my_source,
)
A source only needs to provide:
def load_data(self):
...
Architecture
datadocs has one deterministic rendering path:
Source or prepared data -> Document -> render() -> text
The renderer only knows about templates and data. It does not know whether data came from a file, HTTP API, BigQuery query, or a custom source.
Rendering
render() is the core function:
render(template_path, data, params=None)
It loads a Jinja template from disk and renders it with exactly two template variables:
data
params
This keeps templates predictable and avoids hidden context.
Documents
Document is a small wrapper around the rendering contract. It accepts either
prepared data or a source, never both:
Document(template_path="report.md.j2", data=data)
Document(template_path="report.md.j2", source=source)
doc.render() returns text. doc.render_to(path) writes that text to disk.
Sources
A source is any object with:
def load_data(self):
...
Sources are responsible only for loading ordinary Python data. They should not render templates, transform data, build queries, or manage workflows.
Built-in sources follow that rule:
| Source | Responsibility |
|---|---|
HttpSource |
Make one HTTP request and return the parsed JSON response. |
BigQuerySource |
Execute one supplied SQL file and return query rows as dictionaries. |
Custom sources can be added without changing the renderer:
class MySource:
def load_data(self):
return {"epics": [{"name": "Search"}]}
doc = Document(template_path="report.md.j2", source=MySource())
Loaders
File loaders are intentionally separate from sources. The CLI uses loaders for JSON, YAML, and NDJSON files, while integrations use sources.
HTTP
HTTP auth is passed through headers. datadocs does not manage credentials.
datadocs http render \
--url https://example.com/api \
--header "Authorization=Bearer $TOKEN" \
--query-param period="Q2 2026" \
--template report.md.j2
POST JSON from a file:
datadocs http render \
--url https://example.com/api \
--method POST \
--json-body request.json \
--template report.md.j2
The parsed JSON response is used directly as data.
The SDK HTTP source currently supports JSON responses. Use transform to adapt
API-specific response envelopes before they reach a Document:
from datadocs import Document
from datadocs.http import HttpSource
source = HttpSource(
url="https://api.example.com/report",
headers={"Authorization": "Bearer token"},
response_format="json",
transform=lambda data: data["data"]["items"],
)
doc = Document(template_path="report.md.j2", source=source)
response_format="json" is the only implemented response format for now. The
argument is explicit so future formats such as text or bytes can be added without
changing the default behavior.
BigQuery
BigQuery authentication uses Google Application Default Credentials. datadocs
does not expose credential flags.
datadocs bigquery render \
--query queries/report.sql \
--template report.md.j2 \
--project runtime-project \
--billing-project billing-project \
--location EU
Typed query parameters use name:TYPE=value:
datadocs bigquery render \
--query queries/report.sql \
--template report.md.j2 \
--query-param period:STRING="Q2 2026"
The SQL file is executed as supplied. datadocs does not build or transform
queries.
Template example
# Delivery Report
Period: {{ params.period }}
{% for epic in data.epics %}
## {{ epic.name }}
{% endfor %}
Smoke tests
Opt-in smoke tests and demos live under smoke/. They are intentionally kept
outside the normal tests/ suite because they use real external services.
The countries smoke test fetches nested JSON from REST Countries, loads it into BigQuery with a rendered SQL template, queries it back, and renders a Markdown report.
Install the optional integration dependencies:
uv sync --extra dev --extra http --extra bigquery
Authenticate with Google Application Default Credentials:
gcloud auth application-default login
Run the smoke test:
DATADOCS_BQ_PROJECT=bigquery-sandbox-cpvuft6h \
DATADOCS_BQ_DATASET=test_datadocs \
DATADOCS_BQ_LOCATION=EU \
DATADOCS_COUNTRIES_API_TOKEN=your-rest-countries-token \
uv run python smoke/countries/run.py
The script prints only the generated report to stdout. If the configured dataset
already exists, the smoke test reuses it and does not delete it. If the script
creates the dataset, it deletes it by default unless DATADOCS_SMOKE_KEEP_DATASET=1
is set. See smoke/countries/README.md for sample output and cleanup controls.
Development and publishing
The tracked uv.lock keeps development and CI dependencies aligned. Install
the locked environment and run the checks with:
uv sync --locked --all-extras
uv run --locked --all-extras python -m pytest -q
uv run --locked --all-extras ruff check .
uv run --locked --all-extras python -m mypy
CI runs on Python 3.11–3.14 on Linux, plus Python 3.12 on Windows and macOS. It builds the wheel and source distribution and validates their metadata. The automated checks use mocked integrations; the external countries smoke test above remains opt-in.
.github/workflows/publish.yml publishes only after successful checks:
| Event | Result |
|---|---|
| Pull request | Tests and distribution builds; no publishing. |
New main commit with the same version as the latest release tag |
Development build on TestPyPI. |
main commit with a version above the latest release tag |
Create the matching version tag and publish to PyPI. |
Push of a matching vMAJOR.MINOR.PATCH tag |
Verify the tag and tests, then publish to PyPI. |
| No release tags yet | Development builds on TestPyPI until the first release is explicitly tagged. |
The existing v0.1.0 tag is preserved. With version = "0.1.0", a new main
commit publishes a development version such as 0.1.1.dev1230001 to TestPyPI.
The suffix includes the workflow run ID and attempt, so retries get distinct
development versions. That version is applied only in the build checkout;
the repository's version is unchanged.
To request the next production release, update the version and lockfile:
uv version 0.1.1
Commit pyproject.toml and uv.lock, preferably in a release PR, and merge into
main. pyproject.toml is the version source. Release versions use stable
MAJOR.MINOR.PATCH numbers, compared with Python packaging version rules.
The workflow rejects regressions and tag/version mismatches and never moves an
existing tag. Tags identify the commit CI actually tested. A failed upload can
be retried on that tagged commit without changing the release version.
Before uploading, the publishing workflow installs both the wheel and source
distribution in isolated environments. Each installation must report the
expected distribution version and render a sample document through both the
SDK and the installed datadocs command.
Publishing uses OIDC trusted publishers, with no stored PyPI API token. The publisher settings on each registry must match this workflow:
| Setting | PyPI | TestPyPI |
|---|---|---|
| Project | datadocs |
datadocs |
| Repository owner | AndresVelasco |
AndresVelasco |
| Repository | datadocs |
datadocs |
| Workflow filename | publish.yml |
publish.yml |
| GitHub environment | pypi |
testpypi |
The environment field in the publisher configuration is optional; if supplied, it must match the corresponding job. See the PyPI trusted-publishing documentation.
Future integrations
Future integrations such as datadocs.langchain should be optional layers on top
of the deterministic rendering pipeline.
Metadata
Release files for datadocs 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| datadocs-0.1.1.tar.gz | 8.7 kB | With provenance | Details for datadocs-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| datadocs-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for datadocs-0.1.1-py3-none-any.whl |
Total release size: 22.2 kB
Release files / datadocs-0.1.1.tar.gz
| Download URL | datadocs-0.1.1.tar.gz |
|---|---|
| Size | 8.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2b26ec1d46d5637112386d18e8b03ef44856203eab0a0c838dc86caa8f8f0724
|
|
BLAKE2b-256 checksum How to use checksums |
81e3b6baef6a29e56ea0f84dacd7f24789ece9352457576268a115091ea10b80
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logRelease files / datadocs-0.1.1-py3-none-any.whl
| Download URL | datadocs-0.1.1-py3-none-any.whl |
|---|---|
| Size | 13.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
42d846b4da2476aafe8c419de1893adc4b6a39cbf4c7c96731ebd2a5cc383ed6
|
|
BLAKE2b-256 checksum How to use checksums |
c2bf020714586b843a5ff1b876f6799b94eadd5f65226c9e464609778c7a4a32
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logSecurity for datadocs 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for datadocs-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.