pain001 package#

The Python pain001 module.

Subpackages#

Submodules#

pain001.async_adapter module#

Async-ready wrappers around the synchronous library entry points.

async pain001.async_adapter.generate_xml_string_async(data: list[dict[str, object]], payment_initiation_message_type: str, xml_template_path: str, xsd_schema_path: str) → str[source]#

Run generate_xml_string() in a worker thread.

async pain001.async_adapter.load_payment_data_async(data_source: str | list[dict[str, Any]] | dict[str, Any]) → list[dict[str, Any]][source]#

Run load_payment_data() in a worker thread.

async pain001.async_adapter.process_files_async(xml_message_type: str, xml_template_file_path: str, xsd_schema_file_path: str, data_file_path: str) → str[source]#

Run process_files() in a worker thread.

async pain001.async_adapter.process_files_streaming_async(xml_message_type: str, xml_template_file_path: str, xsd_schema_file_path: str, data_file_path: str, chunk_size: int = 1000) → list[str][source]#

Run process_files_streaming() in a worker thread.

async pain001.async_adapter.validate_all_async(validation_service: ValidationService, config: ValidationConfig) → Any[source]#

Run ValidationService.validate_all() in a worker thread.

pain001.constants module#

Shared constants and configuration for the pain001 library.

pain001.exceptions module#

Custom exception hierarchy for Pain001.

This module provides granular exception types to enable precise error handling in banking integrations. Instead of catching generic ValueError or TypeError, consuming applications can distinguish between data validation errors, configuration errors, and XML generation failures.

Example

>>> try:
...     process_files(...)
... except PaymentValidationError as e:
...     # Handle invalid IBAN/BIC - notify user
...     log.error(f"Payment data invalid: {e}")
... except XMLGenerationError as e:
...     # Handle XML generation failure - check templates
...     log.error(f"XML generation failed: {e}")
... except ConfigurationError as e:
...     # Handle config issues - check setup
...     log.error(f"Configuration error: {e}")
exception pain001.exceptions.ConfigurationError[source]#

Bases: Pain001Error

Raised when configuration or setup is invalid.

This exception indicates issues with: - Missing or invalid setup.cfg - Invalid CLI arguments - Missing required environment variables - Invalid file paths - Unsupported ISO 20022 version

Example

>>> try:
...     load_config("pain.001.001.99")
... except ConfigurationError as e:
...     # Config error - show usage help
...     print(f"Configuration error: {e}")
...     print_usage_help()
exception pain001.exceptions.DataSourceError[source]#

Bases: Pain001Error

Raised when data source access fails.

This exception indicates issues with: - File not found (CSV, SQLite) - Database connection errors - Corrupted data files - Unsupported file formats - Empty data sources

Example

>>> try:
...     load_payment_data("payments.csv")
... except DataSourceError as e:
...     # Data access error - check file exists
...     log.error(f"Cannot access data source: {e}")
exception pain001.exceptions.InvalidBICError(message: str, bic: str, field: str | None = None, reason: str | None = None)[source]#

Bases: PaymentValidationError

Raised when BIC/SWIFT validation fails.

This exception indicates issues with: - Invalid BIC format (must be 8 or 11 characters) - Invalid BIC structure (ISO 9362) - Invalid country code in BIC - Invalid bank/branch code characters

Parameters:
  • message – Human-readable error message.

  • bic – The invalid BIC value.

  • field – Optional field name (e.g., “debtor_agent”).

  • reason – Optional specific reason for failure.

Example

>>> try:
...     validate_bic("INVALID123")
... except InvalidBICError as e:
...     # BIC validation failed - prompt user to correct
...     print(f"Invalid BIC: {e}")
...     print(f"Field: {e.field}, Value: {e.bic}")
exception pain001.exceptions.InvalidIBANError(message: str, iban: str, field: str | None = None, reason: str | None = None)[source]#

Bases: PaymentValidationError

Raised when IBAN validation fails.

This exception indicates issues with: - Invalid IBAN format (wrong structure) - Failed ISO 7064 mod-97-10 checksum validation - Unsupported country code - IBAN length mismatch for country

