pain001.corpus package#

The example corpus engine (ADR-0003).

This package grows over the 0.0.67 to 0.0.69 releases. Today it holds the schema inventory and coverage yardstick, the scenario loader and the version-aware builder with its identifier factories, the shipped market files under data/market, the generated coverage sets under data/coverage and, under rules, the MDR cross-element rules, the overlay grammar, the XML-to-row projection, the validation ladder and the vendored ISO external code sets. The market packs for the sixteen countries follow in 0.0.68 and 0.0.69.

Subpackages#

Submodules#

pain001.corpus.api module#

The shipped corpus, from the installed package.

Everything under pain001/corpus/data ships in the wheel: the market files with their provenance sidecars and the coverage sets with their reports. These functions read them without a checkout, which is what the suite’s other members (the MCP server, the LSP, the website build) need.

class pain001.corpus.api.CorpusFile(kind: str, version: str, path: Path, scenario_id: str | None = None, country: str | None = None, family: str | None = None, variant: str | None = None)[source]#

Bases: object

One shipped XML file.

Variables:
  • kind (str) – market or coverage.

  • version (str) – The message type.

  • path (pathlib.Path) – Where the file is.

  • scenario_id (str | None) – The scenario, for market files.

  • country (str | None) – ISO 3166 alpha-2, for market files.

  • family (str | None) – The rail family, for market files.

  • variant (str | None) – The overlay id a bank variant was built with.

country: str | None = None#
family: str | None = None#
kind: str#
path: Path#
read() → str[source]#

The file’s text.

scenario_id: str | None = None#
twin(fmt: str = 'iso-json') → Any[source]#

A twin of the file (ADR-0005).

Parameters:

fmt – iso-json for the ISO JSON twin as a dict (the shipped .iso.json when there is one, else computed), or records for the RecordsTwin.

Returns:

The twin.

Raises:

ValueError – For an unknown format.

variant: str | None = None#
version: str#
pain001.corpus.api.coverage_report(version: str) → dict[str, Any][source]#

The shipped coverage verdict of one edition.

Parameters:

version – The message type.

Returns:

sources, path and branch counts and percentages, completeness, missing and exempt lists.

Return type:

The coverage.json content

Raises:

FileNotFoundError – If the edition has no coverage set.

pain001.corpus.api.get_file(scenario_id: str, version: str, variant: str | None = None) → str[source]#

The text of one market file.

Parameters:
  • scenario_id – The scenario, e.g. gb.chaps.property-purchase.

  • version – The message type, e.g. pain.001.001.09.

  • variant – An overlay id for a bank variant, else the generic file.

Returns:

The XML text.

Raises:

FileNotFoundError – If the scenario has no file for that edition.

pain001.corpus.api.get_twin(scenario_id: str, version: str, fmt: str = 'iso-json', variant: str | None = None) → Any[source]#

A twin of one market file (ADR-0005).

Parameters:
  • scenario_id – The scenario.

  • version – The message type; twins cover pain.001 editions.

  • fmt – iso-json or records.

  • variant – An overlay id for a bank variant, else the generic file.

Returns:

The ISO JSON twin as a dict, or the records twin.

Raises:

FileNotFoundError – If the scenario has no file for that edition.

pain001.corpus.api.list_files(kind: str | None = None) → list[CorpusFile][source]#

Every shipped corpus file: market files, then coverage sets, each by path.

Parameters:

kind – market, coverage or None for both.

Returns:

The files.

pain001.corpus.api.provenance(scenario_id: str, version: str, variant: str | None = None) → dict[str, Any][source]#

The provenance sidecar of one market file, parsed.

Parameters:
  • scenario_id – The scenario.

  • version – The message type.

  • variant – An overlay id for a bank variant, else the generic file.

Returns:

sources, confidence, evidence, build report, validation ladder and SHA-256.

Return type:

The sidecar

Raises:

FileNotFoundError – If the scenario has no file for that edition.

pain001.corpus.builder module#

Scenario to XML, one edition at a time (ADR-0003, decision 6).

