cbpr-usage-rules 0.2.0__tar.gz → 0.4.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.2.0 → cbpr_usage_rules-0.4.0}/CHANGELOG.md +17 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/MANIFEST.in +1 -0
- {cbpr_usage_rules-0.2.0/src/cbpr_usage_rules.egg-info → cbpr_usage_rules-0.4.0}/PKG-INFO +33 -2
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/README.md +32 -1
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/pyproject.toml +1 -1
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/__init__.py +11 -2
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/cli.py +41 -3
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/engine.py +58 -3
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/camt.052.max.xml +3430 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/camt.052.min.xml +35 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/camt.054.max.xml +3334 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/camt.054.min.xml +40 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.002.max.xml +420 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.002.min.xml +41 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.004.max.xml +1731 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.004.min.xml +60 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.008.max.xml +1318 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.008.min.xml +58 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.008_stp.max.xml +1092 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.008_stp.min.xml +67 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009.max.xml +954 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009.min.xml +51 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009_adv.max.xml +1036 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009_adv.min.xml +51 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009_cov.max.xml +1619 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009_cov.min.xml +65 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pain.001.max.xml +1215 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pain.001.min.xml +50 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/camt.052.max.xml +3430 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/camt.052.min.xml +31 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/camt.054.max.xml +3334 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/camt.054.min.xml +40 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.002.max.xml +420 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.002.min.xml +41 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.004.max.xml +1491 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.004.min.xml +62 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.008.max.xml +1295 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.008.min.xml +59 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.008_stp.max.xml +1072 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.008_stp.min.xml +68 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009.max.xml +954 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009.min.xml +51 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009_adv.max.xml +1036 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009_adv.min.xml +51 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009_cov.max.xml +1616 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009_cov.min.xml +66 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pain.001.max.xml +1149 -0
- cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pain.001.min.xml +50 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0/src/cbpr_usage_rules.egg-info}/PKG-INFO +33 -2
- cbpr_usage_rules-0.4.0/src/cbpr_usage_rules.egg-info/SOURCES.txt +94 -0
- cbpr_usage_rules-0.4.0/tests/test_examples.py +90 -0
- cbpr_usage_rules-0.2.0/src/cbpr_usage_rules.egg-info/SOURCES.txt +0 -53
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/LICENSE +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/setup.cfg +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/helpers.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/loader.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/message.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/models.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/py.typed +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/reference/__init__.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/reference/countries.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/reference/currencies.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/registry.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/__init__.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/__init__.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/camt_052.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/camt_054.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_002.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_004.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_008.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_008_stp.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_009.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_009_adv.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_009_cov.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pain_001.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/__init__.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/camt_052.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/camt_054.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_002.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_004.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_008.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_008_stp.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_009.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_009_adv.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_009_cov.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pain_001.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/schema.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/__init__.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/bic.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/country.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/currency.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/iban.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/lei.py +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_usage_rules.egg-info/dependency_links.txt +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_usage_rules.egg-info/entry_points.txt +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_usage_rules.egg-info/requires.txt +0 -0
- {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_usage_rules.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,23 @@ 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.4.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- List only the **enforceable** rules for a year + message type:
|
|
11
|
+
`list_rules(year, msgtype, enforced_only=True)` in the API and
|
|
12
|
+
`cbpr-validate --enforced --year … --type …` on the CLI (advisory rules omitted;
|
|
13
|
+
composes with `--json`).
|
|
14
|
+
|
|
15
|
+
## [0.3.0]
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Bundled **example messages**: a minimum and a maximum variant for every
|
|
19
|
+
supported message type, each pre-verified to pass the usage rules and the
|
|
20
|
+
CBPR+ XSDs. New API `example_message(year, msgtype, variant="max",
|
|
21
|
+
wrapper="Envelope")` and `example_variants(year, msgtype)`, and CLI
|
|
22
|
+
`--example {min,max}` (with `--wrapper TAG`). Data is fictitious/anonymised.
|
|
23
|
+
|
|
7
24
|
## [0.2.0]
|
|
8
25
|
|
|
9
26
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cbpr-usage-rules
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -148,6 +148,35 @@ Advisory rules are summarised as a count by default; pass `--advisory` to list t
|
|
|
148
148
|
|
|
149
149
|
---
|
|
150
150
|
|
|
151
|
+
## Example messages
|
|
152
|
+
|
|
153
|
+
For every supported message type the package bundles a **minimum** and a
|
|
154
|
+
**maximum** example message — both pre-verified to pass the usage rules *and* the
|
|
155
|
+
CBPR+ XSDs:
|
|
156
|
+
|
|
157
|
+
- **min** — only the mandatory fields (smallest valid message);
|
|
158
|
+
- **max** — every XSD-visible field populated (one representative occurrence; the
|
|
159
|
+
richest single legal combination, since some fields are mutually exclusive).
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Print to stdout (full AppHdr + Document, wrapped in <Envelope>)
|
|
163
|
+
cbpr-validate --example max --year 2025 --type pacs.008
|
|
164
|
+
cbpr-validate --example min --year 2026 --type pacs.009_cov > template.xml
|
|
165
|
+
|
|
166
|
+
# Override the wrapper tag (default: Envelope)
|
|
167
|
+
cbpr-validate --example max --year 2025 --type pacs.008 --wrapper BusinessMessage
|
|
168
|
+
```
|
|
169
|
+
```python
|
|
170
|
+
import cbpr_rules
|
|
171
|
+
xml = cbpr_rules.example_message(2025, "pacs.008", "max") # variant defaults to "max"
|
|
172
|
+
cbpr_rules.example_variants(2025, "pacs.008") # ['min', 'max']
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The data is **fictitious** (synthetic BICs/IBANs/LEIs, `EXAMPLE` names) and does
|
|
176
|
+
not reproduce any real sample — use them as starting templates, not real payments.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
151
180
|
## The result object
|
|
152
181
|
|
|
153
182
|
Both `validate_file` and `validate_string` return a dictionary:
|
|
@@ -229,11 +258,13 @@ matching is by local name, so namespace prefixes never matter.
|
|
|
229
258
|
```python
|
|
230
259
|
cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
|
|
231
260
|
cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
|
|
261
|
+
cbpr_rules.list_rules(2025, "pacs.008", enforced_only=True) # only the enforceable rules
|
|
232
262
|
```
|
|
233
263
|
|
|
234
264
|
```bash
|
|
235
265
|
cbpr-validate --year 2025 --list-types
|
|
236
|
-
cbpr-validate --year 2025 --type pacs.008 --list
|
|
266
|
+
cbpr-validate --year 2025 --type pacs.008 --list # all rules
|
|
267
|
+
cbpr-validate --year 2025 --type pacs.008 --enforced # enforceable rules only
|
|
237
268
|
```
|
|
238
269
|
|
|
239
270
|
### Algorithmic field validation
|
|
@@ -93,6 +93,35 @@ Advisory rules are summarised as a count by default; pass `--advisory` to list t
|
|
|
93
93
|
|
|
94
94
|
---
|
|
95
95
|
|
|
96
|
+
## Example messages
|
|
97
|
+
|
|
98
|
+
For every supported message type the package bundles a **minimum** and a
|
|
99
|
+
**maximum** example message — both pre-verified to pass the usage rules *and* the
|
|
100
|
+
CBPR+ XSDs:
|
|
101
|
+
|
|
102
|
+
- **min** — only the mandatory fields (smallest valid message);
|
|
103
|
+
- **max** — every XSD-visible field populated (one representative occurrence; the
|
|
104
|
+
richest single legal combination, since some fields are mutually exclusive).
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Print to stdout (full AppHdr + Document, wrapped in <Envelope>)
|
|
108
|
+
cbpr-validate --example max --year 2025 --type pacs.008
|
|
109
|
+
cbpr-validate --example min --year 2026 --type pacs.009_cov > template.xml
|
|
110
|
+
|
|
111
|
+
# Override the wrapper tag (default: Envelope)
|
|
112
|
+
cbpr-validate --example max --year 2025 --type pacs.008 --wrapper BusinessMessage
|
|
113
|
+
```
|
|
114
|
+
```python
|
|
115
|
+
import cbpr_rules
|
|
116
|
+
xml = cbpr_rules.example_message(2025, "pacs.008", "max") # variant defaults to "max"
|
|
117
|
+
cbpr_rules.example_variants(2025, "pacs.008") # ['min', 'max']
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The data is **fictitious** (synthetic BICs/IBANs/LEIs, `EXAMPLE` names) and does
|
|
121
|
+
not reproduce any real sample — use them as starting templates, not real payments.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
96
125
|
## The result object
|
|
97
126
|
|
|
98
127
|
Both `validate_file` and `validate_string` return a dictionary:
|
|
@@ -174,11 +203,13 @@ matching is by local name, so namespace prefixes never matter.
|
|
|
174
203
|
```python
|
|
175
204
|
cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
|
|
176
205
|
cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
|
|
206
|
+
cbpr_rules.list_rules(2025, "pacs.008", enforced_only=True) # only the enforceable rules
|
|
177
207
|
```
|
|
178
208
|
|
|
179
209
|
```bash
|
|
180
210
|
cbpr-validate --year 2025 --list-types
|
|
181
|
-
cbpr-validate --year 2025 --type pacs.008 --list
|
|
211
|
+
cbpr-validate --year 2025 --type pacs.008 --list # all rules
|
|
212
|
+
cbpr-validate --year 2025 --type pacs.008 --enforced # enforceable rules only
|
|
182
213
|
```
|
|
183
214
|
|
|
184
215
|
### Algorithmic field validation
|
|
@@ -4,10 +4,17 @@ 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.4.0"
|
|
8
8
|
|
|
9
9
|
from .models import Rule, Severity, Violation
|
|
10
|
-
from .engine import
|
|
10
|
+
from .engine import (
|
|
11
|
+
validate_file,
|
|
12
|
+
validate_string,
|
|
13
|
+
list_rules,
|
|
14
|
+
available,
|
|
15
|
+
example_message,
|
|
16
|
+
example_variants,
|
|
17
|
+
)
|
|
11
18
|
|
|
12
19
|
__all__ = [
|
|
13
20
|
"__version__",
|
|
@@ -18,4 +25,6 @@ __all__ = [
|
|
|
18
25
|
"validate_string",
|
|
19
26
|
"list_rules",
|
|
20
27
|
"available",
|
|
28
|
+
"example_message",
|
|
29
|
+
"example_variants",
|
|
21
30
|
]
|
|
@@ -9,7 +9,14 @@ import textwrap
|
|
|
9
9
|
from typing import List, Optional
|
|
10
10
|
|
|
11
11
|
from . import __version__
|
|
12
|
-
from .engine import
|
|
12
|
+
from .engine import (
|
|
13
|
+
ValidationError,
|
|
14
|
+
available,
|
|
15
|
+
example_message,
|
|
16
|
+
list_rules,
|
|
17
|
+
validate_file,
|
|
18
|
+
validate_string,
|
|
19
|
+
)
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
def _build_parser() -> argparse.ArgumentParser:
|
|
@@ -51,11 +58,31 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
51
58
|
action="store_true",
|
|
52
59
|
help="Also list the advisory (non-enforced) rules in full.",
|
|
53
60
|
)
|
|
61
|
+
p.add_argument(
|
|
62
|
+
"--example",
|
|
63
|
+
choices=("min", "max"),
|
|
64
|
+
help="Print a bundled example message for --year/--type instead of validating: "
|
|
65
|
+
"min (mandatory fields only) or max (every field populated). Both are XSD- and "
|
|
66
|
+
"usage-rule-valid.",
|
|
67
|
+
)
|
|
68
|
+
p.add_argument(
|
|
69
|
+
"--wrapper",
|
|
70
|
+
default="Envelope",
|
|
71
|
+
metavar="TAG",
|
|
72
|
+
help="Root element tag wrapping the AppHdr+Document in --example output "
|
|
73
|
+
"(default: Envelope).",
|
|
74
|
+
)
|
|
54
75
|
p.add_argument(
|
|
55
76
|
"--list",
|
|
56
77
|
action="store_true",
|
|
57
78
|
help="List the rules for --year/--type instead of validating.",
|
|
58
79
|
)
|
|
80
|
+
p.add_argument(
|
|
81
|
+
"--enforced",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="List only the enforceable (mechanically-checked) rules for --year/--type; "
|
|
84
|
+
"advisory rules are omitted. Implies --list.",
|
|
85
|
+
)
|
|
59
86
|
p.add_argument(
|
|
60
87
|
"--list-types", action="store_true", help="List message types available for --year."
|
|
61
88
|
)
|
|
@@ -184,11 +211,22 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
184
211
|
print(json.dumps(types) if args.json else "\n".join(types))
|
|
185
212
|
return 0
|
|
186
213
|
|
|
187
|
-
if args.
|
|
214
|
+
if args.example:
|
|
215
|
+
if args.year is None or args.msgtype is None:
|
|
216
|
+
print("error: --example requires --year and --type", file=sys.stderr)
|
|
217
|
+
return 2
|
|
218
|
+
try:
|
|
219
|
+
print(example_message(args.year, args.msgtype, args.example, wrapper=args.wrapper))
|
|
220
|
+
except ValidationError as exc:
|
|
221
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
222
|
+
return 2
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
if args.list or args.enforced:
|
|
188
226
|
if args.year is None or args.msgtype is None:
|
|
189
227
|
print("error: --year and --type are required for --list", file=sys.stderr)
|
|
190
228
|
return 2
|
|
191
|
-
rules = list_rules(args.year, args.msgtype)
|
|
229
|
+
rules = list_rules(args.year, args.msgtype, enforced_only=args.enforced)
|
|
192
230
|
if args.json:
|
|
193
231
|
print(json.dumps(rules, indent=2))
|
|
194
232
|
else:
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
"""Validation engine and public API."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
from importlib import resources
|
|
4
5
|
from typing import List, Optional
|
|
5
6
|
|
|
7
|
+
from lxml import etree
|
|
8
|
+
|
|
6
9
|
from . import loader
|
|
7
10
|
from . import schema as _schema
|
|
8
11
|
from .message import ParsedMessage
|
|
9
12
|
from .models import Rule, Severity
|
|
10
13
|
from .registry import available_message_types, load_rules
|
|
11
14
|
|
|
15
|
+
_VARIANTS = ("min", "max")
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
class ValidationError(Exception):
|
|
14
19
|
"""Raised when a message cannot be validated (parse error, unknown type)."""
|
|
@@ -90,11 +95,61 @@ def validate_string(xml: str, year: int, msgtype: Optional[str] = None, xsd=None
|
|
|
90
95
|
return _validate_tree(tree, year, msgtype, xsd)
|
|
91
96
|
|
|
92
97
|
|
|
93
|
-
def list_rules(year: int, msgtype: str) -> List[dict]:
|
|
94
|
-
"""Return metadata for
|
|
95
|
-
|
|
98
|
+
def list_rules(year: int, msgtype: str, enforced_only: bool = False) -> List[dict]:
|
|
99
|
+
"""Return metadata for the rules registered for a (year, message type).
|
|
100
|
+
|
|
101
|
+
With ``enforced_only=True`` only the enforceable (mechanically-checked) rules
|
|
102
|
+
are returned, omitting advisory/catalogue-only ones.
|
|
103
|
+
"""
|
|
104
|
+
rules = [r.to_dict() for r in load_rules(year, msgtype)]
|
|
105
|
+
if enforced_only:
|
|
106
|
+
rules = [r for r in rules if r["enforced"]]
|
|
107
|
+
return rules
|
|
96
108
|
|
|
97
109
|
|
|
98
110
|
def available(year: int) -> List[str]:
|
|
99
111
|
"""Message types with rules available for a given year."""
|
|
100
112
|
return available_message_types(year)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _example_resource(year: int, msgtype: str, variant: str):
|
|
116
|
+
return (
|
|
117
|
+
resources.files("cbpr_rules")
|
|
118
|
+
.joinpath("examples", f"y{int(year)}", f"{msgtype}.{variant}.xml")
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def example_variants(year: int, msgtype: str) -> List[str]:
|
|
123
|
+
"""Which example variants ('min'/'max') are bundled for a (year, message type)."""
|
|
124
|
+
return [v for v in _VARIANTS if _example_resource(year, msgtype, v).is_file()]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def example_message(
|
|
128
|
+
year: int, msgtype: str, variant: str = "max", wrapper: str = "Envelope"
|
|
129
|
+
) -> str:
|
|
130
|
+
"""Return a bundled, pre-verified example message for a (year, message type).
|
|
131
|
+
|
|
132
|
+
``variant`` is 'min' (mandatory fields only) or 'max' (every field populated).
|
|
133
|
+
Both are guaranteed to pass the usage rules (and the CBPR+ XSDs). ``wrapper``
|
|
134
|
+
overrides the single root element tag (default ``Envelope``) that holds the
|
|
135
|
+
``AppHdr`` and ``Document``. Raises ``ValidationError`` if no such example.
|
|
136
|
+
"""
|
|
137
|
+
if variant not in _VARIANTS:
|
|
138
|
+
raise ValidationError(
|
|
139
|
+
f"Unknown variant '{variant}'; choose one of {', '.join(_VARIANTS)}."
|
|
140
|
+
)
|
|
141
|
+
res = _example_resource(year, msgtype, variant)
|
|
142
|
+
if not res.is_file():
|
|
143
|
+
types = ", ".join(available(year)) or "(none)"
|
|
144
|
+
raise ValidationError(
|
|
145
|
+
f"No {variant} example for '{msgtype}' in {int(year)}. "
|
|
146
|
+
f"Available message types: {types}."
|
|
147
|
+
)
|
|
148
|
+
text = res.read_text(encoding="utf-8")
|
|
149
|
+
if wrapper and wrapper != "Envelope":
|
|
150
|
+
root = etree.fromstring(text.encode("utf-8"))
|
|
151
|
+
root.tag = wrapper
|
|
152
|
+
text = etree.tostring(
|
|
153
|
+
root, xml_declaration=True, encoding="UTF-8", pretty_print=True
|
|
154
|
+
).decode("utf-8")
|
|
155
|
+
return text
|