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.
Files changed (97) hide show
  1. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/CHANGELOG.md +17 -0
  2. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/MANIFEST.in +1 -0
  3. {cbpr_usage_rules-0.2.0/src/cbpr_usage_rules.egg-info → cbpr_usage_rules-0.4.0}/PKG-INFO +33 -2
  4. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/README.md +32 -1
  5. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/pyproject.toml +1 -1
  6. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/__init__.py +11 -2
  7. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/cli.py +41 -3
  8. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/engine.py +58 -3
  9. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/camt.052.max.xml +3430 -0
  10. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/camt.052.min.xml +35 -0
  11. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/camt.054.max.xml +3334 -0
  12. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/camt.054.min.xml +40 -0
  13. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.002.max.xml +420 -0
  14. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.002.min.xml +41 -0
  15. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.004.max.xml +1731 -0
  16. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.004.min.xml +60 -0
  17. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.008.max.xml +1318 -0
  18. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.008.min.xml +58 -0
  19. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.008_stp.max.xml +1092 -0
  20. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.008_stp.min.xml +67 -0
  21. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009.max.xml +954 -0
  22. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009.min.xml +51 -0
  23. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009_adv.max.xml +1036 -0
  24. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009_adv.min.xml +51 -0
  25. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009_cov.max.xml +1619 -0
  26. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pacs.009_cov.min.xml +65 -0
  27. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pain.001.max.xml +1215 -0
  28. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025/pain.001.min.xml +50 -0
  29. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/camt.052.max.xml +3430 -0
  30. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/camt.052.min.xml +31 -0
  31. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/camt.054.max.xml +3334 -0
  32. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/camt.054.min.xml +40 -0
  33. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.002.max.xml +420 -0
  34. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.002.min.xml +41 -0
  35. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.004.max.xml +1491 -0
  36. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.004.min.xml +62 -0
  37. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.008.max.xml +1295 -0
  38. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.008.min.xml +59 -0
  39. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.008_stp.max.xml +1072 -0
  40. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.008_stp.min.xml +68 -0
  41. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009.max.xml +954 -0
  42. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009.min.xml +51 -0
  43. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009_adv.max.xml +1036 -0
  44. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009_adv.min.xml +51 -0
  45. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009_cov.max.xml +1616 -0
  46. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pacs.009_cov.min.xml +66 -0
  47. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pain.001.max.xml +1149 -0
  48. cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026/pain.001.min.xml +50 -0
  49. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0/src/cbpr_usage_rules.egg-info}/PKG-INFO +33 -2
  50. cbpr_usage_rules-0.4.0/src/cbpr_usage_rules.egg-info/SOURCES.txt +94 -0
  51. cbpr_usage_rules-0.4.0/tests/test_examples.py +90 -0
  52. cbpr_usage_rules-0.2.0/src/cbpr_usage_rules.egg-info/SOURCES.txt +0 -53
  53. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/LICENSE +0 -0
  54. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/setup.cfg +0 -0
  55. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/helpers.py +0 -0
  56. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/loader.py +0 -0
  57. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/message.py +0 -0
  58. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/models.py +0 -0
  59. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/py.typed +0 -0
  60. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/reference/__init__.py +0 -0
  61. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/reference/countries.py +0 -0
  62. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/reference/currencies.py +0 -0
  63. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/registry.py +0 -0
  64. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/__init__.py +0 -0
  65. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/__init__.py +0 -0
  66. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/camt_052.py +0 -0
  67. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/camt_054.py +0 -0
  68. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_002.py +0 -0
  69. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_004.py +0 -0
  70. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_008.py +0 -0
  71. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_008_stp.py +0 -0
  72. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_009.py +0 -0
  73. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_009_adv.py +0 -0
  74. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pacs_009_cov.py +0 -0
  75. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2025/pain_001.py +0 -0
  76. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/__init__.py +0 -0
  77. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/camt_052.py +0 -0
  78. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/camt_054.py +0 -0
  79. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_002.py +0 -0
  80. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_004.py +0 -0
  81. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_008.py +0 -0
  82. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_008_stp.py +0 -0
  83. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_009.py +0 -0
  84. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_009_adv.py +0 -0
  85. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pacs_009_cov.py +0 -0
  86. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/rules/y2026/pain_001.py +0 -0
  87. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/schema.py +0 -0
  88. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/__init__.py +0 -0
  89. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/bic.py +0 -0
  90. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/country.py +0 -0
  91. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/currency.py +0 -0
  92. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/iban.py +0 -0
  93. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_rules/validators/lei.py +0 -0
  94. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_usage_rules.egg-info/dependency_links.txt +0 -0
  95. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_usage_rules.egg-info/entry_points.txt +0 -0
  96. {cbpr_usage_rules-0.2.0 → cbpr_usage_rules-0.4.0}/src/cbpr_usage_rules.egg-info/requires.txt +0 -0
  97. {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
@@ -2,3 +2,4 @@ include LICENSE
2
2
  include README.md
3
3
  include CHANGELOG.md
4
4
  recursive-include src/cbpr_rules py.typed
5
+ recursive-include src/cbpr_rules/examples *.xml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cbpr-usage-rules
3
- Version: 0.2.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
@@ -50,7 +50,7 @@ version = { attr = "cbpr_rules.__version__" }
50
50
  where = ["src"]
51
51
 
52
52
  [tool.setuptools.package-data]
53
- cbpr_rules = ["py.typed"]
53
+ cbpr_rules = ["py.typed", "examples/**/*.xml"]
54
54
 
55
55
  [tool.pytest.ini_options]
56
56
  testpaths = ["tests"]
@@ -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.2.0"
7
+ __version__ = "0.4.0"
8
8
 
9
9
  from .models import Rule, Severity, Violation
10
- from .engine import validate_file, validate_string, list_rules, available
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 ValidationError, available, list_rules, validate_file, validate_string
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.list:
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 every rule registered for a (year, message type)."""
95
- return [r.to_dict() for r in load_rules(year, msgtype)]
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