The builder works in four passes:

  1. compose: the friendly scenario keys become a tree keyed by ISO element names (GrpHdr, PmtInf …), the same tree for every edition. raw objects are merged in verbatim, so anything the friendly layer does not name can still be expressed.

  2. conform: the tree is walked against the target edition’s Inventory. An element the edition spells differently is renamed (BIC and BICFI, BICOrBEI and AnyBIC); a scalar that meets a choice is wrapped (AdrTp became Cd/Prtry in .09, ReqdExctnDt became Dt/DtTm); a wrapped value that meets a plain element is unwrapped; a repeat the edition caps is cut to the cap; an element the edition does not declare is dropped. Every such move is listed in the BuildReport.

  3. order: children are sorted into the schema’s declared order, read from the inventory, and empty branches pruned.

  4. serialise and validate: lxml writes the document with a fixed indentation and declaration, then the edition’s XSD must accept it, or BuildError carries the reasons.

Values are strings all the way through, so 425000.00 renders as written and a rebuild is byte-identical.

pain001.corpus.builder.ALIASES: dict[str, tuple[str, ...]] = {'AnyBIC': ('BICOrBEI',), 'BIC': ('BICFI',), 'BICFI': ('BIC',), 'BICOrBEI': ('AnyBIC',)}#

Elements that changed name between editions but not meaning.

exception pain001.corpus.builder.BuildError[source]#

Bases: ValueError

The rendered document did not validate; the message says why.

class pain001.corpus.builder.BuildReport(version: str, renamed: list[tuple[str, str]] = <factory>, wrapped: list[str] = <factory>, unwrapped: list[str] = <factory>, dropped: list[str] = <factory>, truncated: list[str] = <factory>)[source]#

Bases: object

What the conform pass had to do to fit the edition.

Variables:
  • version (str) – The edition built.

  • renamed (list[tuple[str, str]]) – (path, new_name) for each alias applied.

  • wrapped (list[str]) – Paths where a scalar was wrapped into a choice child.

  • unwrapped (list[str]) – Paths where a wrapped value was flattened.

  • dropped (list[str]) – Paths the edition does not declare, left out.

  • truncated (list[str]) – Paths where a repeat was cut to the edition’s cap.

dropped: list[str]#
renamed: list[tuple[str, str]]#
truncated: list[str]#
unwrapped: list[str]#
version: str#
wrapped: list[str]#
class pain001.corpus.builder.BuildResult(version: str, xml: str, report: BuildReport)[source]#

Bases: object

A rendered edition and the report of how it was fitted.

report: BuildReport#
version: str#
xml: str#
pain001.corpus.builder.NUMBER_FIRST = frozenset({'AU', 'CA', 'GB', 'HK', 'IE', 'MY', 'SG', 'US'})#

Markets that write the house number before the street on a line.

pain001.corpus.builder.WRAP_PREFERENCE: tuple[str, ...] = ('Cd', 'Dt', 'DtTm', 'Tp', 'Id')#

When a scalar meets a complex slot, the child that carries it.

pain001.corpus.builder.build(scenario: Scenario | dict[str, Any], version: str, patch: dict[str, Any] | None = None) → BuildResult[source]#

Render one scenario in one edition.

Parameters:
  • scenario – A Scenario or a scenario document.

  • version – A bundled message type the scenario lists.

  • patch – Scenario keys to merge in first, as an overlay’s patch does for a bank variant.

Returns:

The BuildResult with XML text and the fit report.

Raises:

BuildError – If the scenario cannot be composed, does not list the version, or the rendered document fails the XSD.

pain001.corpus.builder.build_all(scenario: Scenario) → list[BuildResult][source]#

Render a scenario in every edition it lists, in listed order.

pain001.corpus.builder.deep_merge(base: dict[str, Any], patch: dict[str, Any]) → dict[str, Any][source]#

A copy of base with patch merged in, nested dicts recursively.

