cbpr-usage-rules 0.4.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/CHANGELOG.md +21 -0
- {cbpr_usage_rules-0.4.0/src/cbpr_usage_rules.egg-info → cbpr_usage_rules-0.6.0}/PKG-INFO +44 -2
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/README.md +43 -1
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/__init__.py +19 -1
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/cli.py +49 -1
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/engine.py +51 -2
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.052.max.xml +79 -79
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.052.min.xml +1 -1
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.054.max.xml +78 -78
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.054.min.xml +1 -1
- {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2025}/pacs.002.max.xml +17 -17
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.002.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.004.max.xml +49 -49
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.004.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008.max.xml +41 -41
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008_stp.max.xml +41 -41
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008_stp.min.xml +6 -6
- {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2025}/pacs.009.max.xml +37 -37
- {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2025}/pacs.009.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_adv.max.xml +41 -41
- {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2025}/pacs.009_adv.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_cov.max.xml +53 -53
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_cov.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pain.001.max.xml +28 -28
- {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2025}/pain.001.min.xml +2 -2
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.052.max.xml +79 -79
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.052.min.xml +1 -1
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.054.max.xml +78 -78
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.054.min.xml +1 -1
- {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2026}/pacs.002.max.xml +17 -17
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.002.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.004.max.xml +45 -45
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.004.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008.max.xml +41 -41
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008_stp.max.xml +41 -41
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008_stp.min.xml +6 -6
- {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2026}/pacs.009.max.xml +37 -37
- {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2026}/pacs.009.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_adv.max.xml +41 -41
- {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2026}/pacs.009_adv.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_cov.max.xml +53 -53
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_cov.min.xml +3 -3
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pain.001.max.xml +27 -27
- {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2026}/pain.001.min.xml +2 -2
- cbpr_usage_rules-0.6.0/src/cbpr_rules/idgen.py +243 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/message.py +81 -10
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/registry.py +3 -9
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0/src/cbpr_usage_rules.egg-info}/PKG-INFO +44 -2
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/SOURCES.txt +3 -1
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/tests/test_examples.py +24 -8
- cbpr_usage_rules-0.6.0/tests/test_idgen.py +79 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/LICENSE +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/MANIFEST.in +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/pyproject.toml +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/setup.cfg +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/helpers.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/loader.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/models.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/py.typed +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/reference/__init__.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/reference/countries.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/reference/currencies.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/__init__.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/__init__.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/camt_052.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/camt_054.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_002.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_004.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_008.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_008_stp.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_009.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_009_adv.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_009_cov.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pain_001.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/__init__.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/camt_052.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/camt_054.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_002.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_004.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_008.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_008_stp.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_009.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_009_adv.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_009_cov.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pain_001.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/schema.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/__init__.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/bic.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/country.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/currency.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/iban.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/lei.py +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/dependency_links.txt +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/entry_points.txt +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/requires.txt +0 -0
- {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,27 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
|
|
5
5
|
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.6.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Rule listings now show the **affected XPaths** for each rule, so agents can see
|
|
11
|
+
exactly which field(s) a rule constrains (concrete element paths and `@attribute`
|
|
12
|
+
targets, or a `//Name` wildcard). Shown by default in `cbpr-validate --list` /
|
|
13
|
+
`--enforced` (text + JSON) and via `list_rules(..., with_xpaths=True)`. Paths are
|
|
14
|
+
derived by running each rule against the bundled example messages.
|
|
15
|
+
|
|
16
|
+
## [0.5.0]
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Deterministic **ID generation** library (`cbpr_rules.idgen`, "Counting Strings"
|
|
20
|
+
algorithm — no `random`/`uuid`): `generate_iban`, `generate_lei`, `generate_bic`,
|
|
21
|
+
`generate_uuid`/`generate_uetr`, `generate_mid`, plus `counting_string`/
|
|
22
|
+
`counting_sequence` and seeded helpers. Every id is structurally valid and passes
|
|
23
|
+
the package's own validators; same seed → same output. New CLI
|
|
24
|
+
`cbpr-validate --generate {iban,lei,bic,uuid,uetr,mid,text}` (with
|
|
25
|
+
`--seed/--country/--bank/--branch/--count`). The bundled example messages now
|
|
26
|
+
source their identifiers from this library (seeded per example).
|
|
27
|
+
|
|
7
28
|
## [0.4.0]
|
|
8
29
|
|
|
9
30
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cbpr-usage-rules
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Validate ISO 20022 CBPR+ XML messages against SWIFT usage (business) rules.
|
|
5
5
|
Author: Pete Houghton
|
|
6
6
|
License: MIT License
|
|
@@ -172,11 +172,34 @@ xml = cbpr_rules.example_message(2025, "pacs.008", "max") # variant defaults t
|
|
|
172
172
|
cbpr_rules.example_variants(2025, "pacs.008") # ['min', 'max']
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
The data is **fictitious** (synthetic BICs/IBANs/LEIs
|
|
175
|
+
The data is **fictitious** (synthetic, generated BICs/IBANs/LEIs) and does
|
|
176
176
|
not reproduce any real sample — use them as starting templates, not real payments.
|
|
177
177
|
|
|
178
178
|
---
|
|
179
179
|
|
|
180
|
+
## Generating identifiers
|
|
181
|
+
|
|
182
|
+
The example values above are produced by a small **deterministic** ID generator
|
|
183
|
+
(the "Counting Strings" algorithm — no `random`/`uuid`, same seed → same output).
|
|
184
|
+
Every generated id is structurally valid and passes the tool's own validators.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
cbpr-validate --generate iban --country AT --seed s1 # ISO 13616 + mod-97
|
|
188
|
+
cbpr-validate --generate lei --seed acme # ISO 17442 + check digits
|
|
189
|
+
cbpr-validate --generate bic --bank JSBP --country GB # -> JSBPGB..XXX
|
|
190
|
+
cbpr-validate --generate uuid --count 3 # RFC 4122 v4 (UETR)
|
|
191
|
+
cbpr-validate --generate mid --country GB # message identifier
|
|
192
|
+
```
|
|
193
|
+
```python
|
|
194
|
+
from cbpr_rules import generate_iban, generate_lei, generate_bic, generate_uetr
|
|
195
|
+
generate_iban("DE", seed="acme") # deterministic, valid German IBAN
|
|
196
|
+
generate_bic(country="GB", seed="acme")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
IBAN generation supports the 27 EU countries plus GB.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
180
203
|
## The result object
|
|
181
204
|
|
|
182
205
|
Both `validate_file` and `validate_string` return a dictionary:
|
|
@@ -259,6 +282,7 @@ matching is by local name, so namespace prefixes never matter.
|
|
|
259
282
|
cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
|
|
260
283
|
cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
|
|
261
284
|
cbpr_rules.list_rules(2025, "pacs.008", enforced_only=True) # only the enforceable rules
|
|
285
|
+
cbpr_rules.list_rules(2025, "pacs.008", with_xpaths=True) # + "xpaths" each rule affects
|
|
262
286
|
```
|
|
263
287
|
|
|
264
288
|
```bash
|
|
@@ -267,6 +291,24 @@ cbpr-validate --year 2025 --type pacs.008 --list # all rules
|
|
|
267
291
|
cbpr-validate --year 2025 --type pacs.008 --enforced # enforceable rules only
|
|
268
292
|
```
|
|
269
293
|
|
|
294
|
+
The `--list` / `--enforced` output (and `with_xpaths=True`) annotates each rule with
|
|
295
|
+
the **concrete fields it affects**, so you can see exactly which element/attribute a
|
|
296
|
+
rule constrains — handy when several fields share a name (e.g. many `Ctry` fields, but
|
|
297
|
+
a rule targets only one):
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
pacs.008:VAL-IBAN - CBPR_Valid_IBAN
|
|
301
|
+
//IBAN
|
|
302
|
+
pacs.008:VAL-BIC - CBPR_Valid_Agent_BIC
|
|
303
|
+
/Document/FIToFICstmrCdtTrf/CdtTrfTxInf/InstgAgt/FinInstnId/BICFI
|
|
304
|
+
pacs.008:R41 - CBPR_Interbank_Settlement_Currency_FormalRule
|
|
305
|
+
/Document/FIToFICstmrCdtTrf/CdtTrfTxInf/IntrBkSttlmAmt/@Ccy
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Paths are derived by running each rule against the bundled example messages
|
|
309
|
+
(best-effort): concrete element paths and `@attribute` targets where present, or a
|
|
310
|
+
`//Name` wildcard for a datatype rule whose element isn't in the examples.
|
|
311
|
+
|
|
270
312
|
### Algorithmic field validation
|
|
271
313
|
|
|
272
314
|
Beyond the published rules, the following formats are validated with their
|
|
@@ -117,11 +117,34 @@ xml = cbpr_rules.example_message(2025, "pacs.008", "max") # variant defaults t
|
|
|
117
117
|
cbpr_rules.example_variants(2025, "pacs.008") # ['min', 'max']
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
The data is **fictitious** (synthetic BICs/IBANs/LEIs
|
|
120
|
+
The data is **fictitious** (synthetic, generated BICs/IBANs/LEIs) and does
|
|
121
121
|
not reproduce any real sample — use them as starting templates, not real payments.
|
|
122
122
|
|
|
123
123
|
---
|
|
124
124
|
|
|
125
|
+
## Generating identifiers
|
|
126
|
+
|
|
127
|
+
The example values above are produced by a small **deterministic** ID generator
|
|
128
|
+
(the "Counting Strings" algorithm — no `random`/`uuid`, same seed → same output).
|
|
129
|
+
Every generated id is structurally valid and passes the tool's own validators.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
cbpr-validate --generate iban --country AT --seed s1 # ISO 13616 + mod-97
|
|
133
|
+
cbpr-validate --generate lei --seed acme # ISO 17442 + check digits
|
|
134
|
+
cbpr-validate --generate bic --bank JSBP --country GB # -> JSBPGB..XXX
|
|
135
|
+
cbpr-validate --generate uuid --count 3 # RFC 4122 v4 (UETR)
|
|
136
|
+
cbpr-validate --generate mid --country GB # message identifier
|
|
137
|
+
```
|
|
138
|
+
```python
|
|
139
|
+
from cbpr_rules import generate_iban, generate_lei, generate_bic, generate_uetr
|
|
140
|
+
generate_iban("DE", seed="acme") # deterministic, valid German IBAN
|
|
141
|
+
generate_bic(country="GB", seed="acme")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
IBAN generation supports the 27 EU countries plus GB.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
125
148
|
## The result object
|
|
126
149
|
|
|
127
150
|
Both `validate_file` and `validate_string` return a dictionary:
|
|
@@ -204,6 +227,7 @@ matching is by local name, so namespace prefixes never matter.
|
|
|
204
227
|
cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
|
|
205
228
|
cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
|
|
206
229
|
cbpr_rules.list_rules(2025, "pacs.008", enforced_only=True) # only the enforceable rules
|
|
230
|
+
cbpr_rules.list_rules(2025, "pacs.008", with_xpaths=True) # + "xpaths" each rule affects
|
|
207
231
|
```
|
|
208
232
|
|
|
209
233
|
```bash
|
|
@@ -212,6 +236,24 @@ cbpr-validate --year 2025 --type pacs.008 --list # all rules
|
|
|
212
236
|
cbpr-validate --year 2025 --type pacs.008 --enforced # enforceable rules only
|
|
213
237
|
```
|
|
214
238
|
|
|
239
|
+
The `--list` / `--enforced` output (and `with_xpaths=True`) annotates each rule with
|
|
240
|
+
the **concrete fields it affects**, so you can see exactly which element/attribute a
|
|
241
|
+
rule constrains — handy when several fields share a name (e.g. many `Ctry` fields, but
|
|
242
|
+
a rule targets only one):
|
|
243
|
+
|
|
244
|
+
```text
|
|
245
|
+
pacs.008:VAL-IBAN - CBPR_Valid_IBAN
|
|
246
|
+
//IBAN
|
|
247
|
+
pacs.008:VAL-BIC - CBPR_Valid_Agent_BIC
|
|
248
|
+
/Document/FIToFICstmrCdtTrf/CdtTrfTxInf/InstgAgt/FinInstnId/BICFI
|
|
249
|
+
pacs.008:R41 - CBPR_Interbank_Settlement_Currency_FormalRule
|
|
250
|
+
/Document/FIToFICstmrCdtTrf/CdtTrfTxInf/IntrBkSttlmAmt/@Ccy
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Paths are derived by running each rule against the bundled example messages
|
|
254
|
+
(best-effort): concrete element paths and `@attribute` targets where present, or a
|
|
255
|
+
`//Name` wildcard for a datatype rule whose element isn't in the examples.
|
|
256
|
+
|
|
215
257
|
### Algorithmic field validation
|
|
216
258
|
|
|
217
259
|
Beyond the published rules, the following formats are validated with their
|
|
@@ -4,7 +4,7 @@ This package is AI generated. See the README for details and caveats.
|
|
|
4
4
|
"""
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.6.0"
|
|
8
8
|
|
|
9
9
|
from .models import Rule, Severity, Violation
|
|
10
10
|
from .engine import (
|
|
@@ -15,6 +15,16 @@ from .engine import (
|
|
|
15
15
|
example_message,
|
|
16
16
|
example_variants,
|
|
17
17
|
)
|
|
18
|
+
from .idgen import (
|
|
19
|
+
generate_iban,
|
|
20
|
+
generate_lei,
|
|
21
|
+
generate_bic,
|
|
22
|
+
generate_uuid,
|
|
23
|
+
generate_uetr,
|
|
24
|
+
generate_mid,
|
|
25
|
+
counting_string,
|
|
26
|
+
counting_sequence,
|
|
27
|
+
)
|
|
18
28
|
|
|
19
29
|
__all__ = [
|
|
20
30
|
"__version__",
|
|
@@ -27,4 +37,12 @@ __all__ = [
|
|
|
27
37
|
"available",
|
|
28
38
|
"example_message",
|
|
29
39
|
"example_variants",
|
|
40
|
+
"generate_iban",
|
|
41
|
+
"generate_lei",
|
|
42
|
+
"generate_bic",
|
|
43
|
+
"generate_uuid",
|
|
44
|
+
"generate_uetr",
|
|
45
|
+
"generate_mid",
|
|
46
|
+
"counting_string",
|
|
47
|
+
"counting_sequence",
|
|
30
48
|
]
|
|
@@ -9,6 +9,7 @@ import textwrap
|
|
|
9
9
|
from typing import List, Optional
|
|
10
10
|
|
|
11
11
|
from . import __version__
|
|
12
|
+
from . import idgen
|
|
12
13
|
from .engine import (
|
|
13
14
|
ValidationError,
|
|
14
15
|
available,
|
|
@@ -19,6 +20,29 @@ from .engine import (
|
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def _generate(args) -> List[str]:
|
|
24
|
+
"""Produce the ``--generate`` output lines (one id per --count)."""
|
|
25
|
+
kind = args.generate
|
|
26
|
+
out: List[str] = []
|
|
27
|
+
for i in range(max(args.count, 1)):
|
|
28
|
+
seed = f"{args.seed}:{i}" if args.count > 1 else args.seed
|
|
29
|
+
if kind == "iban":
|
|
30
|
+
if not args.country:
|
|
31
|
+
raise ValueError("--generate iban requires --country (e.g. --country AT)")
|
|
32
|
+
out.append(idgen.generate_iban(args.country, seed))
|
|
33
|
+
elif kind == "lei":
|
|
34
|
+
out.append(idgen.generate_lei(seed))
|
|
35
|
+
elif kind == "bic":
|
|
36
|
+
out.append(idgen.generate_bic(args.bank, args.country or "GB", args.branch, seed))
|
|
37
|
+
elif kind in ("uuid", "uetr"):
|
|
38
|
+
out.append(idgen.generate_uuid(idgen.deterministic_int(seed or str(i), 0, 2**63)))
|
|
39
|
+
elif kind == "mid":
|
|
40
|
+
out.append(idgen.generate_mid("CBPR", args.country or "GB", i + 1))
|
|
41
|
+
elif kind == "text":
|
|
42
|
+
out.append(idgen.deterministic_string(seed or "text", 16))
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
|
|
22
46
|
def _build_parser() -> argparse.ArgumentParser:
|
|
23
47
|
p = argparse.ArgumentParser(
|
|
24
48
|
prog="cbpr-validate",
|
|
@@ -72,6 +96,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
72
96
|
help="Root element tag wrapping the AppHdr+Document in --example output "
|
|
73
97
|
"(default: Envelope).",
|
|
74
98
|
)
|
|
99
|
+
p.add_argument(
|
|
100
|
+
"--generate",
|
|
101
|
+
choices=("iban", "lei", "bic", "uuid", "uetr", "mid", "text"),
|
|
102
|
+
help="Generate a deterministic, structurally-valid identifier and print it "
|
|
103
|
+
"(use --seed/--country/--bank/--branch/--count to control output).",
|
|
104
|
+
)
|
|
105
|
+
p.add_argument("--seed", default="", help="Seed for --generate (same seed -> same output).")
|
|
106
|
+
p.add_argument("--country", help="Country code for --generate iban/bic/mid (e.g. AT, GB).")
|
|
107
|
+
p.add_argument("--bank", help="Bank code for --generate bic (4 letters, padded with X).")
|
|
108
|
+
p.add_argument("--branch", default="XXX", help="Branch code for --generate bic (default XXX).")
|
|
109
|
+
p.add_argument("--count", type=int, default=1, help="How many ids to generate (default 1).")
|
|
75
110
|
p.add_argument(
|
|
76
111
|
"--list",
|
|
77
112
|
action="store_true",
|
|
@@ -211,6 +246,15 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
211
246
|
print(json.dumps(types) if args.json else "\n".join(types))
|
|
212
247
|
return 0
|
|
213
248
|
|
|
249
|
+
if args.generate:
|
|
250
|
+
try:
|
|
251
|
+
for line in _generate(args):
|
|
252
|
+
print(line)
|
|
253
|
+
except (ValueError, ValidationError) as exc:
|
|
254
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
255
|
+
return 2
|
|
256
|
+
return 0
|
|
257
|
+
|
|
214
258
|
if args.example:
|
|
215
259
|
if args.year is None or args.msgtype is None:
|
|
216
260
|
print("error: --example requires --year and --type", file=sys.stderr)
|
|
@@ -226,13 +270,17 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
226
270
|
if args.year is None or args.msgtype is None:
|
|
227
271
|
print("error: --year and --type are required for --list", file=sys.stderr)
|
|
228
272
|
return 2
|
|
229
|
-
rules = list_rules(
|
|
273
|
+
rules = list_rules(
|
|
274
|
+
args.year, args.msgtype, enforced_only=args.enforced, with_xpaths=True
|
|
275
|
+
)
|
|
230
276
|
if args.json:
|
|
231
277
|
print(json.dumps(rules, indent=2))
|
|
232
278
|
else:
|
|
233
279
|
for r in rules:
|
|
234
280
|
flag = "" if r["enforced"] else " (advisory)"
|
|
235
281
|
print(f"{r['rule_number']} - {r['name']}{flag}")
|
|
282
|
+
for xp in r.get("xpaths", []):
|
|
283
|
+
print(f" {xp}")
|
|
236
284
|
return 0
|
|
237
285
|
|
|
238
286
|
if args.year is None:
|
|
@@ -95,15 +95,64 @@ def validate_string(xml: str, year: int, msgtype: Optional[str] = None, xsd=None
|
|
|
95
95
|
return _validate_tree(tree, year, msgtype, xsd)
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def
|
|
98
|
+
def _drop_redundant_bare_paths(paths: set) -> List[str]:
|
|
99
|
+
"""Tidy captured paths: drop a bare element path when an ``@attribute`` variant
|
|
100
|
+
exists, and a ``//Name`` wildcard when concrete paths for that name exist."""
|
|
101
|
+
attr_bases = {p.rsplit("/@", 1)[0] for p in paths if "/@" in p}
|
|
102
|
+
wild_names = {p[2:] for p in paths if p.startswith("//")}
|
|
103
|
+
concrete = {p.rsplit("/", 1)[-1].lstrip("@") for p in paths if not p.startswith("//")}
|
|
104
|
+
kept = set()
|
|
105
|
+
for p in paths:
|
|
106
|
+
if p.startswith("//") and p[2:] in concrete:
|
|
107
|
+
continue
|
|
108
|
+
if "/@" not in p and p in attr_bases:
|
|
109
|
+
continue
|
|
110
|
+
kept.add(p)
|
|
111
|
+
return sorted(kept)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def rule_xpaths(year: int, msgtype: str) -> dict:
|
|
115
|
+
"""Map each rule_number to the concrete xpaths it touches.
|
|
116
|
+
|
|
117
|
+
Derived by running each rule against the bundled min+max example messages
|
|
118
|
+
and recording the fields its queries read (best-effort; a field absent from
|
|
119
|
+
both examples and read only inside a non-firing branch may be missed).
|
|
120
|
+
"""
|
|
121
|
+
rules = load_rules(year, msgtype)
|
|
122
|
+
captured = {r.rule_number: set() for r in rules}
|
|
123
|
+
for variant in ("min", "max"):
|
|
124
|
+
try:
|
|
125
|
+
xml = example_message(year, msgtype, variant)
|
|
126
|
+
tree = loader.parse_string(xml)
|
|
127
|
+
except Exception:
|
|
128
|
+
continue
|
|
129
|
+
bah, doc = loader.locate(tree)
|
|
130
|
+
msg = ParsedMessage(tree, bah, doc, message_type=msgtype, year=year)
|
|
131
|
+
for r in rules:
|
|
132
|
+
if not r.enforced:
|
|
133
|
+
continue
|
|
134
|
+
with msg.record() as paths:
|
|
135
|
+
r.run(msg)
|
|
136
|
+
captured[r.rule_number] |= paths
|
|
137
|
+
return {num: _drop_redundant_bare_paths(p) for num, p in captured.items()}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def list_rules(
|
|
141
|
+
year: int, msgtype: str, enforced_only: bool = False, with_xpaths: bool = False
|
|
142
|
+
) -> List[dict]:
|
|
99
143
|
"""Return metadata for the rules registered for a (year, message type).
|
|
100
144
|
|
|
101
145
|
With ``enforced_only=True`` only the enforceable (mechanically-checked) rules
|
|
102
|
-
are returned
|
|
146
|
+
are returned. With ``with_xpaths=True`` each rule dict gains an ``"xpaths"``
|
|
147
|
+
list of the concrete fields it affects (see ``rule_xpaths``).
|
|
103
148
|
"""
|
|
104
149
|
rules = [r.to_dict() for r in load_rules(year, msgtype)]
|
|
105
150
|
if enforced_only:
|
|
106
151
|
rules = [r for r in rules if r["enforced"]]
|
|
152
|
+
if with_xpaths:
|
|
153
|
+
paths = rule_xpaths(year, msgtype)
|
|
154
|
+
for r in rules:
|
|
155
|
+
r["xpaths"] = paths.get(r["rule_number"], [])
|
|
107
156
|
return rules
|
|
108
157
|
|
|
109
158
|
|