Parameters:
  • message – Human-readable error message.

  • iban – The invalid IBAN value.

  • field – Optional field name (e.g., “debtor_account”).

  • reason – Optional specific reason for failure.

Example

>>> try:
...     validate_iban("AT68123456")  # Too short
... except InvalidIBANError as e:
...     # IBAN validation failed - prompt user to correct
...     print(f"Invalid IBAN: {e}")
...     print(f"Field: {e.field}, Value: {e.iban}")
exception pain001.exceptions.MissingRequiredFieldError(message: str, field: str, row_number: int | None = None, required_fields: list[str] | None = None)[source]#

Bases: PaymentValidationError

Raised when a required field is missing from payment data.

This exception indicates issues with: - Missing mandatory fields (debtor_name, creditor_account, etc.) - Empty/null values for required fields - Missing fields in CSV rows - Missing dictionary keys

Parameters:
  • message – Human-readable error message.

  • field – The missing field name.

  • row_number – Optional row/line number where field is missing.

  • required_fields – Optional list of all required fields.

Example

>>> try:
...     validate_required_fields(data, ["debtor_name", "amount"])
... except MissingRequiredFieldError as e:
...     # Required field missing - show user what's needed
...     print(f"Missing field: {e.field}")
...     print(f"Row: {e.row_number}, Expected fields: {e.required_fields}")
exception pain001.exceptions.Pain001Error[source]#

Bases: Exception

Base exception for all Pain001 errors.

All custom exceptions in this library inherit from this base class, allowing consumers to catch any Pain001-specific error with a single except clause if needed.

Example

>>> try:
...     process_files(...)
... except Pain001Error:
...     # Catch any Pain001-specific error
...     log.error("Pain001 operation failed")
exception pain001.exceptions.PaymentValidationError(message: str, field: str | None = None)[source]#

Bases: Pain001Error

Raised when payment data validation fails.

This exception indicates issues with input data such as: - Invalid IBAN format - Invalid BIC/SWIFT code - Invalid amount (negative, too large, wrong format) - Missing required fields (debtor name, creditor account, etc.) - Invalid date formats

Parameters:
  • message – Human-readable error message.

  • field – Optional field name that caused the validation error.

Example

>>> try:
...     validate_payment_data(data)
... except PaymentValidationError as e:
...     # User-facing error - show validation message
...     return {"error": str(e), "field": e.field}
exception pain001.exceptions.SchemaValidationError(message: str, errors: list[str] | None = None)[source]#

Bases: Pain001Error

Raised when XSD schema validation fails.

This exception indicates issues with: - Generated XML does not conform to ISO 20022 schema - Missing required XML elements - Invalid XML element values - Namespace mismatches

Parameters:
  • message – Human-readable error message.

  • errors – Optional list of detailed validation errors.

Example

>>> try:
...     validate_xml_against_schema(xml, xsd)
... except SchemaValidationError as e:
...     # Schema validation error - check data mapping
...     log.error(f"XML schema validation failed: {e}")
...     log.debug(f"Validation errors: {e.errors}")
exception pain001.exceptions.XMLGenerationError[source]#

Bases: Pain001Error

Raised when XML generation or validation fails.

This exception indicates issues with: - Jinja2 template rendering failures - XSD schema validation errors - XML namespace issues - Missing or corrupted template files - Invalid XML structure

Example

>>> try:
...     generate_xml(data, template, schema)
... except XMLGenerationError as e:
...     # System error - check templates and schemas
...     log.error(f"XML generation failed: {e}")
...     alert_ops_team()
pain001.exceptions.XSDValidationError#

alias of SchemaValidationError

pain001.migrate module#

CLI for ISO 20022 version migration.

pain001.suite module#

What the pain001 suite is, and how its versions move.

Users install more than one package — pain001, pain001-mcp, pain001-lsp, a loader or two — and the failure they hit is a version they cannot reason about: is pain001-loader-xlsx==0.0.54 supposed to work with pain001==0.0.60?

The suite answers that with a single rule: every member ships the same version as the core. One number describes the whole suite. If the core is at 0.0.60 then so is every wrapper and every loader, and a user reading two different numbers is reading a mistake rather than a deliberate difference they need to understand.