Parameters:
  • base – The scenario document.

  • patch – Keys to set; a nested dict merges, anything else replaces; {"*": {...}} against a list merges into every item; None removes the key.

Returns:

The merged copy; neither input is modified.

pain001.corpus.coverage_sets module#

Generate the schema coverage set of an edition (ADR-0003, WS3).

The coverage corpus proves what each XSD can express: every element path and every choice branch appears in at least one file of the edition’s set, and every file is schema-valid. Nothing hand-written could keep that promise for 1,700 paths per edition, so the set is derived from the Inventory:

  • every leaf gets a value derived from its facets: an enumeration’s first member, a verified sample for each of the twelve patterns the ISO schemas use, the minimum length of a text, 1.00 for an amount, fixed dates, true for a flag;

  • the first file carries every element once, taking the first branch of every choice; each following file carries only the subtrees that still hold an unhit path or branch (plus whatever is mandatory around them), taking the least-covered branch of every choice, until the coverage() report is complete;

  • every file must pass the edition’s XSD before it is kept.

Later files are therefore small, and an edition needs a handful of files: one per recipe plus one per extra choice branch.

The MDR cross-element rules (pain001.corpus.rules.mdr) shape the set too, so every file is a payment the report would accept: blocks allowed on PmtInf or on the transaction but not both are steered like choice branches; the cheque path has its own recipes (delivered to the creditor agent, or not), transfers never carry a cheque instruction; intermediary agents and charges accounts pull in their prerequisites; a mandate’s amendment flag follows its details.

pain001.corpus.coverage_sets.ANY_CONTENT = {'Data': 'x'}#

The element written into an xs:any slot.

exception pain001.corpus.coverage_sets.CoverageBuildError[source]#

Bases: ValueError

A generated file did not validate, or a facet has no sample.

class pain001.corpus.coverage_sets.CoverageFileInfo(name: str, recipe: str, description: str, focus: tuple[str, ...], adds_paths: int, adds_branches: int)[source]#

Bases: object

What one file of a set is for, so its name means something.

Variables:
  • name (str) – The file name, NN-<recipe>-<focus>.xml.

  • recipe (str) – The recipe the file follows.

  • description (str) – One sentence a reader can pick the file by.

  • focus (tuple[str, ...]) – The blocks (children of PmtInf, of the transaction, or of GrpHdr) the file’s new coverage mostly falls under.

  • adds_paths (int) – Declared paths this file is the first to use.

  • adds_branches (int) – Choice branches this file is the first to take.

adds_branches: int#
adds_paths: int#
description: str#
focus: tuple[str, ...]#
name: str#
recipe: str#
class pain001.corpus.coverage_sets.CoverageSet(version: str, files: tuple[str, ...], report: CoverageReport, manifest: tuple[CoverageFileInfo, ...] = ())[source]#

Bases: object

A generated set, what each file is for, and the report.

files: tuple[str, ...]#
manifest: tuple[CoverageFileInfo, ...] = ()#
report: CoverageReport#
version: str#
pain001.corpus.coverage_sets.EXCLUSIVE: dict[str, tuple[tuple[str, str], ...]] = {'pain.001': (('PmtTpInf', 'CdtTrfTxInf/PmtTpInf'), ('ChrgBr', 'CdtTrfTxInf/ChrgBr'), ('UltmtDbtr', 'CdtTrfTxInf/UltmtDbtr'), ('InstrForDbtrAgt', 'CdtTrfTxInf/InstrForDbtrAgt')), 'pain.008': (('PmtTpInf', 'DrctDbtTxInf/PmtTpInf'), ('ChrgBr', 'DrctDbtTxInf/ChrgBr'), ('UltmtCdtr', 'DrctDbtTxInf/UltmtCdtr'), ('CdtrSchmeId', 'DrctDbtTxInf/DrctDbtTx/CdtrSchmeId'))}#

Blocks allowed on PmtInf or on the transaction, never both.

