Pain001 is a Python Library for Automating ISO 20022-Compliant Payment Files Using CSV Data.
Project description
Pain001
Generate ISO 20022-compliant payment files from CSV, SQLite, JSON, or Parquet data.
Contents
Getting started
- What is Pain001? — the problem it solves and how
- Install — PyPI, extras, and source builds
- Quick start — one command from CSV to validated XML
Library reference
- Supported messages — every bundled ISO 20022 message type
- Input formats — CSV, SQLite, JSON, JSONL, Parquet
- Usage — CLI, dry-run, streaming, REST API, Python API
- When not to use Pain001 — honest boundaries
Operational
- Development — gates, make targets, CI matrix
- Security — hardening posture and reporting
- Documentation — guides, API reference, examples
- Contributing — how to get changes in
- License — dual Apache-2.0 / MIT
What is Pain001?
Banks reject malformed payment files. Pain001 takes the payment data you already have — a CSV export, a SQLite table, a JSON feed, a Parquet file — and turns it into ISO 20022 XML that validates against the official XSD schema before it ever reaches your bank.
It handles the parts that are easy to get wrong:
| Concern | How Pain001 handles it |
|---|---|
| Schema compliance | Every file is validated against the official XSD before it is written |
| Monetary precision | Amounts flow through decimal.Decimal end to end — no float rounding |
| Control totals | NbOfTxs and CtrlSum are computed from the data, never trusted from input |
| Template drift | Bundled template/XSD pairs are guard-railed; mismatches fail loudly |
| XML attacks | All XML parsing goes through defusedxml — XXE and entity expansion are blocked |
| Large batches | Streaming mode chunks input and emits one file per chunk |
Templates and schemas for every supported message type ship inside the package — point Pain001 at your data and it resolves the rest.
Install
| Channel | Command | Notes |
|---|---|---|
| PyPI | pip install pain001 |
Core library and CLI |
| PyPI + REST API | pip install "pain001[api]" |
Adds FastAPI + Uvicorn server |
| PyPI + Parquet | pip install "pain001[parquet]" |
Adds PyArrow for Parquet input |
| Source | git clone https://github.com/sebastienrousseau/pain001 && cd pain001 && poetry install |
For development |
Requires Python 3.10 or later.
Quick start
pain001 -t pain.001.001.03 -m template.xml -s schema.xsd -d payments.csv
The generated XML is validated against the XSD schema and written
to the current directory (override with -o). Grab a template and
schema for any supported
version from the
bundled templates,
or point -m/-s at your own.
Validate without generating anything (CI pre-flight) — here the template and schema are auto-resolved from the bundled registry:
pain001 -t pain.001.001.03 -d payments.csv --dry-run
Exit codes: 0 success, 1 validation or processing error, 2 invalid
arguments.
Supported messages
| Message type | Description |
|---|---|
pain.001.001.03 – pain.001.001.12 |
Customer Credit Transfer Initiation, all ten ISO 20022 versions |
pain.008.001.02 |
Customer Direct Debit Initiation |
Each bundled message type ships with a Jinja2 template, the official XSD schema, and registry metadata. List them from the CLI:
pain001 --list-templates
pain001 --show-template pain.001.001.12
Related tooling included in the package:
- Version migration — map payment data between pain.001 versions
(
python -m pain001.migrate). - pain.002 parser — read payment status reports your bank sends back.
- camt.053 parser — read end-of-day bank statements.
Input formats
| Format | Extension | Notes |
|---|---|---|
| CSV | .csv |
Header row maps columns to template fields |
| SQLite | .db, .sqlite |
Reads from a named table you specify (set the table via --config) |
| JSON | .json |
Array of payment objects |
| JSON Lines | .jsonl |
One payment object per line |
| Parquet | .parquet |
Requires the parquet extra |
All loaders normalise into the same internal representation, so the rest of the pipeline — validation, totals, rendering — is identical regardless of source.
Usage
CLI reference
pain001 [OPTIONS]
-t, --xml-message-type ISO 20022 message type (e.g. pain.001.001.03)
-m, --template Jinja2 XML template (auto-resolved when omitted)
-s, --schema XSD schema for validation (auto-resolved when omitted)
-d, --data Payment data file (CSV, SQLite, JSON, JSONL, Parquet)
-c, --config Configuration file (YAML, TOML, or INI)
-o, --output-dir Output directory (default: current directory)
--dry-run Validate inputs without generating XML
--streaming Process input in chunks, one XML file per chunk
--chunk-size Rows per streaming chunk (default: 1000)
--profile Configuration profile or built-in preset
--show-config Print the resolved configuration and exit
--list-templates List bundled templates and exit
--show-template Show metadata for one bundled template and exit
--emit-metrics Emit timing and lifecycle metrics to stdout
-v, --verbose Detailed logging output
-h, --help Show help and exit
Dry-run validation in CI
--dry-run runs the full validation pipeline — file existence, schema
resolution, data loading, field checks — and stops before XML generation.
It is designed as a pre-flight gate:
pain001 -t pain.001.001.03 -d payments.csv --dry-run || exit 1
Exit code 0 means the data would generate a valid file; 1 means it
would not, with the failures printed.
Streaming large batches
For batches too large to hold in memory, streaming mode chunks the input
and writes one XML file per chunk, each with its own computed NbOfTxs
and CtrlSum:
pain001 -t pain.001.001.03 -d payments.csv --streaming --chunk-size 500
REST API
Install the api extra and start the server:
pip install "pain001[api]"
uvicorn pain001.api.app:app
| Method | Endpoint | Purpose |
|---|---|---|
GET |
/api/health |
Liveness check |
POST |
/api/validate |
Validate payment data without generating |
POST |
/api/generate |
Generate a payment file synchronously |
POST |
/api/generate/async |
Queue generation as a background job |
GET |
/api/status/{job_id} |
Poll an async job |
GET |
/api/download/{job_id} |
Download a finished file |
DELETE |
/api/jobs/{job_id} |
Cancel or clean up a job |
Interactive OpenAPI docs are served at /api/docs.
Python API — generate in memory (serverless)
For Lambdas, APIs, and queues, generate_xml_string returns the validated
XML as a string instead of writing to disk. This snippet is fully
self-contained — it uses the template, schema, and sample data that ship
inside the package, so it runs as-is with no external files:
from pain001 import generate_xml_string
from pain001.constants import TEMPLATES_DIR
from pain001.csv.load_csv_data import load_csv_data
message_type = "pain.001.001.03"
bundled = TEMPLATES_DIR / message_type # templates ship inside the package
# Load the bundled sample dataset; swap in your own list[dict] of rows
payments = load_csv_data(str(bundled / "template.csv"))
xml = generate_xml_string(
payments,
message_type,
str(bundled / "template.xml"),
str(bundled / f"{message_type}.xsd"),
)
# `xml` is validated ISO 20022 XML, ready to return from a handler
print(xml[:38]) # -> <?xml version="1.0" encoding="UTF-8"?>
Python API — generate to a file
process_files loads your data, renders the template, validates against
the XSD, and writes the file — returning the path it wrote:
from pain001.core.core import process_files
output_path = process_files(
xml_message_type="pain.001.001.03",
xml_template_file_path="template.xml",
xsd_schema_file_path="schema.xsd",
data_file_path="payments.csv", # path, or a list[dict] of payment rows
)
print(output_path) # e.g. "pain.001.001.03.xml" — validated and on disk
When not to use Pain001
- You need message types beyond pain.001 / pain.008 generation. The camt.053 and pain.002 modules are parsers, not generators; other ISO 20022 families (camt.052, pacs.*) are out of scope.
- You need bank connectivity. Pain001 produces and validates files; it does not transmit them. Pair it with your EBICS/SFTP/API channel.
- Your data model is wildly non-tabular. The loaders expect row-shaped payment records. Deeply nested custom structures need flattening first.
Development
git clone https://github.com/sebastienrousseau/pain001
cd pain001
poetry install --with dev
The quality model is zero-trust: every gate runs locally and in CI, and the build fails if any regress.
| Target | What it runs |
|---|---|
make lint |
Ruff lint + format check |
make type |
mypy in --strict mode |
make test |
Full pytest suite with branch-coverage gate |
make sec |
Bandit + Safety dependency audit |
make perf |
pytest-benchmark performance suite |
make mutate |
Mutation testing via mutmut |
make check |
lint + coverage + security in one pass |
make tollgates |
Dependency, XSD, idempotency, and env-parity gates |
CI workflows:
| Workflow | Purpose |
|---|---|
ci.yml |
Test matrix on Python 3.10 / 3.11 / 3.12 |
quality.yml |
Lint, types, complexity |
security.yml |
Bandit, Safety, dependency review |
codeql.yml |
Static analysis |
nightly.yml |
Extended nightly suite |
pr.yml |
Pull-request gate |
docs.yml |
Build and deploy documentation |
Current state: 848 tests passing, ~92% branch coverage (90% floor),
mypy --strict clean. Live coverage is tracked by the Codecov badge above.
Security
Pain001 treats payment data as hostile until proven otherwise:
- XML parsing is routed through
defusedxml; XXE, billion-laughs, and external entity resolution are rejected. - Path handling goes through a path validator that blocks traversal outside permitted directories.
- Schema validation is mandatory — output that does not validate against the official XSD is never written as a success.
- Amounts are
Decimalthroughout; control sums are recomputed, not echoed from input. - Dependencies are pinned via
poetry.lockand audited by Safety, Bandit, and CodeQL in CI.
To report a vulnerability, please use GitHub private vulnerability reporting rather than a public issue.
Documentation
- Guides & API reference: docs.pain001.com
- Runnable examples:
examples/— self-checking scripts executed in CI - Bundled templates & schemas:
pain001/templates/ - Release history: CHANGELOG.md
Contributing
Contributions are welcome — see the contributing instructions. Unless you explicitly state otherwise, any contribution you submit is dual-licensed as below, without additional terms or conditions.
Thanks to all the contributors who have helped build Pain001.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option. See CHANGELOG.md for release history.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pain001-0.0.50.tar.gz.
File metadata
- Download URL: pain001-0.0.50.tar.gz
- Upload date:
- Size: 165.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72e1fbc01a514ba44b04fe712184d37390abc01d56946911d4b03dc7fe811223
|
|
| MD5 |
869a4eb743a74140a1411ed7753a96c5
|
|
| BLAKE2b-256 |
22f53287602e97011fe004a2766640ec4d52d628551eb8c930bc3dcf9db3b640
|
Provenance
The following attestation bundles were made for pain001-0.0.50.tar.gz:
Publisher:
ci.yml on sebastienrousseau/pain001
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pain001-0.0.50.tar.gz -
Subject digest:
72e1fbc01a514ba44b04fe712184d37390abc01d56946911d4b03dc7fe811223 - Sigstore transparency entry: 1835564531
- Sigstore integration time:
-
Permalink:
sebastienrousseau/pain001@e95074c32cc0d76b9fa0693b29548fc2b505e545 -
Branch / Tag:
refs/tags/v0.0.50 - Owner: https://github.com/sebastienrousseau
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@e95074c32cc0d76b9fa0693b29548fc2b505e545 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pain001-0.0.50-py3-none-any.whl.
File metadata
- Download URL: pain001-0.0.50-py3-none-any.whl
- Upload date:
- Size: 254.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bbcddb62b3719ade2f75d1bb3b06e2b693aefe19df01d367deae6172b8cae62
|
|
| MD5 |
ec17213bb23433099661c84e17754a44
|
|
| BLAKE2b-256 |
78e760697510992c7b3cf28beb603a190228c0c2e6549e40bef14155442f09bf
|
Provenance
The following attestation bundles were made for pain001-0.0.50-py3-none-any.whl:
Publisher:
ci.yml on sebastienrousseau/pain001
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pain001-0.0.50-py3-none-any.whl -
Subject digest:
7bbcddb62b3719ade2f75d1bb3b06e2b693aefe19df01d367deae6172b8cae62 - Sigstore transparency entry: 1835564650
- Sigstore integration time:
-
Permalink:
sebastienrousseau/pain001@e95074c32cc0d76b9fa0693b29548fc2b505e545 -
Branch / Tag:
refs/tags/v0.0.50 - Owner: https://github.com/sebastienrousseau
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@e95074c32cc0d76b9fa0693b29548fc2b505e545 -
Trigger Event:
push
-
Statement type: