cbpr-usage-rules 0.5.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.5.0 → cbpr_usage_rules-0.6.0}/CHANGELOG.md +9 -0
- {cbpr_usage_rules-0.5.0/src/cbpr_usage_rules.egg-info → cbpr_usage_rules-0.6.0}/PKG-INFO +20 -1
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/README.md +19 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/__init__.py +1 -1
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/cli.py +5 -1
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/engine.py +51 -2
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/message.py +81 -10
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/registry.py +3 -9
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0/src/cbpr_usage_rules.egg-info}/PKG-INFO +20 -1
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/LICENSE +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/MANIFEST.in +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/pyproject.toml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/setup.cfg +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.052.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.052.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.054.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.054.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.002.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.002.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.004.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.004.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008_stp.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008_stp.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_adv.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_adv.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_cov.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_cov.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pain.001.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pain.001.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.052.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.052.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.054.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.054.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.002.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.002.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.004.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.004.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008_stp.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008_stp.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_adv.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_adv.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_cov.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_cov.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pain.001.max.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pain.001.min.xml +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/helpers.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/idgen.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/loader.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/models.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/py.typed +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/reference/__init__.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/reference/countries.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/reference/currencies.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/__init__.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/__init__.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/camt_052.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/camt_054.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_002.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_004.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_008.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_008_stp.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_009.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_009_adv.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_009_cov.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pain_001.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/__init__.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/camt_052.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/camt_054.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_002.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_004.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_008.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_008_stp.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_009.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_009_adv.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_009_cov.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pain_001.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/schema.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/__init__.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/bic.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/country.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/currency.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/iban.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/lei.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/SOURCES.txt +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/dependency_links.txt +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/entry_points.txt +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/requires.txt +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/top_level.txt +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/tests/test_examples.py +0 -0
- {cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/tests/test_idgen.py +0 -0
|
@@ -4,6 +4,15 @@ 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
|
+
|
|
7
16
|
## [0.5.0]
|
|
8
17
|
|
|
9
18
|
### 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
|
|
@@ -282,6 +282,7 @@ matching is by local name, so namespace prefixes never matter.
|
|
|
282
282
|
cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
|
|
283
283
|
cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
|
|
284
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
|
|
285
286
|
```
|
|
286
287
|
|
|
287
288
|
```bash
|
|
@@ -290,6 +291,24 @@ cbpr-validate --year 2025 --type pacs.008 --list # all rules
|
|
|
290
291
|
cbpr-validate --year 2025 --type pacs.008 --enforced # enforceable rules only
|
|
291
292
|
```
|
|
292
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
|
+
|
|
293
312
|
### Algorithmic field validation
|
|
294
313
|
|
|
295
314
|
Beyond the published rules, the following formats are validated with their
|
|
@@ -227,6 +227,7 @@ matching is by local name, so namespace prefixes never matter.
|
|
|
227
227
|
cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
|
|
228
228
|
cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
|
|
229
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
|
|
230
231
|
```
|
|
231
232
|
|
|
232
233
|
```bash
|
|
@@ -235,6 +236,24 @@ cbpr-validate --year 2025 --type pacs.008 --list # all rules
|
|
|
235
236
|
cbpr-validate --year 2025 --type pacs.008 --enforced # enforceable rules only
|
|
236
237
|
```
|
|
237
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
|
+
|
|
238
257
|
### Algorithmic field validation
|
|
239
258
|
|
|
240
259
|
Beyond the published rules, the following formats are validated with their
|
|
@@ -270,13 +270,17 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
270
270
|
if args.year is None or args.msgtype is None:
|
|
271
271
|
print("error: --year and --type are required for --list", file=sys.stderr)
|
|
272
272
|
return 2
|
|
273
|
-
rules = list_rules(
|
|
273
|
+
rules = list_rules(
|
|
274
|
+
args.year, args.msgtype, enforced_only=args.enforced, with_xpaths=True
|
|
275
|
+
)
|
|
274
276
|
if args.json:
|
|
275
277
|
print(json.dumps(rules, indent=2))
|
|
276
278
|
else:
|
|
277
279
|
for r in rules:
|
|
278
280
|
flag = "" if r["enforced"] else " (advisory)"
|
|
279
281
|
print(f"{r['rule_number']} - {r['name']}{flag}")
|
|
282
|
+
for xp in r.get("xpaths", []):
|
|
283
|
+
print(f" {xp}")
|
|
280
284
|
return 0
|
|
281
285
|
|
|
282
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
|
|
|
@@ -16,10 +16,14 @@ pseudo-code convention.
|
|
|
16
16
|
"""
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import re
|
|
20
|
+
from contextlib import contextmanager
|
|
19
21
|
from typing import List, Optional
|
|
20
22
|
|
|
21
23
|
from .loader import local_name
|
|
22
24
|
|
|
25
|
+
_INDEX_RE = re.compile(r"\[\d+\]")
|
|
26
|
+
|
|
23
27
|
|
|
24
28
|
def _split(path: str) -> List[str]:
|
|
25
29
|
return [s for s in (seg.strip() for seg in path.strip().strip("/").split("/")) if s]
|
|
@@ -54,6 +58,53 @@ class ParsedMessage:
|
|
|
54
58
|
self.document = document
|
|
55
59
|
self.message_type = message_type
|
|
56
60
|
self.year = year
|
|
61
|
+
self._rec: Optional[set] = None # active path recorder, or None
|
|
62
|
+
|
|
63
|
+
# -- path recording (for "which fields does a rule touch?") ----------
|
|
64
|
+
@contextmanager
|
|
65
|
+
def record(self):
|
|
66
|
+
"""Within this block, collect the normalized xpaths the queries touch."""
|
|
67
|
+
prev = self._rec
|
|
68
|
+
self._rec = set()
|
|
69
|
+
try:
|
|
70
|
+
yield self._rec
|
|
71
|
+
finally:
|
|
72
|
+
self._rec = prev
|
|
73
|
+
|
|
74
|
+
def _normalize(self, xpath: str) -> str:
|
|
75
|
+
"""Strip positional ``[n]`` and re-root at /Document or /AppHdr."""
|
|
76
|
+
xpath = _INDEX_RE.sub("", xpath)
|
|
77
|
+
segs = xpath.strip("/").split("/")
|
|
78
|
+
for anchor in ("Document", "AppHdr"):
|
|
79
|
+
if anchor in segs:
|
|
80
|
+
return "/" + "/".join(segs[segs.index(anchor):])
|
|
81
|
+
return xpath
|
|
82
|
+
|
|
83
|
+
def _record_elements(self, elements, attr: Optional[str] = None) -> None:
|
|
84
|
+
if self._rec is None:
|
|
85
|
+
return
|
|
86
|
+
for el in elements:
|
|
87
|
+
if not isinstance(el.tag, str):
|
|
88
|
+
continue
|
|
89
|
+
path = self._normalize(self.xpath_of(el))
|
|
90
|
+
self._rec.add(path + "/@" + attr if attr else path)
|
|
91
|
+
|
|
92
|
+
def _record_intent(self, segs: List[str], context) -> None:
|
|
93
|
+
"""Record the intended anchored path of a query, even if it matched nothing."""
|
|
94
|
+
if self._rec is None or not segs:
|
|
95
|
+
return
|
|
96
|
+
head = segs[0]
|
|
97
|
+
if head == "AppHdr" or head.startswith("BusinessApplicationHeader"):
|
|
98
|
+
anchored = "/AppHdr/" + "/".join(segs[1:])
|
|
99
|
+
elif head == "Document":
|
|
100
|
+
anchored = "/Document/" + "/".join(segs[1:])
|
|
101
|
+
elif context is not None:
|
|
102
|
+
rest = segs[1:] if local_name(context) == head else segs
|
|
103
|
+
base = self._normalize(self.xpath_of(context))
|
|
104
|
+
anchored = base + ("/" + "/".join(rest) if rest else "")
|
|
105
|
+
else:
|
|
106
|
+
return
|
|
107
|
+
self._rec.add(anchored.rstrip("/"))
|
|
57
108
|
|
|
58
109
|
# -- element metadata ------------------------------------------------
|
|
59
110
|
def xpath_of(self, el) -> str:
|
|
@@ -119,15 +170,17 @@ class ParsedMessage:
|
|
|
119
170
|
return [context] if context is not None else []
|
|
120
171
|
head = segs[0]
|
|
121
172
|
if head == "AppHdr" or head.startswith("BusinessApplicationHeader"):
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# relative to context
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
173
|
+
result = _descend([self.bah], segs[1:]) if self.bah is not None else []
|
|
174
|
+
elif head == "Document":
|
|
175
|
+
result = _descend([self.document], segs[1:]) if self.document is not None else []
|
|
176
|
+
elif context is not None: # relative to context
|
|
177
|
+
rest = segs[1:] if local_name(context) == head else segs
|
|
178
|
+
result = _descend([context], rest)
|
|
179
|
+
else:
|
|
180
|
+
result = []
|
|
181
|
+
self._record_elements(result)
|
|
182
|
+
self._record_intent(segs, context)
|
|
183
|
+
return result
|
|
131
184
|
|
|
132
185
|
def each(self, path: str, context=None) -> List["object"]:
|
|
133
186
|
"""Iteration helper - same as find(), named for "for each [path]" rules."""
|
|
@@ -152,7 +205,9 @@ class ParsedMessage:
|
|
|
152
205
|
|
|
153
206
|
Used for XML attributes such as the currency on an amount (``@Ccy``).
|
|
154
207
|
"""
|
|
155
|
-
|
|
208
|
+
nodes = self.find(path, context)
|
|
209
|
+
self._record_elements(nodes, attr=attr)
|
|
210
|
+
return [(el, el.get(attr)) for el in nodes]
|
|
156
211
|
|
|
157
212
|
def iter_local(self, name: str, roots=None):
|
|
158
213
|
"""Yield every descendant (at any depth) with local tag ``name``.
|
|
@@ -160,6 +215,8 @@ class ParsedMessage:
|
|
|
160
215
|
Scans the BAH and Document by default - used by cross-cutting checks
|
|
161
216
|
that apply wherever an element occurs (e.g. every PostalAddress).
|
|
162
217
|
"""
|
|
218
|
+
if self._rec is not None:
|
|
219
|
+
self._rec.add(f"//{name}") # wildcard intent (dropped if concrete paths found)
|
|
163
220
|
if roots is None:
|
|
164
221
|
roots = [r for r in (self.bah, self.document) if r is not None]
|
|
165
222
|
for root in roots:
|
|
@@ -167,4 +224,18 @@ class ParsedMessage:
|
|
|
167
224
|
continue
|
|
168
225
|
for el in root.iter():
|
|
169
226
|
if local_name(el) == name:
|
|
227
|
+
self._record_elements([el])
|
|
170
228
|
yield el
|
|
229
|
+
|
|
230
|
+
def iter_attr(self, attr: str):
|
|
231
|
+
"""Yield (element, value) for every element carrying ``attr`` anywhere.
|
|
232
|
+
|
|
233
|
+
Cross-cutting equivalent of ``attr_nodes`` for attributes like ``@Ccy``.
|
|
234
|
+
"""
|
|
235
|
+
for root in (self.bah, self.document):
|
|
236
|
+
if root is None:
|
|
237
|
+
continue
|
|
238
|
+
for el in root.iter():
|
|
239
|
+
if isinstance(el.tag, str) and el.get(attr) is not None:
|
|
240
|
+
self._record_elements([el], attr=attr)
|
|
241
|
+
yield el, el.get(attr)
|
|
@@ -171,15 +171,9 @@ def _currency_rule(msgtype: str) -> Rule:
|
|
|
171
171
|
)
|
|
172
172
|
|
|
173
173
|
# Every ``Ccy`` attribute, anywhere in the message.
|
|
174
|
-
for
|
|
175
|
-
if
|
|
176
|
-
|
|
177
|
-
for el in root.iter():
|
|
178
|
-
if not isinstance(el.tag, str):
|
|
179
|
-
continue
|
|
180
|
-
code = el.get("Ccy")
|
|
181
|
-
if code and not is_valid_currency(code):
|
|
182
|
-
flag(el, code)
|
|
174
|
+
for el, code in msg.iter_attr("Ccy"):
|
|
175
|
+
if code and not is_valid_currency(code):
|
|
176
|
+
flag(el, code)
|
|
183
177
|
# Standalone ``<Ccy>`` elements (e.g. account currency).
|
|
184
178
|
for el in msg.iter_local("Ccy"):
|
|
185
179
|
code = msg.text_of(el)
|
|
@@ -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
|
|
@@ -282,6 +282,7 @@ matching is by local name, so namespace prefixes never matter.
|
|
|
282
282
|
cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
|
|
283
283
|
cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
|
|
284
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
|
|
285
286
|
```
|
|
286
287
|
|
|
287
288
|
```bash
|
|
@@ -290,6 +291,24 @@ cbpr-validate --year 2025 --type pacs.008 --list # all rules
|
|
|
290
291
|
cbpr-validate --year 2025 --type pacs.008 --enforced # enforceable rules only
|
|
291
292
|
```
|
|
292
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
|
+
|
|
293
312
|
### Algorithmic field validation
|
|
294
313
|
|
|
295
314
|
Beyond the published rules, the following formats are validated with their
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.052.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.052.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.054.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.054.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.002.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.002.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.004.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.004.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008_stp.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008_stp.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_adv.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_adv.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_cov.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_cov.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pain.001.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pain.001.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.052.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.052.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.054.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.054.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.002.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.002.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.004.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.004.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008_stp.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008_stp.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_adv.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_adv.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_cov.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_cov.min.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pain.001.max.xml
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pain.001.min.xml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_008_stp.py
RENAMED
|
File without changes
|
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_009_adv.py
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_009_cov.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_008_stp.py
RENAMED
|
File without changes
|
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_009_adv.py
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_009_cov.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/requires.txt
RENAMED
|
File without changes
|
{cbpr_usage_rules-0.5.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|