pain001.corpus.coverage_sets.PATTERN_SAMPLES: dict[str, str] = {'[0-9]{1,15}': '1', '[0-9]{2}': '01', '[A-Z0-9]{18,18}[0-9]{2,2}': '000000K1PEBE35ILCW94', '[A-Z0-9]{18}[0-9]{2}[A-Z0-9]{0,32}': '000000K1PEBE35ILCW94', '[A-Z0-9]{4,4}[A-Z]{2,2}[A-Z0-9]{2,2}([A-Z0-9]{3,3}){0,1}': 'BANKDEFFXXX', '[A-Z]{2,2}': 'DE', '[A-Z]{2,2}[0-9]{2,2}[a-zA-Z0-9]{1,30}': 'DE89370400440532013000', '[A-Z]{3,3}': 'EUR', '[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}': 'BANKDEFFXXX', '[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}': 'c10f2393-6d2a-456b-8d41-d976a10e55ea', '[a-zA-Z0-9]{4}': 'ABCD', '\\+[0-9]{1,3}-[0-9()+\\-]{1,30}': '+49-3012345678'}#

A verified sample for every pattern the bundled schemas declare.

pain001.corpus.coverage_sets.PREREQUISITES: dict[str, str] = {'ChrgsAcctAgt': 'ChrgsAcct', 'IntrmyAgt1Acct': 'IntrmyAgt1', 'IntrmyAgt2': 'IntrmyAgt1', 'IntrmyAgt2Acct': 'IntrmyAgt2', 'IntrmyAgt3': 'IntrmyAgt2', 'IntrmyAgt3Acct': 'IntrmyAgt3'}#

An element that, when emitted, needs its sibling emitted too.

pain001.corpus.coverage_sets.RECIPE_LABELS: dict[str, str] = {'cheque-no-agent': 'cheque with no creditor agent (PmtMtd CHK)', 'cheque-to-agent': 'cheque delivered to the creditor agent (PmtMtd CHK)', 'collection': 'direct debit collection (PmtMtd DD)', 'transfer': 'credit transfer (PmtMtd TRF)'}#

What each recipe shapes, in words a reader of the file name needs.

class pain001.corpus.coverage_sets.Recipe(name: str, pins: dict[str, str], forbid: frozenset[str] = frozenset({}), require: frozenset[str] = frozenset({}))[source]#

Bases: object

How one file of a set is shaped to satisfy the MDR rules.

Paths are relative to PmtInf (CdtTrfTxInf/ChqInstr).

Variables:
  • name (str) – Short label.

  • pins (dict[str, str]) – Values that override the facet sample at a path.

  • forbid (frozenset[str]) – Paths never emitted.

  • require (frozenset[str]) – Paths always emitted, even in a sparse file.

forbid: frozenset[str] = frozenset({})#
name: str#
pins: dict[str, str]#
require: frozenset[str] = frozenset({})#
pain001.corpus.coverage_sets.build_coverage_set(version: str, max_files: int = 16) → CoverageSet[source]#

Generate the coverage set of a bundled edition.

Parameters:
  • version – The message type.

  • max_files – Safety cap on the number of files.

Returns:

The CoverageSet; its report is complete unless the cap was hit.

Raises:

CoverageBuildError – If a generated file fails the XSD.

pain001.corpus.coverage_sets.describe_file(index: int, recipe: Recipe, new_paths: int, new_branches: int, focus: tuple[str, ...]) → CoverageFileInfo[source]#

Name and describe the index-th file of a set.

Parameters:
  • index – One-based position in the set.

  • recipe – The recipe the file follows.

  • new_paths – Paths the file is the first to use.

  • new_branches – Branches the file is the first to take.

  • focus – The blocks the new coverage mostly falls under.

Returns:

The CoverageFileInfo.

pain001.corpus.coverage_sets.sample_value(entry: ElementEntry) → str[source]#

A schema-valid value for a leaf, from its facets.

Parameters:

entry – A simple or attribute inventory entry.

Returns:

The value.

Raises:

CoverageBuildError – If a pattern has no sample in the table.

pain001.corpus.identifiers module#

Synthetic identifiers that pass every structural check (ADR-0003, 5).

