attestwire
Validate EN 16931 e-invoices — XRechnung, Factur-X/ZUGFeRD, Peppol BIS 3 —
from Python. Standard library only (urllib, json, subprocess); nothing
to install beyond this package itself. Python 3.9+.
pip install attestwire
import attestwire
with open("invoice.xml", "rb") as f:
result = attestwire.validate(f.read(), api_key="aw_live_...")
if not result.valid:
for finding in result.errors:
print(finding.rule, finding.message)
print(" fix:", finding.fix)
data is the document itself — XML bytes/text, or a Factur-X/ZUGFeRD PDF's
bytes — sniffed from its content (%PDF, <, ...), not a file name. There is
no separate "read this file" step: pass the bytes you already have, from disk,
from a request body, from whatever generated them.
Two ways to validate
attestwire.validate(data, api_key=API_KEY) # mode="api" (default)
attestwire.validate(data, mode="cli") # mode="cli"
mode="api" (default) posts the document to the hosted Attestwire API —
POST https://api.attestwire.com/v1/validate — with your API key as
Authorization: Bearer. Needs network access and a key: pass api_key=, or
set the ATTESTWIRE_API_KEY environment variable. Get one free (no card
required) at POST /v1/keys — see
api.attestwire.com/docs#auth.
mode="cli" runs npx --yes @attestwire/en16931 --json <tmpfile>
locally: the document is written to a temporary file, handed to the engine's
own command-line validator, and the temp file is deleted afterwards.
Nothing is uploaded anywhere. Needs Node.js (npx) on PATH — the first
run downloads @attestwire/en16931 from npm if it isn't already cached, but
that's the validator being fetched, never your invoice. No API key needed.
Both return the same ValidationResult, so switching between them — api in
a production webhook, cli in a pre-commit hook or CI job with no key
configured — is a one-keyword change.
The result
@dataclass
class ValidationResult:
valid: bool
findings: list[Finding]
mode: str # "api" or "cli"
profile: str | None
syntax: str | None # "ubl" or "cii", when the document said which
container: str | None # the PDF attachment name, for a Factur-X/ZUGFeRD PDF
source: str | None # mode="api" only: what was checked and what it doesn't prove
raw: dict | None # the full parsed JSON, for anything not modelled above
errors: list[Finding] # severity == "fatal" — these set valid=False
warnings: list[Finding] # severity == "warning"
information: list[Finding] # severity == "information" (advisory)
@dataclass
class Finding:
rule: str | None # "BR-DE-15", "ATW-CREDIT-NOTE-...", or an "AW-*" whole-document finding
severity: str | None # "fatal" | "warning" | "information"
message: str | None
fix: str | None
field: ... # the business term(s), e.g. "BT-10" or ["BT-31", "BT-32"]
xpath: str | None
docs_url: str | None # https://attestwire.com/rules/<rule>, when the finding is a rule
example: str | None
location: Location | None # line/column/path in the document you sent, when applicable
@dataclass
class Location:
line: int | None
column: int | None
path: str | None
exact: bool | None
attachment: str | None # set only when the document was a PDF: which embedded XML this is in
findings is errors + warnings + information, in that order — already
tagged with its own severity, so filter it however you like; the three
properties above are there for when you just want one bucket.
Errors you handle vs. exceptions you don't expect
A non-compliant invoice is never an exception. validate() returns
normally with result.valid is False and the findings that explain why —
that's the whole point of the package. What can raise:
| Exception | When |
|---|---|
attestwire.ApiError |
mode="api" got an HTTP error status: .status, .code, .message, .docs, .upgrade_url (e.g. 401 invalid_api_key, 413 too_large, 429 rate_limited). |
attestwire.CliError |
mode="cli" couldn't run at all: Node/npx missing, the subprocess timed out, or its output wasn't the JSON it promises (message includes stderr). |
attestwire.AttestwireError |
mode="api" with no key available anywhere, or a network failure that never reached the API. Base class of the two above, if you want to catch either. |
TypeError |
data is neither bytes nor str. |
ValueError |
mode is neither "api" nor "cli". |
Recipes: the validation step for existing Python e-invoicing libraries
Both packages below build Factur-X/CII documents; neither is a full EN 16931/XRechnung/Peppol business-rule checker (see why for each). Attestwire plugs into the validation step either already needs to run elsewhere, or never had at all.
akretion's factur-x
factur-x builds and reads Factur-X
PDFs (CII or UBL XML embedded in a PDF/A-3) and can check the XML it produces
two ways: xml_check_xsd() (schema-only, local, no Java) and
xml_check_schematron() (the EN 16931/CIUS business rules — BR-*, BR-DE-*,
and friends). As of factur-x 6.0, that second check no longer runs
in-process: it removed the bundled saxonche engine and instead does an
HTTP POST to a Saxon Server you run yourself — every schematron-capable
function (xml_check_schematron, generate_from_file/generate_from_binary
with check_schematron=True, the get_*_xml_from_pdf extractors,
generate_cii_xml/generate_ubl_xml) takes saxon_server_url, defaulting to
http://localhost:5000/transform — i.e. it expects a Saxon HTTP server
already listening on your machine or network, which factur-x does not itself
install, start or document how to stand up.
Attestwire replaces that call with one that needs no Java, no Saxon server, and no server to keep running:
from facturx import generate_from_file
import attestwire
# 1. Build the PDF/A-3, XSD-checked only (fast, local, no Saxon involved) —
# this half of factur-x's validation is unaffected by any of this.
pdf_bytes = generate_from_binary(
pdf_content, xml_content, check_xsd=True, check_schematron=False,
)
# 2. Where you would otherwise call
# xml_check_schematron(xml_content, saxon_server_url="http://localhost:5000/transform")
# validate the business rules with Attestwire instead — no Saxon server to run:
result = attestwire.validate(xml_content, api_key=API_KEY) # or mode="cli": no key, no network
if not result.valid:
for finding in result.errors:
print(finding.rule, finding.message, "-- fix:", finding.fix)
raise SystemExit(1)
You can skip factur-x's get_facturx_xml_from_pdf() extraction step
entirely, too: attestwire.validate() reads a Factur-X/ZUGFeRD PDF directly
(same as the hosted API's POST /v1/validate and the local CLI both do) and
validates the CII XML it finds inside, reporting which attachment it came
from as result.container.
with open("invoice-facturx.pdf", "rb") as f:
result = attestwire.validate(f.read(), api_key=API_KEY)
print(result.container) # "factur-x.xml"
pretix's python-drafthorse
python-drafthorse is a
direct, 1:1 data binding for the CII XML format — you build a Document,
populate its trade/line-item tree, and call .serialize(). Its own
validation is XSD-only (Document.serialize(schema="FACTUR-X_EXTENDED") runs
the embedded XSD via lxml; pass schema=None to skip even that) — there is no
schematron or business-rule engine in it at all, so this isn't replacing a
Java step, it's adding the EN 16931/XRechnung layer python-drafthorse never
had. Schema-valid CII XML can still fail BR-DE-15 (no Leitweg-ID), use the
wrong VAT category, or omit a mandatory party field — none of which an XSD
catches.
from drafthorse.models.document import Document
from drafthorse.pdf import attach_xml
import attestwire
doc = Document()
# ... populate doc.header, doc.trade.agreement.seller / .buyer, line items ...
xml_bytes = doc.serialize(schema="FACTUR-X_EXTENDED") # XSD-only; no business rules
result = attestwire.validate(xml_bytes, api_key=API_KEY) # or mode="cli"
if not result.valid:
for finding in result.errors:
print(finding.rule, finding.message, "-- fix:", finding.fix)
raise SystemExit(1)
pdf_bytes = attach_xml(base_pdf_bytes, xml_bytes) # only once validation passed
mode="cli" in CI, with no API key
Because mode="cli" needs no account, it's the natural choice for a
pre-commit hook or a CI job that only has Node available:
result = attestwire.validate(generated_xml, mode="cli")
assert result.valid, "\n".join(f"{f.rule}: {f.message}" for f in result.errors)
Pin the engine version explicitly in CI for reproducibility:
attestwire.validate(xml, mode="cli", package_spec="@attestwire/en16931@0.12.1")
Development
python3 -m unittest discover -v
Every test mocks urllib.request.urlopen or subprocess.run — nothing here
touches the network or Node. tests/test_live.py is the one exception: it is
skipped unless you set ATTESTWIRE_LIVE_TEST=1 (and, for the API half,
ATTESTWIRE_API_KEY), and it exists specifically to catch the hosted
contract drifting from what the mocks assume — something mocks cannot catch
by construction.
ATTESTWIRE_LIVE_TEST=1 ATTESTWIRE_API_KEY=aw_live_... python3 -m unittest tests.test_live -v
Related
@attestwire/en16931— the rule enginemode="cli"runs locally.- Attestwire API docs — the full
request/response contract
mode="api"implements. - Rule reference — one page per rule, with the reason and the fix.
License
MIT.
Trademark
"Attestwire"™ and the Attestwire logo are trademarks of this project's owner. The MIT license covers the code and grants no trademark rights. You may say your integration uses this package, or is built on it; you may not name or brand a product or service "Attestwire", or imply that we endorse yours.
Metadata
Release files for attestwire 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 | |
|---|---|---|---|
| attestwire-0.1.0.tar.gz | 18.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| attestwire-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 34.7 kB
Release files / attestwire-0.1.0.tar.gz
| Download URL | attestwire-0.1.0.tar.gz |
|---|---|
| Size | 18.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1222863ca55a9afe362f124d197b1b83129cde6c5d8117714346dcc86b96cdaa
|
|
BLAKE2b-256 checksum How to use checksums |
5e802543ce883be55b018a8c3481eafd6fdded56177eac5cd49189b72d736b3b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.7
|
Release files / attestwire-0.1.0-py3-none-any.whl
| Download URL | attestwire-0.1.0-py3-none-any.whl |
|---|---|
| Size | 16.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2a77ffa0f6a6b0cef622384e76afc47da68491769ad565dcab79bc0231adb3c7
|
|
BLAKE2b-256 checksum How to use checksums |
089c600a32b32dff043359687183143437d7312f271aa5ec29194dc193e18d56
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.7
|