forktex-documents
A framework for generating documents from structured data. It ships the vocabulary and the machinery — not document types: an invoice, a contract, a delivery note are things a consumer defines, because what they must contain is a fact about a business and a jurisdiction, not about a renderer.
data (JSON) → canonical (Pydantic) → block tree → HTML → PDF
↘ read directly for a machine projection (XML, JSON…)
A PDF here is a view over an object, never a source of truth. The canonical
record is the document; the page is one projection of it. A machine projection
reads the canonical directly rather than the block tree — you cannot recover
"19% VAT on this line" from a table cell that says 19%, which is why the two
representations both exist.
Install
pip install forktex-documents # canonical, blocks, HTML
pip install "forktex-documents[pdf]" # + PDF, via WeasyPrint
PDF is an extra because WeasyPrint pulls native libraries (cairo, pango) that a consumer producing only HTML or a machine projection should not have to install.
What it does
A consumer declares its own canonical, and gets validation, composition and rendering from the framework:
from forktex_documents import FamilyCanonical, Mandate, ParagraphBlock, known
@known
class DeliveryNote(FamilyCanonical):
family = "logistics"
mandates = (
Mandate(
path="carrier",
authority="acme-logistics",
reference="ops:carrier-is-named",
reason="A note with no carrier cannot be chased when the goods do not arrive.",
),
)
reference: str
carrier: str
def blocks(self, profile):
return (ParagraphBlock(text=f"Carried by {self.carrier}."),)
note = DeliveryNote(reference="DN-1", carrier="Speedy")
note.check() # raises if a mandate is unsatisfied
note.blocks(profile) # a typed block tree, ready to project
See examples/showcase.py for a document exercising
every block kind.
The ideas worth knowing
Derived values are never stored. A consumer's totals, VAT subtotals and
expiry dates are computed_fields on its own canonical — and this package is
what makes that hold. Every model sets extra="forbid", so a record carrying a
hand-written total is a validation error rather than a silent disagreement,
and a stored total that was tampered with is dropped and recomputed on load.
A structural guarantee rather than a review rule.
Mandates are cited and conditional. What a jurisdiction requires is data: a path, the authority, the legal reference, and why. Conditions read computed flags, so a retail stock-reception note demands a selling price while a cost-method one does not, and a delivery showing quantity differences demands a three-member committee and a third copy — because those are the rules the form actually carries.
Fiscal facts are time-versioned. VAT rates resolve against the document's own date, so re-rendering a July 2025 Romanian invoice reproduces 19% rather than silently restating it at the current 21%. A date no schedule covers raises instead of guessing.
Blocks render themselves. Twenty-five typed block kinds form a closed discriminated union, each emitting its own HTML. There is no template language deciding layout — the only templating is sandboxed text substitution for locale fragments, where a clause has placeholders to resolve.
Identifiers are validated, not merely shaped. CUI and CNP check digits, IBAN mod-97 across every country's length rule, per-member-state VAT formats. A transposed digit in a national id files a real document against the wrong person; the checksum is what catches it.
What is not here
No invoice, no contract, no delivery note, and no jurisdiction. There is no
RO_VAT_RATES, no CUI or CNP check digit, and no default country, language or
currency anywhere — a default jurisdiction is a wrong jurisdiction for everyone
else, quietly. A consumer supplies its own:
- a
LegislationProfilewith its date format, separators, currency and mandates - a
VatSchedulewith its rates and their validity windows - national identifier validators, registered into
NATIONAL_TAX_ID_VALIDATORS - the numbers already spent, passed to a register as
BURNED
Layout
domain/ blocks, canonical, mandates, entities, fiscal, localization, theme,
registry, series ↑ no I/O — `grep Path domain/` is empty, by test
render/ the HTML projector, inline SVG charts, the asset port
Imports run one way: domain → render → facade.
Development
make install # poetry install --with dev
make ci # format, lint, licences, docs, types, audit, tests, build, smoke
make reference # rewrite committed reference renders — then read the diff
make docs # render the design records in sources/ into docs/
make ci re-renders every reference document and diffs it byte-for-byte
against the committed HTML. A generated artefact that was hand-edited, or a
change that altered output nobody intended, fails the build.
The design notes in docs/ are generated the same way, from JSON
records in sources/ via
forktex-knowledge — the
library eats its own principle, so a hand-edited page fails the gate rather
than becoming a second author. Start with
everything-is-a-document for why a page is
never a source of truth, then
mandates-are-cited-data for why a legal
requirement travels with its citation.
Licence
Dual-licensed: AGPL-3.0-or-later, or a commercial licence from FORKTEX
S.R.L. for use in proprietary products, SaaS deployments, or any context where
the AGPL's obligations cannot be met. See LICENSE and NOTICE, or contact
info@forktex.com.
Metadata
Release files for forktex-documents 0.1.0
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 |
|---|---|---|---|---|
| forktex_documents-0.1.0.tar.gz | 83.0 kB | Without provenance | Details for forktex_documents-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| forktex_documents-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for forktex_documents-0.1.0-py3-none-any.whl |
Total release size: 189.7 kB
Release files / forktex_documents-0.1.0.tar.gz
| Download URL | forktex_documents-0.1.0.tar.gz |
|---|---|
| Size | 83.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
acd9daa597c7fd367280ee1930bc88734bad6e0aa2502e08b3762d42d74d649c
|
|
BLAKE2b-256 checksum How to use checksums |
669a2843724af0e94e719b284160327f91a180b25edfc9f3b54a04996f7946ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Release files / forktex_documents-0.1.0-py3-none-any.whl
| Download URL | forktex_documents-0.1.0-py3-none-any.whl |
|---|---|
| Size | 106.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9bafca8eb2739cf723e901d68cc4b871c48c454173a8af5f96d7de219a289593
|
|
BLAKE2b-256 checksum How to use checksums |
fddf8f2cb6295f2db5fc18872d0940ef427482dcf59faead4704932fb94769ee
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Security for forktex-documents 0.1.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A 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.