Corpus files need account numbers, BICs and LEIs that validators accept without pointing at anyone’s real account. Every factory here is deterministic in its seed, so a scenario renders the same bytes on every build, and produces values that satisfy the published structure: IBAN mod-97 with the country’s BBAN shape and its internal check digits (Belgian mod 97, French RIB key, Italian CIN, Spanish control digits, Czech mod 11, Swedish Luhn over clearing and account, Dutch eleven-proof), the SWIFT BIC pattern in its test-and-training form (0 as the eighth character), ISO 17442 LEIs under an unassigned LOU prefix, ABA routing numbers with their check digit, and RFC 4122 v4-shaped UETRs.

Real public routing codes (sort codes, BLZ, ABI/CAB, bank codes) are fine to use and the scenarios do; the account parts are synthetic.

pain001.corpus.identifiers.COUNTRIES: tuple[str, ...] = ('GB', 'FR', 'NL', 'DE', 'IT', 'BE', 'ES', 'US', 'CZ', 'CH', 'SE', 'LU', 'HK', 'SG', 'MY', 'QA', 'AE')#

The sixteen markets of the plan; “SW” covers Switzerland and Sweden.

pain001.corpus.identifiers.LEI_PREFIX = '0000'#

An LOU prefix no operator has been assigned, so a corpus LEI can never collide with a registered one.

pain001.corpus.identifiers.NON_IBAN: frozenset[str] = frozenset({'HK', 'MY', 'SG', 'US'})#

Markets whose accounts are not expressed as IBANs.

pain001.corpus.identifiers.aba_check_digit(first_eight: str) → str[source]#

The ninth digit of an ABA routing number.

pain001.corpus.identifiers.aba_is_valid(routing: str) → bool[source]#

True when routing is nine digits with the right check digit.

pain001.corpus.identifiers.account_for(country: str, seed: int) → dict[str, str][source]#

A domestic account identification for a non-IBAN market.

Parameters:
  • country – US, HK, SG or MY.

  • seed – Any integer.

Returns:

{"routing": ..., "number": ...} with the market’s routing identifier (ABA number, HK bank+branch, SG bank+branch, MY bank code) and a synthetic account number.

Raises:

ValueError – If the country is an IBAN market.

pain001.corpus.identifiers.belgian_check(body: str) → str[source]#

Belgian account check: body mod 97, with 0 written as 97.

pain001.corpus.identifiers.bic_is_test(bic: str) → bool[source]#

True when the BIC carries the test-and-training marker.

pain001.corpus.identifiers.czech_mod11_ok(number: str) → bool[source]#

Czech prefix/account numbers weight 6,3,7,9,10,5,8,4,2,1 mod 11.

pain001.corpus.identifiers.czech_number(rng: _Stream, length: int) → str[source]#

A number of length digits that passes the Czech mod-11 check.

pain001.corpus.identifiers.dutch_eleven_proof(rng: _Stream) → str[source]#

A ten-digit Dutch account number that passes the eleven-proof.

pain001.corpus.identifiers.french_rib_key(bank: str, branch: str, account: str) → str[source]#

The RIB key: 97 minus (89·bank + 15·branch + 3·account) mod 97.

pain001.corpus.identifiers.iban_check_digits(country: str, bban: str) → str[source]#

The two check digits that make country + ?? + bban valid.

pain001.corpus.identifiers.iban_for(country: str, seed: int) → str[source]#

A valid, synthetic IBAN in the country’s BBAN shape.

Parameters:
  • country – One of the IBAN markets in COUNTRIES.

  • seed – Any integer; the same seed gives the same IBAN.

Returns:

The IBAN.

Raises:

ValueError – If the country has no IBAN shape here.

pain001.corpus.identifiers.iban_is_valid(iban: str) → bool[source]#

True when the IBAN has its country’s length and mod-97 remainder 1.

pain001.corpus.identifiers.italian_cin(abi: str, cab: str, account: str) → str[source]#

The CIN control letter over ABI + CAB + account.

