cbpr-usage-rules 0.1.3__tar.gz → 0.3.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.1.3 → cbpr_usage_rules-0.3.0}/CHANGELOG.md +18 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/MANIFEST.in +1 -0
- {cbpr_usage_rules-0.1.3/src/cbpr_usage_rules.egg-info → cbpr_usage_rules-0.3.0}/PKG-INFO +49 -3
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/README.md +48 -2
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/pyproject.toml +1 -1
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/__init__.py +11 -2
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/cli.py +87 -3
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/engine.py +48 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/camt.052.max.xml +3430 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/camt.052.min.xml +35 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/camt.054.max.xml +3334 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/camt.054.min.xml +40 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.002.max.xml +420 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.002.min.xml +41 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.004.max.xml +1731 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.004.min.xml +60 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.008.max.xml +1318 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.008.min.xml +58 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.008_stp.max.xml +1092 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.008_stp.min.xml +67 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009.max.xml +954 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009.min.xml +51 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009_adv.max.xml +1036 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009_adv.min.xml +51 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009_cov.max.xml +1619 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009_cov.min.xml +65 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pain.001.max.xml +1215 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pain.001.min.xml +50 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/camt.052.max.xml +3430 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/camt.052.min.xml +31 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/camt.054.max.xml +3334 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/camt.054.min.xml +40 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.002.max.xml +420 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.002.min.xml +41 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.004.max.xml +1491 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.004.min.xml +62 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.008.max.xml +1295 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.008.min.xml +59 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.008_stp.max.xml +1072 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.008_stp.min.xml +68 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009.max.xml +954 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009.min.xml +51 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009_adv.max.xml +1036 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009_adv.min.xml +51 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009_cov.max.xml +1616 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009_cov.min.xml +66 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pain.001.max.xml +1149 -0
- cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pain.001.min.xml +50 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0/src/cbpr_usage_rules.egg-info}/PKG-INFO +49 -3
- cbpr_usage_rules-0.3.0/src/cbpr_usage_rules.egg-info/SOURCES.txt +94 -0
- cbpr_usage_rules-0.3.0/tests/test_examples.py +90 -0
- cbpr_usage_rules-0.1.3/src/cbpr_usage_rules.egg-info/SOURCES.txt +0 -53
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/LICENSE +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/setup.cfg +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/helpers.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/loader.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/message.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/models.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/py.typed +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/reference/__init__.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/reference/countries.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/reference/currencies.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/registry.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/__init__.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/__init__.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/camt_052.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/camt_054.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_002.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_004.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_008.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_008_stp.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_009.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_009_adv.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_009_cov.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pain_001.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/__init__.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/camt_052.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/camt_054.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_002.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_004.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_008.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_008_stp.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_009.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_009_adv.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_009_cov.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pain_001.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/schema.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/__init__.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/bic.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/country.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/currency.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/iban.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/lei.py +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_usage_rules.egg-info/dependency_links.txt +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_usage_rules.egg-info/entry_points.txt +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_usage_rules.egg-info/requires.txt +0 -0
- {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_usage_rules.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,24 @@ 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.3.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Bundled **example messages**: a minimum and a maximum variant for every
|
|
11
|
+
supported message type, each pre-verified to pass the usage rules and the
|
|
12
|
+
CBPR+ XSDs. New API `example_message(year, msgtype, variant="max",
|
|
13
|
+
wrapper="Envelope")` and `example_variants(year, msgtype)`, and CLI
|
|
14
|
+
`--example {min,max}` (with `--wrapper TAG`). Data is fictitious/anonymised.
|
|
15
|
+
|
|
16
|
+
## [0.2.0]
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `--format {text,compact,json}` CLI option. The new `compact` format prints one
|
|
20
|
+
finding per line, compiler/linter-style (`file:line: severity [rule] message | at
|
|
21
|
+
xpath`) with a stable `VALID:`/`INVALID:` summary line — easy for tools and coding
|
|
22
|
+
agents to parse. `--json` is now an alias for `--format json`; the default rich
|
|
23
|
+
text output is unchanged.
|
|
24
|
+
|
|
7
25
|
## [0.1.3]
|
|
8
26
|
|
|
9
27
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cbpr-usage-rules
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -112,8 +112,11 @@ result = cbpr_rules.validate_string(xml_text, year=2026)
|
|
|
112
112
|
# Human-readable (default). Exit code is non-zero if there are violations.
|
|
113
113
|
cbpr-validate payment.xml --year 2025
|
|
114
114
|
|
|
115
|
-
#
|
|
116
|
-
cbpr-validate payment.xml --year 2025 --
|
|
115
|
+
# Compact, one finding per line (compiler-style) — for tools and coding agents
|
|
116
|
+
cbpr-validate payment.xml --year 2025 --format compact
|
|
117
|
+
|
|
118
|
+
# JSON output (full machine-readable result)
|
|
119
|
+
cbpr-validate payment.xml --year 2025 --json # same as --format json
|
|
117
120
|
|
|
118
121
|
# Read from stdin
|
|
119
122
|
cat payment.xml | cbpr-validate --year 2026
|
|
@@ -127,10 +130,53 @@ cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
|
|
|
127
130
|
|
|
128
131
|
Each reported violation shows the rule, **why** this instance failed (`Problem:`),
|
|
129
132
|
the **offending XML** from your file (`Found:`), and its line + xpath (`At:`).
|
|
133
|
+
|
|
134
|
+
**For tools and coding agents**, prefer `--format compact` (one finding per line,
|
|
135
|
+
parseable like a compiler/linter) or `--json` (the full structured result):
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
$ cbpr-validate payment.xml --year 2025 --format compact
|
|
139
|
+
payment.xml:7: violation [pacs.008:R2] From vs Instructing Agent: ABCDGB2LXXX != GGGGGB2LXXX | at /AppHdr/Fr/FIId/FinInstnId/BICFI
|
|
140
|
+
payment.xml:63: violation [pacs.008:VAL-IBAN] invalid IBAN: 'GB29NWBK60161331926818' | at /Document/.../DbtrAcct/Id/IBAN
|
|
141
|
+
INVALID: 5 violations (87 rules)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Each line is `file:line: severity [rule] message | at xpath`, and the final
|
|
145
|
+
`VALID:` / `INVALID:` line is a stable summary to grep for. With `--xsd`, schema
|
|
146
|
+
errors appear as `schema-error` lines and are counted in the summary.
|
|
130
147
|
Advisory rules are summarised as a count by default; pass `--advisory` to list them.
|
|
131
148
|
|
|
132
149
|
---
|
|
133
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
|
+
|
|
134
180
|
## The result object
|
|
135
181
|
|
|
136
182
|
Both `validate_file` and `validate_string` return a dictionary:
|
|
@@ -57,8 +57,11 @@ result = cbpr_rules.validate_string(xml_text, year=2026)
|
|
|
57
57
|
# Human-readable (default). Exit code is non-zero if there are violations.
|
|
58
58
|
cbpr-validate payment.xml --year 2025
|
|
59
59
|
|
|
60
|
-
#
|
|
61
|
-
cbpr-validate payment.xml --year 2025 --
|
|
60
|
+
# Compact, one finding per line (compiler-style) — for tools and coding agents
|
|
61
|
+
cbpr-validate payment.xml --year 2025 --format compact
|
|
62
|
+
|
|
63
|
+
# JSON output (full machine-readable result)
|
|
64
|
+
cbpr-validate payment.xml --year 2025 --json # same as --format json
|
|
62
65
|
|
|
63
66
|
# Read from stdin
|
|
64
67
|
cat payment.xml | cbpr-validate --year 2026
|
|
@@ -72,10 +75,53 @@ cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
|
|
|
72
75
|
|
|
73
76
|
Each reported violation shows the rule, **why** this instance failed (`Problem:`),
|
|
74
77
|
the **offending XML** from your file (`Found:`), and its line + xpath (`At:`).
|
|
78
|
+
|
|
79
|
+
**For tools and coding agents**, prefer `--format compact` (one finding per line,
|
|
80
|
+
parseable like a compiler/linter) or `--json` (the full structured result):
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
$ cbpr-validate payment.xml --year 2025 --format compact
|
|
84
|
+
payment.xml:7: violation [pacs.008:R2] From vs Instructing Agent: ABCDGB2LXXX != GGGGGB2LXXX | at /AppHdr/Fr/FIId/FinInstnId/BICFI
|
|
85
|
+
payment.xml:63: violation [pacs.008:VAL-IBAN] invalid IBAN: 'GB29NWBK60161331926818' | at /Document/.../DbtrAcct/Id/IBAN
|
|
86
|
+
INVALID: 5 violations (87 rules)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Each line is `file:line: severity [rule] message | at xpath`, and the final
|
|
90
|
+
`VALID:` / `INVALID:` line is a stable summary to grep for. With `--xsd`, schema
|
|
91
|
+
errors appear as `schema-error` lines and are counted in the summary.
|
|
75
92
|
Advisory rules are summarised as a count by default; pass `--advisory` to list them.
|
|
76
93
|
|
|
77
94
|
---
|
|
78
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
|
+
|
|
79
125
|
## The result object
|
|
80
126
|
|
|
81
127
|
Both `validate_file` and `validate_string` return a dictionary:
|
|
@@ -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.3.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:
|
|
@@ -26,7 +33,18 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
26
33
|
help="Message type override, e.g. pacs.008 or pacs.008_stp "
|
|
27
34
|
"(auto-detected from the Document namespace if omitted).",
|
|
28
35
|
)
|
|
29
|
-
p.add_argument(
|
|
36
|
+
p.add_argument(
|
|
37
|
+
"--format",
|
|
38
|
+
choices=("text", "compact", "json"),
|
|
39
|
+
default="text",
|
|
40
|
+
help="Output format: text (rich, default), compact (one finding per line, "
|
|
41
|
+
"compiler-style — for tools/agents), or json (full machine-readable).",
|
|
42
|
+
)
|
|
43
|
+
p.add_argument(
|
|
44
|
+
"--json",
|
|
45
|
+
action="store_true",
|
|
46
|
+
help="Alias for --format json.",
|
|
47
|
+
)
|
|
30
48
|
p.add_argument(
|
|
31
49
|
"--xsd",
|
|
32
50
|
action="append",
|
|
@@ -40,6 +58,20 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
40
58
|
action="store_true",
|
|
41
59
|
help="Also list the advisory (non-enforced) rules in full.",
|
|
42
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
|
+
)
|
|
43
75
|
p.add_argument(
|
|
44
76
|
"--list",
|
|
45
77
|
action="store_true",
|
|
@@ -125,6 +157,43 @@ def _format_text(result: dict, show_advisory: bool = False) -> str:
|
|
|
125
157
|
return "\n".join(lines)
|
|
126
158
|
|
|
127
159
|
|
|
160
|
+
def _format_compact(result: dict, source: str) -> str:
|
|
161
|
+
"""One finding per line, compiler/linter-style, for tools and coding agents.
|
|
162
|
+
|
|
163
|
+
``<source>:<line>: <severity> [<rule>] <message> | at <xpath>`` per violation,
|
|
164
|
+
followed by a single greppable ``VALID:``/``INVALID:`` summary line. Advisory
|
|
165
|
+
(non-enforced) rules are omitted; use --json or text --advisory for those.
|
|
166
|
+
"""
|
|
167
|
+
lines: List[str] = []
|
|
168
|
+
for v in result["violations"]:
|
|
169
|
+
line = v["line"] if v["line"] is not None else "?"
|
|
170
|
+
message = " ".join((v.get("detail") or v["description"] or "").split())
|
|
171
|
+
suffix = f" | at {v['xpath']}" if v.get("xpath") else ""
|
|
172
|
+
lines.append(f"{source}:{line}: {v['severity']} [{v['rule_number']}] {message}{suffix}")
|
|
173
|
+
|
|
174
|
+
xsd = result.get("xsd")
|
|
175
|
+
schema_errors = 0
|
|
176
|
+
if xsd is not None:
|
|
177
|
+
for s in xsd["schemas"]:
|
|
178
|
+
if s.get("load_error"):
|
|
179
|
+
schema_errors += 1
|
|
180
|
+
lines.append(f"{s['file']}: schema-error could not load schema: {s['load_error']}")
|
|
181
|
+
for e in s["errors"]:
|
|
182
|
+
schema_errors += 1
|
|
183
|
+
eline = e["line"] if e.get("line") is not None else "?"
|
|
184
|
+
msg = " ".join((e.get("message") or "").split())
|
|
185
|
+
lines.append(f"{s['file']}:{eline}: schema-error {msg}")
|
|
186
|
+
|
|
187
|
+
n = len(result["violations"])
|
|
188
|
+
verdict = "VALID" if result["valid"] else "INVALID"
|
|
189
|
+
parts = [f"{n} violation{'s' if n != 1 else ''}"]
|
|
190
|
+
if xsd is not None:
|
|
191
|
+
parts.append(f"{schema_errors} schema error{'s' if schema_errors != 1 else ''}")
|
|
192
|
+
summary = f"{verdict}: {', '.join(parts)} ({result['rules_evaluated']} rules)"
|
|
193
|
+
lines.append(summary)
|
|
194
|
+
return "\n".join(lines)
|
|
195
|
+
|
|
196
|
+
|
|
128
197
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
129
198
|
args = _build_parser().parse_args(argv)
|
|
130
199
|
|
|
@@ -136,6 +205,17 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
136
205
|
print(json.dumps(types) if args.json else "\n".join(types))
|
|
137
206
|
return 0
|
|
138
207
|
|
|
208
|
+
if args.example:
|
|
209
|
+
if args.year is None or args.msgtype is None:
|
|
210
|
+
print("error: --example requires --year and --type", file=sys.stderr)
|
|
211
|
+
return 2
|
|
212
|
+
try:
|
|
213
|
+
print(example_message(args.year, args.msgtype, args.example, wrapper=args.wrapper))
|
|
214
|
+
except ValidationError as exc:
|
|
215
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
216
|
+
return 2
|
|
217
|
+
return 0
|
|
218
|
+
|
|
139
219
|
if args.list:
|
|
140
220
|
if args.year is None or args.msgtype is None:
|
|
141
221
|
print("error: --year and --type are required for --list", file=sys.stderr)
|
|
@@ -153,6 +233,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
153
233
|
print("error: --year is required to validate", file=sys.stderr)
|
|
154
234
|
return 2
|
|
155
235
|
|
|
236
|
+
source = args.file or "<stdin>"
|
|
156
237
|
try:
|
|
157
238
|
if args.file:
|
|
158
239
|
result = validate_file(args.file, args.year, args.msgtype, xsd=args.xsd)
|
|
@@ -163,8 +244,11 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
163
244
|
print(f"error: {exc}", file=sys.stderr)
|
|
164
245
|
return 2
|
|
165
246
|
|
|
166
|
-
if args.json
|
|
247
|
+
fmt = "json" if args.json else args.format
|
|
248
|
+
if fmt == "json":
|
|
167
249
|
print(json.dumps(result, indent=2))
|
|
250
|
+
elif fmt == "compact":
|
|
251
|
+
print(_format_compact(result, source))
|
|
168
252
|
else:
|
|
169
253
|
print(_format_text(result, show_advisory=args.advisory))
|
|
170
254
|
|
|
@@ -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)."""
|
|
@@ -98,3 +103,46 @@ def list_rules(year: int, msgtype: str) -> List[dict]:
|
|
|
98
103
|
def available(year: int) -> List[str]:
|
|
99
104
|
"""Message types with rules available for a given year."""
|
|
100
105
|
return available_message_types(year)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _example_resource(year: int, msgtype: str, variant: str):
|
|
109
|
+
return (
|
|
110
|
+
resources.files("cbpr_rules")
|
|
111
|
+
.joinpath("examples", f"y{int(year)}", f"{msgtype}.{variant}.xml")
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def example_variants(year: int, msgtype: str) -> List[str]:
|
|
116
|
+
"""Which example variants ('min'/'max') are bundled for a (year, message type)."""
|
|
117
|
+
return [v for v in _VARIANTS if _example_resource(year, msgtype, v).is_file()]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def example_message(
|
|
121
|
+
year: int, msgtype: str, variant: str = "max", wrapper: str = "Envelope"
|
|
122
|
+
) -> str:
|
|
123
|
+
"""Return a bundled, pre-verified example message for a (year, message type).
|
|
124
|
+
|
|
125
|
+
``variant`` is 'min' (mandatory fields only) or 'max' (every field populated).
|
|
126
|
+
Both are guaranteed to pass the usage rules (and the CBPR+ XSDs). ``wrapper``
|
|
127
|
+
overrides the single root element tag (default ``Envelope``) that holds the
|
|
128
|
+
``AppHdr`` and ``Document``. Raises ``ValidationError`` if no such example.
|
|
129
|
+
"""
|
|
130
|
+
if variant not in _VARIANTS:
|
|
131
|
+
raise ValidationError(
|
|
132
|
+
f"Unknown variant '{variant}'; choose one of {', '.join(_VARIANTS)}."
|
|
133
|
+
)
|
|
134
|
+
res = _example_resource(year, msgtype, variant)
|
|
135
|
+
if not res.is_file():
|
|
136
|
+
types = ", ".join(available(year)) or "(none)"
|
|
137
|
+
raise ValidationError(
|
|
138
|
+
f"No {variant} example for '{msgtype}' in {int(year)}. "
|
|
139
|
+
f"Available message types: {types}."
|
|
140
|
+
)
|
|
141
|
+
text = res.read_text(encoding="utf-8")
|
|
142
|
+
if wrapper and wrapper != "Envelope":
|
|
143
|
+
root = etree.fromstring(text.encode("utf-8"))
|
|
144
|
+
root.tag = wrapper
|
|
145
|
+
text = etree.tostring(
|
|
146
|
+
root, xml_declaration=True, encoding="UTF-8", pretty_print=True
|
|
147
|
+
).decode("utf-8")
|
|
148
|
+
return text
|