#!/usr/bin/env python3 """Generate and validate UBL through Financewolf. Python 3, standard library only.""" import base64 import hashlib import json import os import pathlib import sys import urllib.error import urllib.request import uuid class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None def main(): if len(sys.argv) != 3: raise ValueError("Usage: python3 generate.py invoice.json invoice.xml") source = pathlib.Path(sys.argv[1]).read_bytes() if len(source) > 5 * 1024 * 1024: raise ValueError("Input exceeds 5 MiB") base = os.environ.get("FINANCEWOLF_API_BASE", "https://api.ironfang.uk/financewolf").rstrip("/") headers = {"Content-Type": "application/json", "Accept": "application/json", "User-Agent": "Financewolf-Quickstart/1.0"} if os.environ.get("IRONFANG_API_KEY"): headers["Authorization"] = "Bearer " + os.environ["IRONFANG_API_KEY"] headers["Idempotency-Key"] = os.environ.get("FINANCEWOLF_IDEMPOTENCY_KEY") or str(uuid.uuid4()) request = urllib.request.Request(base + "/v1/einvoices/generate?ruleset=latest&profile=peppol-bis-billing-3", data=source, headers=headers) with urllib.request.build_opener(NoRedirect()).open(request, timeout=30) as response: if response.headers.get_content_type() != "application/json": raise ValueError("Unexpected response type") raw = response.read(12 * 1024 * 1024 + 1) if len(raw) > 12 * 1024 * 1024: raise ValueError("Response exceeds 12 MiB") result = json.loads(raw) validation, artifact = result["validation"], result["artifact"] if (result["schema"] != "financewolf/einvoice/generation-result/v1" or result["status"] != "completed" or result["outcome"] != "valid" or validation["outcome"] != "valid" or [layer["layer"] for layer in validation["layers"]] != ["input", "xml", "xsd", "en16931", "peppol"] or any(layer["status"] != "passed" for layer in validation["layers"])): raise ValueError("Generation did not pass every validation layer") xml = base64.b64decode(artifact["data_base64"], validate=True) digest = hashlib.sha256(xml).hexdigest() if (not xml or len(xml) != artifact["bytes"] or digest != artifact["sha256"] or digest != validation["input"]["sha256"] or len(xml) != validation["input"]["bytes"]): raise ValueError("XML does not match its validation hash/length") pathlib.Path(sys.argv[2]).write_bytes(xml) print(json.dumps({"xml": sys.argv[2], "sha256": digest, "ruleset": validation["ruleset"]["id"], "operation_id": result.get("operation_id")})) if __name__ == "__main__": try: main() except urllib.error.HTTPError as error: print(error.read(65536).decode("utf-8", errors="replace"), file=sys.stderr) sys.exit(1) except (ValueError, KeyError, OSError) as error: print(str(error), file=sys.stderr) sys.exit(1)