forktex-documents 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forktex_documents-0.1.0/LICENSE +45 -0
- forktex_documents-0.1.0/NOTICE +41 -0
- forktex_documents-0.1.0/PKG-INFO +179 -0
- forktex_documents-0.1.0/README.md +143 -0
- forktex_documents-0.1.0/pyproject.toml +155 -0
- forktex_documents-0.1.0/src/forktex_documents/__init__.py +291 -0
- forktex_documents-0.1.0/src/forktex_documents/assets/styles/paged.css +314 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/__init__.py +36 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/base.py +81 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/blocks.py +1253 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/canonical.py +139 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/catalog.py +280 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/document.py +118 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/entities.py +563 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/errors.py +163 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/fiscal.py +193 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/formatting.py +167 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/instance.py +159 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/localization.py +250 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/mandates.py +137 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/numbering.py +108 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/registry.py +268 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/series.py +119 -0
- forktex_documents-0.1.0/src/forktex_documents/domain/theme.py +317 -0
- forktex_documents-0.1.0/src/forktex_documents/ports.py +290 -0
- forktex_documents-0.1.0/src/forktex_documents/py.typed +0 -0
- forktex_documents-0.1.0/src/forktex_documents/render/__init__.py +36 -0
- forktex_documents-0.1.0/src/forktex_documents/render/assets.py +113 -0
- forktex_documents-0.1.0/src/forktex_documents/render/charts.py +318 -0
- forktex_documents-0.1.0/src/forktex_documents/render/projector.py +271 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
ForkTex Documents -- Dual License
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2026 FORKTEX S.R.L.
|
|
4
|
+
|
|
5
|
+
This software is licensed under a dual-license model:
|
|
6
|
+
|
|
7
|
+
============================================================================
|
|
8
|
+
|
|
9
|
+
1. OPEN-SOURCE LICENSE -- GNU Affero General Public License v3.0 (AGPL-3.0)
|
|
10
|
+
|
|
11
|
+
You may use, modify, and redistribute this software under the terms of the
|
|
12
|
+
GNU Affero General Public License v3.0 or later, as published by the Free
|
|
13
|
+
Software Foundation. The full license text is available at:
|
|
14
|
+
https://www.gnu.org/licenses/agpl-3.0.html
|
|
15
|
+
|
|
16
|
+
Key obligations under AGPL-3.0:
|
|
17
|
+
- Any modified version MUST be released under the same AGPL-3.0 license.
|
|
18
|
+
- If you run a modified version on a server and let users interact with it
|
|
19
|
+
over a network, you MUST make the complete source code available to those
|
|
20
|
+
users under AGPL-3.0.
|
|
21
|
+
- You MUST preserve all copyright notices and attribution to FORKTEX S.R.L.
|
|
22
|
+
|
|
23
|
+
============================================================================
|
|
24
|
+
|
|
25
|
+
2. COMMERCIAL LICENSE
|
|
26
|
+
|
|
27
|
+
If you wish to use this software without the obligations of AGPL-3.0 --
|
|
28
|
+
for example, to keep your modifications proprietary, to embed this software
|
|
29
|
+
in a closed-source product, or to operate it as part of a commercial service
|
|
30
|
+
without releasing your source code -- you MUST obtain a commercial license
|
|
31
|
+
from FORKTEX S.R.L.
|
|
32
|
+
|
|
33
|
+
Commercial licensing includes, but is not limited to:
|
|
34
|
+
- SaaS / hosted deployments generating revenue
|
|
35
|
+
- Embedding in proprietary products or platforms
|
|
36
|
+
- Redistribution in closed-source form
|
|
37
|
+
- Use by organizations exceeding the AGPL-3.0 compliance scope
|
|
38
|
+
|
|
39
|
+
For commercial licensing inquiries, contact:
|
|
40
|
+
info@forktex.com
|
|
41
|
+
|
|
42
|
+
============================================================================
|
|
43
|
+
|
|
44
|
+
Unless you have obtained a separate commercial license from FORKTEX S.R.L.,
|
|
45
|
+
your use of this software is governed exclusively by the AGPL-3.0 license.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
ForkTex Documents
|
|
2
|
+
Copyright (C) 2026 FORKTEX S.R.L.
|
|
3
|
+
|
|
4
|
+
This product includes software developed by FORKTEX S.R.L. (info@forktex.com).
|
|
5
|
+
|
|
6
|
+
`forktex-documents` generates the documents a company issues -- invoices,
|
|
7
|
+
contracts, stock-reception notes, declarations, reports -- from structured
|
|
8
|
+
data. A record is validated by a Pydantic canonical, composed into a typed
|
|
9
|
+
block tree, and projected to HTML, PDF or UBL XML. The rendered page is a view
|
|
10
|
+
over the record, never a source of truth. It ships no company's data: a
|
|
11
|
+
consumer supplies the parties, the lines and the numbering.
|
|
12
|
+
|
|
13
|
+
LICENSING
|
|
14
|
+
|
|
15
|
+
This software is dual-licensed:
|
|
16
|
+
|
|
17
|
+
1. GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later).
|
|
18
|
+
Use it under the AGPL and its obligations apply -- notably that running a
|
|
19
|
+
modified version over a network requires offering the corresponding source
|
|
20
|
+
to its users.
|
|
21
|
+
|
|
22
|
+
2. A proprietary commercial license from FORKTEX S.R.L.
|
|
23
|
+
|
|
24
|
+
If you cannot meet the AGPL's obligations -- use in a proprietary product, a
|
|
25
|
+
SaaS deployment, or any context where offering source is not possible -- you
|
|
26
|
+
MUST obtain a commercial license. Contact info@forktex.com.
|
|
27
|
+
|
|
28
|
+
See the LICENSE file for the full terms of both.
|
|
29
|
+
|
|
30
|
+
THIRD-PARTY NOTICES
|
|
31
|
+
|
|
32
|
+
This package depends on, but does not vendor:
|
|
33
|
+
|
|
34
|
+
* pydantic -- MIT
|
|
35
|
+
* jinja2 -- BSD-3-Clause (sandboxed text-fragment rendering only)
|
|
36
|
+
* forktex -- AGPL-3.0-or-later OR LicenseRef-ForkTex-Commercial
|
|
37
|
+
* weasyprint -- BSD-3-Clause (optional, for the `pdf` extra)
|
|
38
|
+
|
|
39
|
+
The document text carried in this repository's Romanian locale bundle is
|
|
40
|
+
transcribed from statutory and administrative forms; the legal wording is not
|
|
41
|
+
the copyrightable work of FORKTEX S.R.L.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forktex-documents
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A framework for generating documents from structured data: typed blocks that render themselves, canonicals that compute their own totals, and cited mandates. Document types belong to consumers.
|
|
5
|
+
License-Expression: AGPL-3.0-or-later OR LicenseRef-ForkTex-Commercial
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: NOTICE
|
|
8
|
+
Author: FORKTEX
|
|
9
|
+
Author-email: info@forktex.com
|
|
10
|
+
Requires-Python: >=3.14,<4.0
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: Intended Audience :: Legal Industry
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Office/Business
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
21
|
+
Classifier: Topic :: Printing
|
|
22
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Provides-Extra: pdf
|
|
25
|
+
Requires-Dist: forktex (>=0.10,<0.11)
|
|
26
|
+
Requires-Dist: jinja2 (>=3.1)
|
|
27
|
+
Requires-Dist: pydantic (>=2.12)
|
|
28
|
+
Requires-Dist: weasyprint (>=62) ; extra == "pdf"
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/forktex/forktex-documents/issues
|
|
30
|
+
Project-URL: Changelog, https://github.com/forktex/forktex-documents/blob/master/CHANGELOG.md
|
|
31
|
+
Project-URL: Documentation, https://github.com/forktex/forktex-documents/tree/master/docs
|
|
32
|
+
Project-URL: Homepage, https://forktex.com
|
|
33
|
+
Project-URL: Repository, https://github.com/forktex/forktex-documents
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# forktex-documents
|
|
37
|
+
|
|
38
|
+
A framework for generating documents from structured data. It ships the
|
|
39
|
+
vocabulary and the machinery — **not** document types: an invoice, a contract, a
|
|
40
|
+
delivery note are things a consumer defines, because what they must contain is
|
|
41
|
+
a fact about a business and a jurisdiction, not about a renderer.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
data (JSON) → canonical (Pydantic) → block tree → HTML → PDF
|
|
45
|
+
↘ read directly for a machine projection (XML, JSON…)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A PDF here is a **view over an object**, never a source of truth. The canonical
|
|
49
|
+
record is the document; the page is one projection of it. A machine projection
|
|
50
|
+
reads the canonical directly rather than the block tree — you cannot recover
|
|
51
|
+
"19% VAT on this line" from a table cell that says `19%`, which is why the two
|
|
52
|
+
representations both exist.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install forktex-documents # canonical, blocks, HTML
|
|
58
|
+
pip install "forktex-documents[pdf]" # + PDF, via WeasyPrint
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
PDF is an extra because WeasyPrint pulls native libraries (cairo, pango) that a
|
|
62
|
+
consumer producing only HTML or a machine projection should not have to install.
|
|
63
|
+
|
|
64
|
+
## What it does
|
|
65
|
+
|
|
66
|
+
A consumer declares its own canonical, and gets validation, composition and
|
|
67
|
+
rendering from the framework:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from forktex_documents import FamilyCanonical, Mandate, ParagraphBlock, known
|
|
71
|
+
|
|
72
|
+
@known
|
|
73
|
+
class DeliveryNote(FamilyCanonical):
|
|
74
|
+
family = "logistics"
|
|
75
|
+
mandates = (
|
|
76
|
+
Mandate(
|
|
77
|
+
path="carrier",
|
|
78
|
+
authority="acme-logistics",
|
|
79
|
+
reference="ops:carrier-is-named",
|
|
80
|
+
reason="A note with no carrier cannot be chased when the goods do not arrive.",
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
reference: str
|
|
84
|
+
carrier: str
|
|
85
|
+
|
|
86
|
+
def blocks(self, profile):
|
|
87
|
+
return (ParagraphBlock(text=f"Carried by {self.carrier}."),)
|
|
88
|
+
|
|
89
|
+
note = DeliveryNote(reference="DN-1", carrier="Speedy")
|
|
90
|
+
note.check() # raises if a mandate is unsatisfied
|
|
91
|
+
note.blocks(profile) # a typed block tree, ready to project
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See [`examples/showcase.py`](examples/showcase.py) for a document exercising
|
|
95
|
+
every block kind.
|
|
96
|
+
|
|
97
|
+
## The ideas worth knowing
|
|
98
|
+
|
|
99
|
+
**Derived values are never stored.** A consumer's totals, VAT subtotals and
|
|
100
|
+
expiry dates are `computed_field`s on its own canonical — and this package is
|
|
101
|
+
what makes that hold. Every model sets `extra="forbid"`, so a record carrying a
|
|
102
|
+
hand-written `total` is a validation error rather than a silent disagreement,
|
|
103
|
+
and a stored total that was tampered with is dropped and recomputed on load.
|
|
104
|
+
A structural guarantee rather than a review rule.
|
|
105
|
+
|
|
106
|
+
**Mandates are cited and conditional.** What a jurisdiction requires is data:
|
|
107
|
+
a path, the authority, the legal reference, and *why*. Conditions read computed
|
|
108
|
+
flags, so a retail stock-reception note demands a selling price while a
|
|
109
|
+
cost-method one does not, and a delivery showing quantity differences demands a
|
|
110
|
+
three-member committee and a third copy — because those are the rules the form
|
|
111
|
+
actually carries.
|
|
112
|
+
|
|
113
|
+
**Fiscal facts are time-versioned.** VAT rates resolve against the document's
|
|
114
|
+
own date, so re-rendering a July 2025 Romanian invoice reproduces 19% rather
|
|
115
|
+
than silently restating it at the current 21%. A date no schedule covers raises
|
|
116
|
+
instead of guessing.
|
|
117
|
+
|
|
118
|
+
**Blocks render themselves.** Twenty-five typed block kinds form a closed
|
|
119
|
+
discriminated union, each emitting its own HTML. There is no template language
|
|
120
|
+
deciding layout — the only templating is sandboxed *text* substitution for
|
|
121
|
+
locale fragments, where a clause has placeholders to resolve.
|
|
122
|
+
|
|
123
|
+
**Identifiers are validated, not merely shaped.** CUI and CNP check digits,
|
|
124
|
+
IBAN mod-97 across every country's length rule, per-member-state VAT formats.
|
|
125
|
+
A transposed digit in a national id files a real document against the wrong
|
|
126
|
+
person; the checksum is what catches it.
|
|
127
|
+
|
|
128
|
+
## What is *not* here
|
|
129
|
+
|
|
130
|
+
No invoice, no contract, no delivery note, and no jurisdiction. There is no
|
|
131
|
+
`RO_VAT_RATES`, no CUI or CNP check digit, and no default country, language or
|
|
132
|
+
currency anywhere — a default jurisdiction is a wrong jurisdiction for everyone
|
|
133
|
+
else, quietly. A consumer supplies its own:
|
|
134
|
+
|
|
135
|
+
- a `LegislationProfile` with its date format, separators, currency and mandates
|
|
136
|
+
- a `VatSchedule` with its rates and their validity windows
|
|
137
|
+
- national identifier validators, registered into `NATIONAL_TAX_ID_VALIDATORS`
|
|
138
|
+
- the numbers already spent, passed to a register as `BURNED`
|
|
139
|
+
|
|
140
|
+
## Layout
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
domain/ blocks, canonical, mandates, entities, fiscal, localization, theme,
|
|
144
|
+
registry, series ↑ no I/O — `grep Path domain/` is empty, by test
|
|
145
|
+
render/ the HTML projector, inline SVG charts, the asset port
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Imports run one way: `domain → render → facade`.
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
make install # poetry install --with dev
|
|
154
|
+
make ci # format, lint, licences, docs, types, audit, tests, build, smoke
|
|
155
|
+
make reference # rewrite committed reference renders — then read the diff
|
|
156
|
+
make docs # render the design records in sources/ into docs/
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`make ci` re-renders every reference document and diffs it byte-for-byte
|
|
160
|
+
against the committed HTML. A generated artefact that was hand-edited, or a
|
|
161
|
+
change that altered output nobody intended, fails the build.
|
|
162
|
+
|
|
163
|
+
The design notes in [`docs/`](docs/) are generated the same way, from JSON
|
|
164
|
+
records in [`sources/`](sources/) via
|
|
165
|
+
[`forktex-knowledge`](https://pypi.org/project/forktex-knowledge/) — the
|
|
166
|
+
library eats its own principle, so a hand-edited page fails the gate rather
|
|
167
|
+
than becoming a second author. Start with
|
|
168
|
+
[everything-is-a-document](docs/everything-is-a-document.md) for why a page is
|
|
169
|
+
never a source of truth, then
|
|
170
|
+
[mandates-are-cited-data](docs/mandates-are-cited-data.md) for why a legal
|
|
171
|
+
requirement travels with its citation.
|
|
172
|
+
|
|
173
|
+
## Licence
|
|
174
|
+
|
|
175
|
+
Dual-licensed: **AGPL-3.0-or-later**, or a commercial licence from FORKTEX
|
|
176
|
+
S.R.L. for use in proprietary products, SaaS deployments, or any context where
|
|
177
|
+
the AGPL's obligations cannot be met. See `LICENSE` and `NOTICE`, or contact
|
|
178
|
+
info@forktex.com.
|
|
179
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# forktex-documents
|
|
2
|
+
|
|
3
|
+
A framework for generating documents from structured data. It ships the
|
|
4
|
+
vocabulary and the machinery — **not** document types: an invoice, a contract, a
|
|
5
|
+
delivery note are things a consumer defines, because what they must contain is
|
|
6
|
+
a fact about a business and a jurisdiction, not about a renderer.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
data (JSON) → canonical (Pydantic) → block tree → HTML → PDF
|
|
10
|
+
↘ read directly for a machine projection (XML, JSON…)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
A PDF here is a **view over an object**, never a source of truth. The canonical
|
|
14
|
+
record is the document; the page is one projection of it. A machine projection
|
|
15
|
+
reads the canonical directly rather than the block tree — you cannot recover
|
|
16
|
+
"19% VAT on this line" from a table cell that says `19%`, which is why the two
|
|
17
|
+
representations both exist.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install forktex-documents # canonical, blocks, HTML
|
|
23
|
+
pip install "forktex-documents[pdf]" # + PDF, via WeasyPrint
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
PDF is an extra because WeasyPrint pulls native libraries (cairo, pango) that a
|
|
27
|
+
consumer producing only HTML or a machine projection should not have to install.
|
|
28
|
+
|
|
29
|
+
## What it does
|
|
30
|
+
|
|
31
|
+
A consumer declares its own canonical, and gets validation, composition and
|
|
32
|
+
rendering from the framework:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from forktex_documents import FamilyCanonical, Mandate, ParagraphBlock, known
|
|
36
|
+
|
|
37
|
+
@known
|
|
38
|
+
class DeliveryNote(FamilyCanonical):
|
|
39
|
+
family = "logistics"
|
|
40
|
+
mandates = (
|
|
41
|
+
Mandate(
|
|
42
|
+
path="carrier",
|
|
43
|
+
authority="acme-logistics",
|
|
44
|
+
reference="ops:carrier-is-named",
|
|
45
|
+
reason="A note with no carrier cannot be chased when the goods do not arrive.",
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
reference: str
|
|
49
|
+
carrier: str
|
|
50
|
+
|
|
51
|
+
def blocks(self, profile):
|
|
52
|
+
return (ParagraphBlock(text=f"Carried by {self.carrier}."),)
|
|
53
|
+
|
|
54
|
+
note = DeliveryNote(reference="DN-1", carrier="Speedy")
|
|
55
|
+
note.check() # raises if a mandate is unsatisfied
|
|
56
|
+
note.blocks(profile) # a typed block tree, ready to project
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
See [`examples/showcase.py`](examples/showcase.py) for a document exercising
|
|
60
|
+
every block kind.
|
|
61
|
+
|
|
62
|
+
## The ideas worth knowing
|
|
63
|
+
|
|
64
|
+
**Derived values are never stored.** A consumer's totals, VAT subtotals and
|
|
65
|
+
expiry dates are `computed_field`s on its own canonical — and this package is
|
|
66
|
+
what makes that hold. Every model sets `extra="forbid"`, so a record carrying a
|
|
67
|
+
hand-written `total` is a validation error rather than a silent disagreement,
|
|
68
|
+
and a stored total that was tampered with is dropped and recomputed on load.
|
|
69
|
+
A structural guarantee rather than a review rule.
|
|
70
|
+
|
|
71
|
+
**Mandates are cited and conditional.** What a jurisdiction requires is data:
|
|
72
|
+
a path, the authority, the legal reference, and *why*. Conditions read computed
|
|
73
|
+
flags, so a retail stock-reception note demands a selling price while a
|
|
74
|
+
cost-method one does not, and a delivery showing quantity differences demands a
|
|
75
|
+
three-member committee and a third copy — because those are the rules the form
|
|
76
|
+
actually carries.
|
|
77
|
+
|
|
78
|
+
**Fiscal facts are time-versioned.** VAT rates resolve against the document's
|
|
79
|
+
own date, so re-rendering a July 2025 Romanian invoice reproduces 19% rather
|
|
80
|
+
than silently restating it at the current 21%. A date no schedule covers raises
|
|
81
|
+
instead of guessing.
|
|
82
|
+
|
|
83
|
+
**Blocks render themselves.** Twenty-five typed block kinds form a closed
|
|
84
|
+
discriminated union, each emitting its own HTML. There is no template language
|
|
85
|
+
deciding layout — the only templating is sandboxed *text* substitution for
|
|
86
|
+
locale fragments, where a clause has placeholders to resolve.
|
|
87
|
+
|
|
88
|
+
**Identifiers are validated, not merely shaped.** CUI and CNP check digits,
|
|
89
|
+
IBAN mod-97 across every country's length rule, per-member-state VAT formats.
|
|
90
|
+
A transposed digit in a national id files a real document against the wrong
|
|
91
|
+
person; the checksum is what catches it.
|
|
92
|
+
|
|
93
|
+
## What is *not* here
|
|
94
|
+
|
|
95
|
+
No invoice, no contract, no delivery note, and no jurisdiction. There is no
|
|
96
|
+
`RO_VAT_RATES`, no CUI or CNP check digit, and no default country, language or
|
|
97
|
+
currency anywhere — a default jurisdiction is a wrong jurisdiction for everyone
|
|
98
|
+
else, quietly. A consumer supplies its own:
|
|
99
|
+
|
|
100
|
+
- a `LegislationProfile` with its date format, separators, currency and mandates
|
|
101
|
+
- a `VatSchedule` with its rates and their validity windows
|
|
102
|
+
- national identifier validators, registered into `NATIONAL_TAX_ID_VALIDATORS`
|
|
103
|
+
- the numbers already spent, passed to a register as `BURNED`
|
|
104
|
+
|
|
105
|
+
## Layout
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
domain/ blocks, canonical, mandates, entities, fiscal, localization, theme,
|
|
109
|
+
registry, series ↑ no I/O — `grep Path domain/` is empty, by test
|
|
110
|
+
render/ the HTML projector, inline SVG charts, the asset port
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Imports run one way: `domain → render → facade`.
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
make install # poetry install --with dev
|
|
119
|
+
make ci # format, lint, licences, docs, types, audit, tests, build, smoke
|
|
120
|
+
make reference # rewrite committed reference renders — then read the diff
|
|
121
|
+
make docs # render the design records in sources/ into docs/
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`make ci` re-renders every reference document and diffs it byte-for-byte
|
|
125
|
+
against the committed HTML. A generated artefact that was hand-edited, or a
|
|
126
|
+
change that altered output nobody intended, fails the build.
|
|
127
|
+
|
|
128
|
+
The design notes in [`docs/`](docs/) are generated the same way, from JSON
|
|
129
|
+
records in [`sources/`](sources/) via
|
|
130
|
+
[`forktex-knowledge`](https://pypi.org/project/forktex-knowledge/) — the
|
|
131
|
+
library eats its own principle, so a hand-edited page fails the gate rather
|
|
132
|
+
than becoming a second author. Start with
|
|
133
|
+
[everything-is-a-document](docs/everything-is-a-document.md) for why a page is
|
|
134
|
+
never a source of truth, then
|
|
135
|
+
[mandates-are-cited-data](docs/mandates-are-cited-data.md) for why a legal
|
|
136
|
+
requirement travels with its citation.
|
|
137
|
+
|
|
138
|
+
## Licence
|
|
139
|
+
|
|
140
|
+
Dual-licensed: **AGPL-3.0-or-later**, or a commercial licence from FORKTEX
|
|
141
|
+
S.R.L. for use in proprietary products, SaaS deployments, or any context where
|
|
142
|
+
the AGPL's obligations cannot be met. See `LICENSE` and `NOTICE`, or contact
|
|
143
|
+
info@forktex.com.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Copyright (C) 2026 FORKTEX S.R.L.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-ForkTex-Commercial
|
|
4
|
+
#
|
|
5
|
+
# This file is part of ForkTex Documents.
|
|
6
|
+
#
|
|
7
|
+
# For commercial licensing -- including use in proprietary products, SaaS
|
|
8
|
+
# deployments, or any context where AGPL obligations cannot be met -- you
|
|
9
|
+
# MUST obtain a commercial license from FORKTEX S.R.L. (info@forktex.com).
|
|
10
|
+
#
|
|
11
|
+
# This program is free software: you can redistribute it and/or modify
|
|
12
|
+
# it under the terms of the GNU Affero General Public License as published by
|
|
13
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
14
|
+
# (at your option) any later version.
|
|
15
|
+
#
|
|
16
|
+
# This program is distributed in the hope that it will be useful,
|
|
17
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
18
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
19
|
+
# GNU Affero General Public License for more details.
|
|
20
|
+
#
|
|
21
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
22
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
23
|
+
|
|
24
|
+
[project]
|
|
25
|
+
name = "forktex-documents"
|
|
26
|
+
version = "0.1.0"
|
|
27
|
+
description = "A framework for generating documents from structured data: typed blocks that render themselves, canonicals that compute their own totals, and cited mandates. Document types belong to consumers."
|
|
28
|
+
authors = [
|
|
29
|
+
{name = "FORKTEX",email = "info@forktex.com"}
|
|
30
|
+
]
|
|
31
|
+
readme = "README.md"
|
|
32
|
+
# Dual-licensed: AGPL-3.0-or-later for OSS use; a proprietary commercial
|
|
33
|
+
# license is also available (see LICENSE + NOTICE; info@forktex.com).
|
|
34
|
+
license = "AGPL-3.0-or-later OR LicenseRef-ForkTex-Commercial"
|
|
35
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
36
|
+
requires-python = ">=3.14,<4.0"
|
|
37
|
+
# 4 - Beta: the vocabulary is proven — it was extracted from a working consumer
|
|
38
|
+
# that renders thirteen real documents across four families. What has not been
|
|
39
|
+
# proven is a *second* consumer pushing on the seams, which is what would justify
|
|
40
|
+
# promoting this to 5.
|
|
41
|
+
classifiers = [
|
|
42
|
+
"Development Status :: 4 - Beta",
|
|
43
|
+
"Intended Audience :: Developers",
|
|
44
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
45
|
+
"Intended Audience :: Legal Industry",
|
|
46
|
+
"Operating System :: OS Independent",
|
|
47
|
+
"Programming Language :: Python :: 3",
|
|
48
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
49
|
+
"Programming Language :: Python :: 3.14",
|
|
50
|
+
"Topic :: Office/Business",
|
|
51
|
+
"Topic :: Office/Business :: Financial :: Accounting",
|
|
52
|
+
"Topic :: Printing",
|
|
53
|
+
"Topic :: Text Processing :: Markup :: HTML",
|
|
54
|
+
"Typing :: Typed",
|
|
55
|
+
]
|
|
56
|
+
dependencies = [
|
|
57
|
+
# The shared substrate. Consumed, not re-implemented: `forktex.error` supplies
|
|
58
|
+
# the one exception hierarchy this package's errors are leaves of, and
|
|
59
|
+
# `forktex.types` supplies the frozen value-object base every model derives
|
|
60
|
+
# from. Pinned to a minor range because both are surfaces we track closely.
|
|
61
|
+
"forktex>=0.10,<0.11",
|
|
62
|
+
# Every model, every validator, every computed total. The design rule that
|
|
63
|
+
# derived values are never stored is enforced by `computed_field` plus
|
|
64
|
+
# `extra="forbid"`, so this is load-bearing rather than convenience.
|
|
65
|
+
"pydantic>=2.12",
|
|
66
|
+
# Sandboxed rendering of *text fragments* only — a locale bundle's clause and
|
|
67
|
+
# declaration text, where a placeholder must resolve against document data.
|
|
68
|
+
# No template decides layout: blocks render themselves in Python. That
|
|
69
|
+
# distinction is why the dependency is small and stays that way.
|
|
70
|
+
"jinja2>=3.1",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[project.optional-dependencies]
|
|
74
|
+
# PDF is optional because the canonical, the block tree and the HTML projection
|
|
75
|
+
# are useful without it, and WeasyPrint pulls native libraries (cairo, pango)
|
|
76
|
+
# that a consumer producing only HTML or UBL should not be made to install.
|
|
77
|
+
pdf = ["weasyprint>=62"]
|
|
78
|
+
|
|
79
|
+
# No [project.scripts]: this is a library. A consumer wires its own entry point,
|
|
80
|
+
# because the asset roots, the theme and the number register are its own.
|
|
81
|
+
|
|
82
|
+
[project.urls]
|
|
83
|
+
"Homepage" = "https://forktex.com"
|
|
84
|
+
"Repository" = "https://github.com/forktex/forktex-documents"
|
|
85
|
+
"Documentation" = "https://github.com/forktex/forktex-documents/tree/master/docs"
|
|
86
|
+
"Changelog" = "https://github.com/forktex/forktex-documents/blob/master/CHANGELOG.md"
|
|
87
|
+
"Bug Tracker" = "https://github.com/forktex/forktex-documents/issues"
|
|
88
|
+
|
|
89
|
+
[build-system]
|
|
90
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
91
|
+
build-backend = "poetry.core.masonry.api"
|
|
92
|
+
|
|
93
|
+
[tool.poetry]
|
|
94
|
+
# The distribution is `forktex-documents` and the import is `forktex_documents`
|
|
95
|
+
# — PEP 503 wants the hyphen on an index, Python wants the underscore in code.
|
|
96
|
+
packages = [{ include = "forktex_documents", from = "src" }]
|
|
97
|
+
# The stylesheet is not a sample: `HtmlProjector` reads `styles/paged.css`
|
|
98
|
+
# through the asset port on every render, so a wheel without it produces
|
|
99
|
+
# unstyled output. Shipped explicitly because it is not a `.py` file and would
|
|
100
|
+
# otherwise be dropped from both the sdist and the wheel.
|
|
101
|
+
include = [
|
|
102
|
+
{ path = "src/forktex_documents/py.typed", format = ["sdist", "wheel"] },
|
|
103
|
+
{ path = "src/forktex_documents/assets/**/*", format = ["sdist", "wheel"] },
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
[dependency-groups]
|
|
107
|
+
dev = [
|
|
108
|
+
"pytest>=8.0",
|
|
109
|
+
"pytest-cov>=6.0",
|
|
110
|
+
"ruff>=0.8.0",
|
|
111
|
+
"pyright>=1.1.350",
|
|
112
|
+
"pip-audit>=2.7.0",
|
|
113
|
+
"build>=1.2.0",
|
|
114
|
+
"twine>=5.0.0",
|
|
115
|
+
# The drift gate renders the reference document, and `make render` takes it
|
|
116
|
+
# to PDF. Without it the example cannot run.
|
|
117
|
+
"weasyprint>=62",
|
|
118
|
+
# Renders the design records in `sources/` into `docs/`.
|
|
119
|
+
"forktex-knowledge (>=0.1.0,<0.2.0)",
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
[tool.ruff]
|
|
123
|
+
target-version = "py314"
|
|
124
|
+
line-length = 110
|
|
125
|
+
|
|
126
|
+
[tool.ruff.lint]
|
|
127
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF", "ANN"]
|
|
128
|
+
# RUF001/2/3 flag characters that could be homoglyph attacks in *code*. This library's
|
|
129
|
+
# strings are document text in Romanian, where they are simply correct typography: an
|
|
130
|
+
# en dash in a range ("3–4 săptămâni"), a multiplication sign in a specification
|
|
131
|
+
# ("10 × 450 W"), and — load-bearing — a NO-BREAK SPACE as the thousands separator, which
|
|
132
|
+
# is what stops "201 020,27" being broken across a line in a printed invoice. Replacing
|
|
133
|
+
# them with ASCII would change what the documents say, so the rules are off rather than
|
|
134
|
+
# suppressed line by line.
|
|
135
|
+
ignore = ["RUF001", "RUF002", "RUF003"]
|
|
136
|
+
|
|
137
|
+
[tool.ruff.lint.per-file-ignores]
|
|
138
|
+
# `src/` carries NO exemptions — every module passes the full strict set. A new
|
|
139
|
+
# `Any` in a signature is a prompt to name the type, not to add a line here.
|
|
140
|
+
# Tests and the demo builders are held to the baseline only.
|
|
141
|
+
"tests/**" = ["E1", "E2", "E5", "I", "UP", "B", "SIM", "RUF", "ANN"]
|
|
142
|
+
"examples/**" = ["E1", "E2", "E5", "I", "UP", "B", "SIM", "RUF", "ANN"]
|
|
143
|
+
|
|
144
|
+
[tool.pyright]
|
|
145
|
+
# src/ only: `python:ci-is-the-gate` type-checks the library, not the demos.
|
|
146
|
+
include = ["src"]
|
|
147
|
+
exclude = ["**/__pycache__"]
|
|
148
|
+
pythonVersion = "3.14"
|
|
149
|
+
typeCheckingMode = "standard"
|
|
150
|
+
|
|
151
|
+
[tool.pytest.ini_options]
|
|
152
|
+
pythonpath = ["src"]
|
|
153
|
+
testpaths = ["tests"]
|
|
154
|
+
log_cli = false
|
|
155
|
+
log_level = "WARNING"
|