pain001.corpus.identifiers.lei_check_digits(body: str) → str[source]#

ISO 17442 check digits (MOD 97-10 over the 18-character body).

pain001.corpus.identifiers.lei_is_valid(lei: str) → bool[source]#

True when lei is twenty characters and passes MOD 97-10.

pain001.corpus.identifiers.luhn_check_digit(body: str) → str[source]#

The Luhn (mod 10) check digit that completes body.

pain001.corpus.identifiers.make_aba(seed: int) → str[source]#

A nine-digit ABA routing number with a valid check digit.

pain001.corpus.identifiers.make_bic(country: str, seed: int, branch: bool = False) → str[source]#

A BIC in the SWIFT test-and-training form.

The eighth character is 0, which SWIFT reserves for test BICs, so the value matches the ISO pattern and can never be a live institution’s code.

Parameters:
  • country – ISO 3166 alpha-2 code for characters five and six.

  • seed – Any integer.

  • branch – Append a three-character branch code.

Returns:

An eight- or eleven-character BIC.

pain001.corpus.identifiers.make_iban(country: str, bban: str) → str[source]#

Assemble a valid IBAN from a country and its BBAN.

Parameters:
  • country – ISO 3166 alpha-2 code.

  • bban – The basic bank account number, already shaped.

Returns:

The IBAN.

Raises:

ValueError – If the BBAN length does not match the country.

pain001.corpus.identifiers.make_lei(seed: int) → str[source]#

A structurally valid LEI under LEI_PREFIX.

Parameters:

seed – Any integer.

Returns:

A twenty-character LEI whose check digits verify.

pain001.corpus.identifiers.make_uetr(seed: int) → str[source]#

A deterministic RFC 4122 version-4-shaped UUID for PmtId/UETR.

Parameters:

seed – Any integer.

Returns:

The canonical lower-case hyphenated form.

pain001.corpus.identifiers.mod97(text: str) → int[source]#

ISO 7064 MOD 97-10 remainder of a digit-and-letter string.

pain001.corpus.identifiers.spanish_control_digits(bank: str, branch: str, account: str) → str[source]#

The two Spanish DC digits over bank+branch and account.

pain001.corpus.identifiers.to_digits(text: str) → str[source]#

Map letters to two-digit values (A=10 … Z=35), as ISO 7064 does.

Parameters:

text – Digits and upper-case letters.

Returns:

The digit string.

pain001.corpus.inventory module#

What an XSD can express, and how much of it a set of files uses.

An Inventory lists every element path an ISO 20022 schema declares, every attribute, every xs:any slot, and every xs:choice group with its branches, with the leaf facets (length, pattern, enumeration, digits, bounds) a file must respect. It is the yardstick ADR-0003 defines coverage against: every element path and every choice branch hit by at least one file of a version’s coverage set, with a named exemption list.

coverage() measures a set of XML files against an inventory and returns a CoverageReport; scripts/corpus_coverage.py turns that into the make corpus-coverage gate.