Versions advance in 0.0.1 steps and stay on the 0.0.x line; 0.1.0 follows 0.0.999, not 0.0.60. A member that jumps off that line is not signalling independence, it is breaking the one property this module exists to guarantee.

An earlier revision of this module split the suite in two, versioning the loaders independently on the grounds that they implement the published plugin contract rather than the core’s internals, so a loader fix need not wait for a core release. That is a defensible design and it is not the one this project uses: the cost of a user having to know which packages track the core and which do not outweighs the cost of an occasional no-change release. The contract generation in PAIN001_API_VERSION still exists and the registry still enforces it at load time — it is a safety net, not the versioning policy.

Three rules follow, and all are checked by scripts/check_suite_consistency.py:

  1. Every member’s published version must equal the core’s.

  2. Every member’s declared pain001 floor must be a version that actually exists, or the combination is uninstallable.

  3. Every member’s declared pain001 floor follows the policy recorded here (SuiteMember.floor). Every member is LOCKSTEP: each release requires the core at its own number, so pain001-mcp 0.0.74 requires pain001>=0.0.74 and so does each loader. The wrappers (pain001-mcp, pain001-lsp) call the core’s own API; the loaders were held at the oldest core whose plugin contract they needed (>=0.0.55 for MT101, >=0.0.56 for xlsx) until 0.0.74, when the family moved them to lockstep too, as the camt053 family’s loaders already are: one suite version is then one tested combination, and a floor below it promises a pairing nobody ran. Before any rule the floors drifted silently (>=0.0.55 on one loader, >=0.0.56 on the other, >=0.0.70 on the wrappers) with nothing saying which was intended. An explicit version is still accepted here for a member that genuinely needs one; it is a decision made in this table, never a side effect of a release.

pain001.suite.CORE: Final[str] = 'pain001'#

The core distribution every other member depends on.

pain001.suite.LOCKSTEP: Final[str] = 'lockstep'#

Floor policy for a member whose declared pain001 floor must equal its own version: it uses the core’s API, not only the plugin contract.

pain001.suite.SUITE: Final[mappingproxy[str, SuiteMember]] = mappingproxy({'pain001': SuiteMember(distribution='pain001', repository='sebastienrousseau/pain001', summary='Core library and CLI.', floor=None), 'pain001-mcp': SuiteMember(distribution='pain001-mcp', repository='sebastienrousseau/pain001-mcp', summary='Model Context Protocol server.', floor='lockstep'), 'pain001-lsp': SuiteMember(distribution='pain001-lsp', repository='sebastienrousseau/pain001-lsp', summary='Language server for payment data files.', floor='lockstep'), 'pain001-loader-xlsx': SuiteMember(distribution='pain001-loader-xlsx', repository='sebastienrousseau/pain001-loader-xlsx', summary='Excel (.xlsx/.xlsm) loader plugin.', floor='lockstep'), 'pain001-loader-mt101': SuiteMember(distribution='pain001-loader-mt101', repository='sebastienrousseau/pain001-loader-mt101', summary='SWIFT MT101 loader plugin.', floor='lockstep')})#

Every published member of the suite, keyed by distribution name.

Read-only: this is reference data, and a caller reshaping it would change what a consistency check believes about the world.

class pain001.suite.SuiteMember(distribution: str, repository: str, summary: str, floor: str | None = 'lockstep')[source]#

Bases: NamedTuple

One published package in the suite.

Variables:
  • distribution (str) – The name on PyPI.

  • repository (str) – The GitHub repository, owner/name.

  • summary (str) – One line, for the README table and error messages.

  • floor (str | None) – The pain001 floor this member must declare: LOCKSTEP (equal to its own version), an explicit version (the oldest core whose plugin contract it needs), or None for the core itself.

distribution: str#

Alias for field number 0

floor: str | None#

Alias for field number 3

repository: str#

Alias for field number 1

summary: str#

Alias for field number 2

pain001.suite.members() → tuple[SuiteMember, ...][source]#

Return every member of the suite, core first.

Every member is lockstep, so there is no second accessor to pick a subset — the absence of one is the policy.

Returns:

All suite members, core first.

Example

>>> [m.distribution for m in members()][0]
'pain001'