trustrender 0.3.3__tar.gz → 0.3.5__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.
Files changed (86) hide show
  1. {trustrender-0.3.3/src/trustrender.egg-info → trustrender-0.3.5}/PKG-INFO +82 -35
  2. {trustrender-0.3.3 → trustrender-0.3.5}/README.md +81 -34
  3. {trustrender-0.3.3 → trustrender-0.3.5}/pyproject.toml +8 -2
  4. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/__init__.py +50 -42
  5. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/cli.py +52 -39
  6. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/contract.py +4 -9
  7. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/dashboard.py +1 -4
  8. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/doctor.py +5 -16
  9. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/engine.py +104 -14
  10. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fingerprint.py +145 -104
  11. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/invoice_aliases.py +59 -38
  12. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/invoice_ingest.py +351 -265
  13. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/invoice_schema.py +7 -6
  14. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/provenance.py +8 -2
  15. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/readiness.py +153 -119
  16. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/regression.py +112 -98
  17. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/semantic.py +172 -129
  18. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/server.py +102 -26
  19. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/trace.py +24 -27
  20. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/zugferd.py +355 -175
  21. {trustrender-0.3.3 → trustrender-0.3.5/src/trustrender.egg-info}/PKG-INFO +82 -35
  22. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_adapters.py +99 -76
  23. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_adversarial_corpus.py +48 -30
  24. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_audit_e2e.py +63 -36
  25. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_contract.py +32 -34
  26. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_doctor.py +21 -22
  27. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_engine.py +154 -0
  28. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_fingerprint.py +3 -7
  29. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_fingerprint_stress.py +27 -19
  30. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_fonts.py +6 -18
  31. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_invoice_ingest.py +106 -68
  32. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_pagination.py +0 -1
  33. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_provenance.py +9 -6
  34. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_readiness.py +8 -12
  35. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_real_payloads.py +6 -4
  36. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_regression.py +26 -37
  37. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_regression_stress.py +51 -34
  38. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_semantic.py +20 -23
  39. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_semantic_stress.py +8 -2
  40. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_server.py +7 -1
  41. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_templates.py +5 -12
  42. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_trace.py +2 -4
  43. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_ugly_data.py +5 -18
  44. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_ugly_data_pressure.py +31 -28
  45. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_zugferd.py +173 -25
  46. {trustrender-0.3.3 → trustrender-0.3.5}/LICENSE +0 -0
  47. {trustrender-0.3.3 → trustrender-0.3.5}/setup.cfg +0 -0
  48. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/adapters/__init__.py +1 -1
  49. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/adapters/shopify.py +0 -0
  50. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/adapters/stripe.py +0 -0
  51. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/einvoice.j2.typ +0 -0
  52. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/einvoice_data.json +0 -0
  53. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/invoice.j2.typ +0 -0
  54. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/invoice_data.json +0 -0
  55. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/letter.j2.typ +0 -0
  56. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/letter_data.json +0 -0
  57. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/receipt.j2.typ +0 -0
  58. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/receipt_data.json +0 -0
  59. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/report.j2.typ +0 -0
  60. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/report_data.json +0 -0
  61. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/statement.j2.typ +0 -0
  62. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/statement_data.json +0 -0
  63. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/errors.py +0 -0
  64. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/filters.py +0 -0
  65. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fonts/Inter/Inter-Bold.ttf +0 -0
  66. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fonts/Inter/Inter-BoldItalic.ttf +0 -0
  67. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fonts/Inter/Inter-Italic.ttf +0 -0
  68. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fonts/Inter/Inter-Regular.ttf +0 -0
  69. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fonts/Inter/LICENSE.txt +0 -0
  70. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/assets/index-Bsf6YnD1.css +0 -0
  71. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/assets/index-C6K0JsLw.js +0 -0
  72. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/assets/pdf.worker-C1breYqn.mjs +0 -0
  73. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/favicon.svg +0 -0
  74. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/icons.svg +0 -0
  75. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/index.html +0 -0
  76. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/templates.py +0 -0
  77. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender.egg-info/SOURCES.txt +0 -0
  78. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender.egg-info/dependency_links.txt +0 -0
  79. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender.egg-info/entry_points.txt +0 -0
  80. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender.egg-info/requires.txt +0 -0
  81. {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender.egg-info/top_level.txt +0 -0
  82. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_cli.py +0 -0
  83. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_error_pipeline.py +0 -0
  84. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_errors.py +0 -0
  85. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_filters.py +0 -0
  86. {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_render.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trustrender
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: Validate and normalize Stripe, Shopify, and custom billing data for Factur-X/ZUGFeRD
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://trustrender.dev
@@ -42,6 +42,11 @@ Dynamic: license-file
42
42
 
43
43
  # TrustRender
44
44
 
45
+ [![PyPI](https://img.shields.io/pypi/v/trustrender.svg)](https://pypi.org/project/trustrender/)
46
+ [![Python](https://img.shields.io/pypi/pyversions/trustrender.svg)](https://pypi.org/project/trustrender/)
47
+ [![Tests](https://img.shields.io/github/actions/workflow/status/verityengine/trustrender/ci.yml?branch=main&label=tests)](https://github.com/verityengine/trustrender/actions)
48
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
49
+
45
50
  Validate and normalize billing data before Factur-X/ZUGFeRD embedding.
46
51
 
47
52
  ## Quick start
@@ -59,7 +64,7 @@ Invoice: INV-2026-0187
59
64
  From:
60
65
  To: Rheingold Maschinenbau GmbH
61
66
  Items: 3
62
- Total: $2,685.37
67
+ Total: $2,945.25
63
68
 
64
69
  BLOCKED — 1 problem(s)
65
70
 
@@ -95,7 +100,7 @@ Invoice: INV-2026-0187
95
100
  From: NovaTech Solutions GmbH
96
101
  To: Rheingold Maschinenbau GmbH
97
102
  Items: 3
98
- Total: $2,685.37
103
+ Total: $2,945.25
99
104
 
100
105
  PASS — invoice data is valid
101
106
 
@@ -149,12 +154,14 @@ Requires Python 3.11+.
149
154
  from trustrender import validate_invoice
150
155
  from trustrender.adapters import from_stripe
151
156
 
152
- # Raw Stripe API response → validated canonical invoice
153
- result = validate_invoice(from_stripe(raw_stripe_response), zugferd=True)
157
+ # Raw Stripe API response → canonical invoice (structural validation)
158
+ result = validate_invoice(from_stripe(raw_stripe_response))
154
159
 
155
- if result["render_ready"] and result.get("zugferd_ready"):
160
+ if result["render_ready"]:
156
161
  canonical = result["canonical"]
157
- # safe to hand off to factur-x / drafthorse
162
+ # canonical is now ready for either:
163
+ # - your own template renderer (most users)
164
+ # - the to_zugferd_data() bridge → drafthorse / factur-x (see end-to-end example below)
158
165
  else:
159
166
  for error in result["errors"]:
160
167
  print(f"BLOCKED: {error['message']}")
@@ -167,7 +174,8 @@ else:
167
174
  - `errors`: list of blocking issues with rule_id, path, expected/actual
168
175
  - `warnings`: advisory issues
169
176
  - `normalizations`: field-level provenance (what was renamed, coerced, computed)
170
- - `zugferd_ready`: bool (if `zugferd=True`)
177
+
178
+ The `zugferd=True` flag is also accepted but only useful if your input is already in ZUGFeRD shape (with `seller`/`buyer`/`tax_entries`/`payment` keys). For Stripe and Shopify, use the `to_zugferd_data()` bridge after `validate_invoice()` instead — see the end-to-end example below.
171
179
 
172
180
  Also works with messy data from any source:
173
181
 
@@ -213,6 +221,51 @@ result = validate_invoice(from_shopify(raw_shopify_order))
213
221
 
214
222
  Parses string amounts to floats, combines first_name + last_name, maps order fields to invoice structure, preserves structured address fields. Shopify orders have no seller info or due date — TrustRender flags the missing seller and handles the absent due date correctly.
215
223
 
224
+ ## End-to-end example: Stripe → real Factur-X PDF
225
+
226
+ [`examples/with_drafthorse_facturx.py`](examples/with_drafthorse_facturx.py) runs the full pipeline:
227
+
228
+ ```python
229
+ from trustrender import validate_invoice
230
+ from trustrender.adapters import from_stripe
231
+ from trustrender.zugferd import to_zugferd_data, build_invoice_xml, apply_zugferd
232
+
233
+ # 1. Adapt + validate the Stripe payload (canonical structure check)
234
+ result = validate_invoice(from_stripe(stripe_invoice))
235
+
236
+ # 2. Bridge canonical → ZUGFeRD shape with the three things Stripe never includes:
237
+ # seller VAT/address, payment IBAN, applicable tax rate
238
+ zugferd_data = to_zugferd_data(
239
+ result["canonical"],
240
+ seller={"name": "...", "address": "...", "city": "...",
241
+ "postal_code": "...", "country": "DE", "vat_id": "DE..."},
242
+ payment={"means": "credit_transfer", "iban": "DE..."},
243
+ tax_rate=19,
244
+ )
245
+ # Real EN 16931 contract validation runs here — catches anything XML build would reject.
246
+
247
+ # 3. drafthorse builds UN/CEFACT CII XML
248
+ xml_bytes = build_invoice_xml(zugferd_data)
249
+
250
+ # 4. factur-x embeds the XML into a PDF/A-3b container
251
+ factur_x_pdf = apply_zugferd(your_visual_pdf_bytes, xml_bytes, lang="de")
252
+ ```
253
+
254
+ Output: a 15 KB Factur-X invoice PDF that validates against the EN 16931 XSD ([sample committed at `examples/invoice_facturx.pdf`](examples/invoice_facturx.pdf)).
255
+
256
+ ```
257
+ $ python examples/with_drafthorse_facturx.py
258
+ Step 1: Adapt + validate via TrustRender (canonical structure) → status=ready
259
+ Step 2: Bridge canonical → ZUGFeRD shape (one call) → passes EN 16931 contract validation
260
+ Step 3: Build CII XML via drafthorse → 8,312 bytes of CII XML
261
+ Step 4: Render visual PDF (your template) → 1,599 bytes
262
+ Step 5: Embed CII XML as PDF/A-3b → 15,004 bytes
263
+ Step 6: Verify with factur-x library → ✓ passes EN 16931 XSD
264
+ ✓ factur-x.xml is embedded in the PDF
265
+ ```
266
+
267
+ What's TrustRender vs what's you: the seller VAT, payment IBAN, and tax rate come from your billing setup — Stripe's API doesn't include them, and TrustRender doesn't invent them. Everything else is automatic.
268
+
216
269
  ## What it normalizes
217
270
 
218
271
  90+ vendor field aliases across QuickBooks, Xero, Stripe, and generic CSV/ERP formats:
@@ -257,53 +310,47 @@ trustrender serve --templates <dir> [--port 8190]
257
310
  trustrender doctor [--smoke]
258
311
  ```
259
312
 
260
- ## Integration with factur-x / drafthorse
261
-
262
- TrustRender validates and normalizes. You generate and embed with the library of your choice.
263
-
264
- ```python
265
- from trustrender import validate_invoice
266
-
267
- # Step 1: Validate with TrustRender
268
- result = validate_invoice(messy_data, zugferd=True)
269
- if not result["render_ready"] or not result["zugferd_ready"]:
270
- raise ValueError(f"Invoice blocked: {result['errors']}")
271
-
272
- # Step 2: Use the canonical payload with drafthorse or factur-x
273
- canonical = result["canonical"]
274
- # ... your existing ZUGFeRD generation code here
275
- ```
276
-
277
313
  ## EN 16931 e-invoicing (narrow scope)
278
314
 
279
- Catches many document-level and ZUGFeRD/EN 16931 readiness issues before embedding. Currently supports:
315
+ Catches structural and contract-level EN 16931 issues before drafthorse builds the XML. Currently supports:
280
316
 
281
317
  - **Domestic German B2B invoices** with standard VAT, EUR, SEPA payment
282
- - Single or mixed VAT rates (7% + 19%)
318
+ - Single VAT rate per invoice (mixed rates not yet supported in the bridge)
283
319
  - Invoice type 380 and credit note 381
284
- - PDF/A-3b with embedded CII XML (requires `trustrender[render]`)
320
+ - PDF/A-3b with embedded CII XML via `apply_zugferd()` (wraps `factur-x.attach_xml`)
321
+
322
+ Not supported, fails loudly: reverse charge, cross-border, allowances/charges, non-EUR currencies, non-DE seller country.
285
323
 
286
- Not supported (fails loudly): reverse charge, cross-border, allowances/charges, non-EUR currencies.
324
+ What still slips past TrustRender: things only the official Schematron rules catch (cross-field business rules, value range checks). Run [`xml_check_xsd`](https://github.com/akretion/factur-x) and `xml_check_schematron` on the produced XML for full compliance certainty.
287
325
 
288
326
  See [docs/einvoice-scope.md](docs/einvoice-scope.md) for the full scope matrix.
289
327
 
290
- ## Optional: PDF rendering
328
+ ## Also: deterministic PDF rendering
329
+
330
+ TrustRender ships a Jinja2 + Typst render engine alongside the validation layer. Used internally to produce the visual PDF for Factur-X embedding, and usable standalone if you want one Python package that goes from data → validated canonical → final PDF without bringing in a headless browser.
291
331
 
292
- If you also want TrustRender to generate PDFs (not just validate):
332
+ - 1,000-line invoice rendered in ~59 ms median (~72 ms with content varying every render; Apple M5 Pro, single core, v0.3.5, 2026-08-29)
333
+ - 1,011-page invoice rendered in 37 seconds
334
+ - No headless browser, no Chromium binary, no Java runtime — pure Python + the Typst CLI
335
+ - SHA-256 fingerprinting of input, template, and output for audit trails (see `trustrender audit`)
336
+ - Drift detection vs saved baselines for visual regression in CI
337
+ - Subprocess-based backend with real timeouts (killable renders, not just abandoned)
338
+ - Bundled fonts, six built-in templates (invoice, einvoice, statement, letter, receipt, report)
293
339
 
294
340
  ```
295
341
  pip install "trustrender[render]"
296
342
  trustrender render invoice.j2.typ data.json -o invoice.pdf --zugferd en16931
297
343
  ```
298
344
 
299
- Rendering uses Typst — no browser, no Chromium. Fast and deterministic.
345
+ `render`, `preflight`, `audit`, `baseline`, and `serve` are all part of the rendering surface — see `trustrender --help` for the full set.
300
346
 
301
347
  ## What this is not
302
348
 
303
349
  - Not a full AP automation platform
304
- - Not an e-invoice compliance certification
305
- - Not an AI-powered data fixer (all corrections are deterministic)
306
- - Not a replacement for factur-x or drafthorse — it's the validation layer you run before them
350
+ - Not an e-invoice compliance certification (run Schematron on the output XML for that)
351
+ - Not an AI-powered data fixer — all normalizations and validations are deterministic
352
+ - Not a replacement for factur-x or drafthorse — it validates billing data before them and wraps their entry points for convenience
353
+ - Not a billing platform replacement — it expects you already collect payment via Stripe, Shopify, or something else
307
354
 
308
355
  ## Development
309
356
 
@@ -1,5 +1,10 @@
1
1
  # TrustRender
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/trustrender.svg)](https://pypi.org/project/trustrender/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/trustrender.svg)](https://pypi.org/project/trustrender/)
5
+ [![Tests](https://img.shields.io/github/actions/workflow/status/verityengine/trustrender/ci.yml?branch=main&label=tests)](https://github.com/verityengine/trustrender/actions)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
3
8
  Validate and normalize billing data before Factur-X/ZUGFeRD embedding.
4
9
 
5
10
  ## Quick start
@@ -17,7 +22,7 @@ Invoice: INV-2026-0187
17
22
  From:
18
23
  To: Rheingold Maschinenbau GmbH
19
24
  Items: 3
20
- Total: $2,685.37
25
+ Total: $2,945.25
21
26
 
22
27
  BLOCKED — 1 problem(s)
23
28
 
@@ -53,7 +58,7 @@ Invoice: INV-2026-0187
53
58
  From: NovaTech Solutions GmbH
54
59
  To: Rheingold Maschinenbau GmbH
55
60
  Items: 3
56
- Total: $2,685.37
61
+ Total: $2,945.25
57
62
 
58
63
  PASS — invoice data is valid
59
64
 
@@ -107,12 +112,14 @@ Requires Python 3.11+.
107
112
  from trustrender import validate_invoice
108
113
  from trustrender.adapters import from_stripe
109
114
 
110
- # Raw Stripe API response → validated canonical invoice
111
- result = validate_invoice(from_stripe(raw_stripe_response), zugferd=True)
115
+ # Raw Stripe API response → canonical invoice (structural validation)
116
+ result = validate_invoice(from_stripe(raw_stripe_response))
112
117
 
113
- if result["render_ready"] and result.get("zugferd_ready"):
118
+ if result["render_ready"]:
114
119
  canonical = result["canonical"]
115
- # safe to hand off to factur-x / drafthorse
120
+ # canonical is now ready for either:
121
+ # - your own template renderer (most users)
122
+ # - the to_zugferd_data() bridge → drafthorse / factur-x (see end-to-end example below)
116
123
  else:
117
124
  for error in result["errors"]:
118
125
  print(f"BLOCKED: {error['message']}")
@@ -125,7 +132,8 @@ else:
125
132
  - `errors`: list of blocking issues with rule_id, path, expected/actual
126
133
  - `warnings`: advisory issues
127
134
  - `normalizations`: field-level provenance (what was renamed, coerced, computed)
128
- - `zugferd_ready`: bool (if `zugferd=True`)
135
+
136
+ The `zugferd=True` flag is also accepted but only useful if your input is already in ZUGFeRD shape (with `seller`/`buyer`/`tax_entries`/`payment` keys). For Stripe and Shopify, use the `to_zugferd_data()` bridge after `validate_invoice()` instead — see the end-to-end example below.
129
137
 
130
138
  Also works with messy data from any source:
131
139
 
@@ -171,6 +179,51 @@ result = validate_invoice(from_shopify(raw_shopify_order))
171
179
 
172
180
  Parses string amounts to floats, combines first_name + last_name, maps order fields to invoice structure, preserves structured address fields. Shopify orders have no seller info or due date — TrustRender flags the missing seller and handles the absent due date correctly.
173
181
 
182
+ ## End-to-end example: Stripe → real Factur-X PDF
183
+
184
+ [`examples/with_drafthorse_facturx.py`](examples/with_drafthorse_facturx.py) runs the full pipeline:
185
+
186
+ ```python
187
+ from trustrender import validate_invoice
188
+ from trustrender.adapters import from_stripe
189
+ from trustrender.zugferd import to_zugferd_data, build_invoice_xml, apply_zugferd
190
+
191
+ # 1. Adapt + validate the Stripe payload (canonical structure check)
192
+ result = validate_invoice(from_stripe(stripe_invoice))
193
+
194
+ # 2. Bridge canonical → ZUGFeRD shape with the three things Stripe never includes:
195
+ # seller VAT/address, payment IBAN, applicable tax rate
196
+ zugferd_data = to_zugferd_data(
197
+ result["canonical"],
198
+ seller={"name": "...", "address": "...", "city": "...",
199
+ "postal_code": "...", "country": "DE", "vat_id": "DE..."},
200
+ payment={"means": "credit_transfer", "iban": "DE..."},
201
+ tax_rate=19,
202
+ )
203
+ # Real EN 16931 contract validation runs here — catches anything XML build would reject.
204
+
205
+ # 3. drafthorse builds UN/CEFACT CII XML
206
+ xml_bytes = build_invoice_xml(zugferd_data)
207
+
208
+ # 4. factur-x embeds the XML into a PDF/A-3b container
209
+ factur_x_pdf = apply_zugferd(your_visual_pdf_bytes, xml_bytes, lang="de")
210
+ ```
211
+
212
+ Output: a 15 KB Factur-X invoice PDF that validates against the EN 16931 XSD ([sample committed at `examples/invoice_facturx.pdf`](examples/invoice_facturx.pdf)).
213
+
214
+ ```
215
+ $ python examples/with_drafthorse_facturx.py
216
+ Step 1: Adapt + validate via TrustRender (canonical structure) → status=ready
217
+ Step 2: Bridge canonical → ZUGFeRD shape (one call) → passes EN 16931 contract validation
218
+ Step 3: Build CII XML via drafthorse → 8,312 bytes of CII XML
219
+ Step 4: Render visual PDF (your template) → 1,599 bytes
220
+ Step 5: Embed CII XML as PDF/A-3b → 15,004 bytes
221
+ Step 6: Verify with factur-x library → ✓ passes EN 16931 XSD
222
+ ✓ factur-x.xml is embedded in the PDF
223
+ ```
224
+
225
+ What's TrustRender vs what's you: the seller VAT, payment IBAN, and tax rate come from your billing setup — Stripe's API doesn't include them, and TrustRender doesn't invent them. Everything else is automatic.
226
+
174
227
  ## What it normalizes
175
228
 
176
229
  90+ vendor field aliases across QuickBooks, Xero, Stripe, and generic CSV/ERP formats:
@@ -215,53 +268,47 @@ trustrender serve --templates <dir> [--port 8190]
215
268
  trustrender doctor [--smoke]
216
269
  ```
217
270
 
218
- ## Integration with factur-x / drafthorse
219
-
220
- TrustRender validates and normalizes. You generate and embed with the library of your choice.
221
-
222
- ```python
223
- from trustrender import validate_invoice
224
-
225
- # Step 1: Validate with TrustRender
226
- result = validate_invoice(messy_data, zugferd=True)
227
- if not result["render_ready"] or not result["zugferd_ready"]:
228
- raise ValueError(f"Invoice blocked: {result['errors']}")
229
-
230
- # Step 2: Use the canonical payload with drafthorse or factur-x
231
- canonical = result["canonical"]
232
- # ... your existing ZUGFeRD generation code here
233
- ```
234
-
235
271
  ## EN 16931 e-invoicing (narrow scope)
236
272
 
237
- Catches many document-level and ZUGFeRD/EN 16931 readiness issues before embedding. Currently supports:
273
+ Catches structural and contract-level EN 16931 issues before drafthorse builds the XML. Currently supports:
238
274
 
239
275
  - **Domestic German B2B invoices** with standard VAT, EUR, SEPA payment
240
- - Single or mixed VAT rates (7% + 19%)
276
+ - Single VAT rate per invoice (mixed rates not yet supported in the bridge)
241
277
  - Invoice type 380 and credit note 381
242
- - PDF/A-3b with embedded CII XML (requires `trustrender[render]`)
278
+ - PDF/A-3b with embedded CII XML via `apply_zugferd()` (wraps `factur-x.attach_xml`)
279
+
280
+ Not supported, fails loudly: reverse charge, cross-border, allowances/charges, non-EUR currencies, non-DE seller country.
243
281
 
244
- Not supported (fails loudly): reverse charge, cross-border, allowances/charges, non-EUR currencies.
282
+ What still slips past TrustRender: things only the official Schematron rules catch (cross-field business rules, value range checks). Run [`xml_check_xsd`](https://github.com/akretion/factur-x) and `xml_check_schematron` on the produced XML for full compliance certainty.
245
283
 
246
284
  See [docs/einvoice-scope.md](docs/einvoice-scope.md) for the full scope matrix.
247
285
 
248
- ## Optional: PDF rendering
286
+ ## Also: deterministic PDF rendering
287
+
288
+ TrustRender ships a Jinja2 + Typst render engine alongside the validation layer. Used internally to produce the visual PDF for Factur-X embedding, and usable standalone if you want one Python package that goes from data → validated canonical → final PDF without bringing in a headless browser.
249
289
 
250
- If you also want TrustRender to generate PDFs (not just validate):
290
+ - 1,000-line invoice rendered in ~59 ms median (~72 ms with content varying every render; Apple M5 Pro, single core, v0.3.5, 2026-08-29)
291
+ - 1,011-page invoice rendered in 37 seconds
292
+ - No headless browser, no Chromium binary, no Java runtime — pure Python + the Typst CLI
293
+ - SHA-256 fingerprinting of input, template, and output for audit trails (see `trustrender audit`)
294
+ - Drift detection vs saved baselines for visual regression in CI
295
+ - Subprocess-based backend with real timeouts (killable renders, not just abandoned)
296
+ - Bundled fonts, six built-in templates (invoice, einvoice, statement, letter, receipt, report)
251
297
 
252
298
  ```
253
299
  pip install "trustrender[render]"
254
300
  trustrender render invoice.j2.typ data.json -o invoice.pdf --zugferd en16931
255
301
  ```
256
302
 
257
- Rendering uses Typst — no browser, no Chromium. Fast and deterministic.
303
+ `render`, `preflight`, `audit`, `baseline`, and `serve` are all part of the rendering surface — see `trustrender --help` for the full set.
258
304
 
259
305
  ## What this is not
260
306
 
261
307
  - Not a full AP automation platform
262
- - Not an e-invoice compliance certification
263
- - Not an AI-powered data fixer (all corrections are deterministic)
264
- - Not a replacement for factur-x or drafthorse — it's the validation layer you run before them
308
+ - Not an e-invoice compliance certification (run Schematron on the output XML for that)
309
+ - Not an AI-powered data fixer — all normalizations and validations are deterministic
310
+ - Not a replacement for factur-x or drafthorse — it validates billing data before them and wraps their entry points for convenience
311
+ - Not a billing platform replacement — it expects you already collect payment via Stripe, Shopify, or something else
265
312
 
266
313
  ## Development
267
314
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trustrender"
7
- version = "0.3.3"
7
+ version = "0.3.5"
8
8
  description = "Validate and normalize Stripe, Shopify, and custom billing data for Factur-X/ZUGFeRD"
9
9
  license = "MIT"
10
10
  classifiers = [
@@ -66,8 +66,14 @@ where = ["src"]
66
66
  trustrender = ["playground/**/*", "fonts/**/*", "builtin_templates/*"]
67
67
 
68
68
  [tool.ruff]
69
- line-length = 99
69
+ line-length = 140
70
70
  target-version = "py311"
71
71
 
72
72
  [tool.ruff.lint]
73
73
  select = ["E", "F", "I", "W"]
74
+
75
+ [tool.ruff.lint.per-file-ignores]
76
+ # Dashboard generates HTML/CSS as inline string literals — long lines are intentional
77
+ "src/trustrender/dashboard.py" = ["E501"]
78
+ # Tests: long fixture strings, side-effect render() calls, ad-hoc fixture vars
79
+ "tests/*" = ["E501", "F841"]
@@ -23,6 +23,7 @@ from .invoice_ingest import IngestionReport, ingest_invoice
23
23
  try:
24
24
  from .engine import CompileBackend, compile_typst, compile_typst_file
25
25
  from .templates import render_template
26
+
26
27
  _HAS_RENDER = True
27
28
  except ImportError:
28
29
  _HAS_RENDER = False
@@ -36,22 +37,29 @@ class RenderResult:
36
37
  trace_id: str | None = None
37
38
 
38
39
 
39
- __version__ = "0.3.3"
40
+ __version__ = "0.3.5"
40
41
 
41
42
  __all__ = [
42
43
  # Core API (always available)
43
- "validate_invoice", "ingest_invoice", "IngestionReport",
44
+ "validate_invoice",
45
+ "ingest_invoice",
46
+ "IngestionReport",
44
47
  # Rendering API (requires [render] extras)
45
- "render", "audit", "AuditResult",
48
+ "render",
49
+ "audit",
50
+ "AuditResult",
46
51
  # Errors
47
- "TrustRenderError", "ErrorCode",
52
+ "TrustRenderError",
53
+ "ErrorCode",
48
54
  # Meta
49
- "__version__", "bundled_font_dir",
55
+ "__version__",
56
+ "bundled_font_dir",
50
57
  ]
51
58
 
52
59
 
53
60
  # ── Core API: validation (always available) ──────────────────────────
54
61
 
62
+
55
63
  def validate_invoice(data: dict, *, zugferd: bool = False) -> dict:
56
64
  """Validate and normalize messy invoice data. Returns structured result.
57
65
 
@@ -100,10 +108,9 @@ def validate_invoice(data: dict, *, zugferd: bool = False) -> dict:
100
108
 
101
109
  if zugferd and report.render_ready:
102
110
  from .zugferd import validate_zugferd_invoice_data
111
+
103
112
  zug_errors = validate_zugferd_invoice_data(report.canonical)
104
- result["zugferd_errors"] = [
105
- {"path": e.path, "message": e.message} for e in zug_errors
106
- ]
113
+ result["zugferd_errors"] = [{"path": e.path, "message": e.message} for e in zug_errors]
107
114
  result["zugferd_ready"] = len(zug_errors) == 0
108
115
  elif zugferd:
109
116
  result["zugferd_errors"] = []
@@ -171,8 +178,7 @@ def _require_render():
171
178
  """Raise a clear error if rendering dependencies are not installed."""
172
179
  if not _HAS_RENDER:
173
180
  raise TrustRenderError(
174
- "Rendering requires additional dependencies. "
175
- "Install with: pip install trustrender[render]",
181
+ "Rendering requires additional dependencies. Install with: pip install trustrender[render]",
176
182
  code=ErrorCode.BACKEND_ERROR,
177
183
  stage="import",
178
184
  )
@@ -221,7 +227,7 @@ def _render_document_pipeline(
221
227
  trace = RenderTrace(
222
228
  template_name=display_name or template_path.name,
223
229
  template_hash=f"sha256:{hashlib.sha256(template_path.read_bytes()).hexdigest()[:16]}",
224
- data_hash=f"sha256:{hashlib.sha256(json.dumps(data, sort_keys=True, separators=(',',':')).encode()).hexdigest()[:16]}",
230
+ data_hash=f"sha256:{hashlib.sha256(json.dumps(data, sort_keys=True, separators=(',', ':')).encode()).hexdigest()[:16]}",
225
231
  engine_version=__version__,
226
232
  zugferd_profile=zugferd or "",
227
233
  validated=validate,
@@ -330,16 +336,18 @@ def _render_document_pipeline(
330
336
  timeout=timeout,
331
337
  pdf_standards=pdf_standards,
332
338
  )
333
- trace.stages.append(StageTrace(
334
- stage="compilation",
335
- status="pass",
336
- duration_ms=int((time.monotonic() - t0) * 1000),
337
- metadata={
338
- "template_type": "jinja2" if is_jinja else "raw",
339
- "pdf_standards": pdf_standards or [],
340
- "pdf_size": len(pdf_bytes),
341
- },
342
- ))
339
+ trace.stages.append(
340
+ StageTrace(
341
+ stage="compilation",
342
+ status="pass",
343
+ duration_ms=int((time.monotonic() - t0) * 1000),
344
+ metadata={
345
+ "template_type": "jinja2" if is_jinja else "raw",
346
+ "pdf_standards": pdf_standards or [],
347
+ "pdf_size": len(pdf_bytes),
348
+ },
349
+ )
350
+ )
343
351
 
344
352
  # 4. ZUGFeRD post-processing
345
353
  if zugferd:
@@ -363,12 +371,14 @@ def _render_document_pipeline(
363
371
  )
364
372
 
365
373
  pdf_bytes = apply_zugferd(pdf_bytes, xml_bytes)
366
- trace.stages.append(StageTrace(
367
- stage="zugferd_postprocess",
368
- status="pass",
369
- duration_ms=int((time.monotonic() - t0) * 1000),
370
- metadata={"xml_size": len(xml_bytes), "profile": zugferd},
371
- ))
374
+ trace.stages.append(
375
+ StageTrace(
376
+ stage="zugferd_postprocess",
377
+ status="pass",
378
+ duration_ms=int((time.monotonic() - t0) * 1000),
379
+ metadata={"xml_size": len(xml_bytes), "profile": zugferd},
380
+ )
381
+ )
372
382
  except TrustRenderError:
373
383
  raise
374
384
  except Exception as exc:
@@ -387,12 +397,14 @@ def _render_document_pipeline(
387
397
  t0 = time.monotonic()
388
398
  prov_record = create_provenance(template_path, data)
389
399
  pdf_bytes = embed_provenance(pdf_bytes, prov_record)
390
- trace.stages.append(StageTrace(
391
- stage="provenance",
392
- status="pass",
393
- duration_ms=int((time.monotonic() - t0) * 1000),
394
- metadata={"proof_hash": prov_record.proof[:30]},
395
- ))
400
+ trace.stages.append(
401
+ StageTrace(
402
+ stage="provenance",
403
+ status="pass",
404
+ duration_ms=int((time.monotonic() - t0) * 1000),
405
+ metadata={"proof_hash": prov_record.proof[:30]},
406
+ )
407
+ )
396
408
  trace.provenance_hash = prov_record.proof
397
409
 
398
410
  # Output fingerprint: hash the final PDF bytes after all post-processing
@@ -545,7 +557,8 @@ def audit(
545
557
  import time
546
558
 
547
559
  from .fingerprint import InputFingerprint, compare, compute_fingerprint
548
- from .regression import check_drift, save_baseline as _save_baseline
560
+ from .regression import check_drift
561
+ from .regression import save_baseline as _save_baseline
549
562
  from .semantic import SemanticReport, validate_semantics
550
563
 
551
564
  _SUPPORTED_ZUGFERD = {"en16931"}
@@ -623,9 +636,7 @@ def audit(
623
636
  pdf_bytes,
624
637
  zugferd_valid=zugferd is not None,
625
638
  contract_valid=validate,
626
- semantic_issue_count=(
627
- len(semantic_report.issues) if semantic_report else 0
628
- ),
639
+ semantic_issue_count=(len(semantic_report.issues) if semantic_report else 0),
629
640
  )
630
641
 
631
642
  # 6. Save baseline (if requested)
@@ -638,9 +649,7 @@ def audit(
638
649
  render_duration_ms=render_duration_ms,
639
650
  zugferd_valid=zugferd is not None if zugferd else None,
640
651
  contract_valid=True if validate else None,
641
- semantic_issue_count=(
642
- len(semantic_report.issues) if semantic_report else 0
643
- ),
652
+ semantic_issue_count=(len(semantic_report.issues) if semantic_report else 0),
644
653
  )
645
654
 
646
655
  return AuditResult(
@@ -659,8 +668,7 @@ def _resolve_data(data: dict | str | os.PathLike) -> dict:
659
668
 
660
669
  if not isinstance(data, (str, os.PathLike)):
661
670
  raise TrustRenderError(
662
- f"Data must be a dict, JSON string, or path to a .json file, "
663
- f"got {type(data).__name__}",
671
+ f"Data must be a dict, JSON string, or path to a .json file, got {type(data).__name__}",
664
672
  code=ErrorCode.INVALID_DATA,
665
673
  stage="data_resolution",
666
674
  )