Paths are slash-joined local names from the root, /Document/...; attributes are path/@name; an xs:any slot is path/*. A choice is identified by the path of the element that holds it, and a branch by the local names of the elements that start it.

class pain001.corpus.inventory.ChoiceEntry(path: str, branches: tuple[tuple[str, ...], ...])[source]#

Bases: object

An xs:choice group and the branches a file can take.

Variables:
  • path (str) – Path of the element whose content holds the choice.

  • branches (tuple[tuple[str, ...], ...]) – One tuple per branch, holding the local names of the elements that branch starts with (("*",) for xs:any).

branch_ids() → list[str][source]#

Stable identifiers, path -> A+B, one per branch.

branches: tuple[tuple[str, ...], ...]#
path: str#
class pain001.corpus.inventory.CoverageReport(message_type: str, sources: tuple[str, ...], hit_paths: tuple[str, ...], missing_paths: tuple[str, ...], hit_branches: tuple[str, ...], missing_branches: tuple[str, ...], exempt: tuple[str, ...], unknown: tuple[str, ...])[source]#

Bases: object

How much of an inventory a set of files exercises.

Variables:
  • message_type (str) – The inventory’s message type.

  • sources (tuple[str, ...]) – The files measured, as given.

  • hit_paths (tuple[str, ...]) – Declared paths at least one file uses.

  • missing_paths (tuple[str, ...]) – Declared paths no file uses, exemptions removed.

  • hit_branches (tuple[str, ...]) – Choice branch ids at least one file takes.

  • missing_branches (tuple[str, ...]) – Branch ids no file takes, exemptions removed.

  • exempt (tuple[str, ...]) – Paths and branch ids excluded from the gate, as given.

  • unknown (tuple[str, ...]) – Paths the files use that the inventory does not declare.

property branch_percent: float#

Hit branches over declared branches, exemptions excluded.

property complete: bool#

True when nothing non-exempt is missing.

exempt: tuple[str, ...]#
hit_branches: tuple[str, ...]#
hit_paths: tuple[str, ...]#
message_type: str#
missing_branches: tuple[str, ...]#
missing_paths: tuple[str, ...]#
property path_percent: float#

Hit paths over declared paths, exemptions excluded.

sources: tuple[str, ...]#
to_dict() → dict[str, Any][source]#

A JSON-ready dict with the percentages included.

unknown: tuple[str, ...]#
class pain001.corpus.inventory.ElementEntry(path: str, kind: str, type_name: str | None = None, min_occurs: int = 1, max_occurs: int | None = 1, primitive: str | None = None, facets: dict[str, ~typing.Any] = <factory>)[source]#

Bases: object

One declared slot in a schema: element, attribute or xs:any.

Variables:
  • path (str) – Slash-joined local names from /Document.

  • kind (str) – complex, simple, attribute or any.

  • type_name (str | None) – The schema type’s local name, if it has one.

  • min_occurs (int) – Minimum occurrences at this position.

  • max_occurs (int | None) – Maximum occurrences, None for unbounded.

  • primitive (str | None) – The XSD primitive a simple value reduces to.

  • facets (dict[str, Any]) – Normalised facets: enumeration (list), patterns (list of regex strings) and the length, digit and bound facets keyed by their snake_case name.

facets: dict[str, Any]#
kind: str#
max_occurs: int | None = 1#
min_occurs: int = 1#
path: str#
primitive: str | None = None#
type_name: str | None = None#
class pain001.corpus.inventory.Inventory(message_type: str, namespace: str, elements: tuple[ElementEntry, ...], choices: tuple[ChoiceEntry, ...])[source]#

Bases: object

Everything one schema can express.

Variables:
choices: tuple[ChoiceEntry, ...]#
elements: tuple[ElementEntry, ...]#
classmethod from_dict(data: dict[str, Any]) → Inventory[source]#

Rebuild an inventory from to_dict() output.

Parameters:

data – The dict form of an inventory.

Returns:

The equivalent Inventory.

message_type: str#
namespace: str#
paths() → frozenset[str][source]#

Every declared path, attributes and any slots included.

to_dict() → dict[str, Any][source]#

A JSON-ready dict; from_dict() reverses it.

to_json(indent: int | None = 2) → str[source]#

The dict form as JSON text.

pain001.corpus.inventory.MAX_DEPTH = 64#

Descent stops when a complex type appears again on its own path. ISO 20022 payment schemas are not recursive, so the cap is a guard, not a tuning knob.

pain001.corpus.inventory.build_inventory(xsd_path: str | Path, message_type: str) → Inventory[source]#

Inventory one schema file.

Parameters:
  • xsd_path – The XSD to walk; its root element must be Document.

  • message_type – The message type to label the inventory with.

Returns:

The schema’s Inventory.

pain001.corpus.inventory.coverage(inventory: Inventory, sources: Iterable[str | Path], exemptions: Iterable[str] = ()) → CoverageReport[source]#

Measure files against an inventory.

Parameters:
  • inventory – The yardstick.

  • sources – XML files or XML texts; their paths are unioned.

  • exemptions – Paths or branch ids (path -> A+B) to leave out of the missing lists. Unknown names are ignored.

Returns:

The CoverageReport.

pain001.corpus.inventory.inventory_for(message_type: str) → Inventory[source]#

The inventory of a bundled message type’s XSD, cached.

Parameters:

message_type – A registered message type, e.g. pain.001.001.09.

Returns:

The bundled schema’s Inventory. A message type that is not bundled propagates the registry’s KeyError.

pain001.corpus.inventory.present_paths(source: str | Path) → frozenset[str][source]#

The element and attribute paths one XML document uses.

Parameters:

source – A path to an XML file, or XML text.

Returns:

Paths in inventory form, namespaces stripped.

pain001.corpus.registry module#

Scenario documents: what a payment is, validated and discovered.

A scenario is a YAML document conforming to pain001/corpus/schema/corpus.schema.json. It names the payment’s parties, accounts, agents, rail choices and transactions in friendly keys; pain001.corpus.builder turns it into each ISO edition the scenario lists. Scenarios live under scenarios/ at the repository root, the source of truth that is not shipped; the built files are.

pain001.corpus.registry.SCENARIOS_DIR = PosixPath('/home/runner/work/pain001/pain001/scenarios')#

The source-of-truth directory in a checkout (absent from the wheel).

class pain001.corpus.registry.Scenario(id: str, family: str, country: str, versions: tuple[str, ...], data: dict[str, Any], source: Path | None = None)[source]#

Bases: object

One validated scenario.

Variables:
  • id (str) – The scenario id, country.rail.name.

  • family (str) – The rail family, e.g. priority-payment.

  • country (str) – ISO 3166 alpha-2 market.

  • versions (tuple[str, ...]) – The message types the scenario renders to.

  • data (dict[str, Any]) – The whole validated document.

  • source (pathlib.Path | None) – Where it was loaded from, if a file.

country: str#
data: dict[str, Any]#
family: str#
id: str#
property message: str#

pain.001 or pain.008, from the first version.

property overlays: tuple[str, ...] | None#

Overlay ids to apply (L3), or None to apply by applies_to.

property profiles: tuple[str, ...]#

Scheme and rail profile names the built files must pass (L2).

property seed: int#

seed if given, else a hash of the id.

Type:

The identifier seed

source: Path | None = None#
versions: tuple[str, ...]#
exception pain001.corpus.registry.ScenarioError[source]#

Bases: ValueError

A scenario document is malformed; the message names where.

pain001.corpus.registry.load_scenario(path: str | Path) → Scenario[source]#

Load and validate one YAML scenario file.

Parameters:

path – The .yaml file.

Returns:

The Scenario.

Raises:

ScenarioError – If the YAML does not parse or fails the schema.

pain001.corpus.registry.load_scenarios(root: str | Path = PosixPath('/home/runner/work/pain001/pain001/scenarios')) → list[Scenario][source]#

Load every *.yaml under root, sorted by id.

The overlays subtree holds overlay files, not scenarios, and is skipped.

Parameters:

root – The scenarios directory.

Returns:

The scenarios; empty when the directory does not exist.

Raises:

ScenarioError – If any file is invalid, or two share an id.

pain001.corpus.registry.scenario_from(document: Any, source: Path | None = None) → Scenario[source]#

Validate a document and wrap it.

Parameters:
  • document – The parsed scenario.

  • source – The file it came from, for messages.

Returns:

The Scenario.

Raises:

ScenarioError – If the document fails the schema, or lists editions of more than one message family.

pain001.corpus.registry.schema() → dict[str, Any][source]#

The scenario JSON schema, loaded once.

pain001.corpus.registry.validate_document(document: Any, source: str = '<scenario>') → None[source]#

Check a document against the scenario schema.

Parameters:
  • document – The parsed YAML/JSON.

  • source – A label for error messages, usually the file path.

Raises:

ScenarioError – With the JSON pointer and reason of the first problem, and the count of others.