dpm-synth 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: dpm-synth
3
+ Version: 1.0.0
4
+ Summary: Generate synthetic EBA xBRL-CSV reports that satisfy a module's DPM-XL validation rules
5
+ Keywords: DPM,DPM-XL,EBA,XBRL,xBRL-CSV,test data,synthetic data,dpm-toolkit
6
+ Author: Luuk Hopman
7
+ Author-email: Luuk Hopman <github@luukhopman.nl>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
22
+ Classifier: Topic :: Office/Business :: Financial
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Classifier: Typing :: Typed
26
+ Requires-Dist: dpm-xl>=1.0,<2
27
+ Requires-Dist: dpm-xbrl>=1.0,<2
28
+ Requires-Python: >=3.11
29
+ Project-URL: Homepage, https://github.com/luukhopman/dpm-synth
30
+ Project-URL: Documentation, https://luukhopman.github.io/dpm-synth/
31
+ Project-URL: Source, https://github.com/luukhopman/dpm-synth
32
+ Project-URL: Issues, https://github.com/luukhopman/dpm-synth/issues
33
+ Project-URL: Changelog, https://github.com/luukhopman/dpm-synth/blob/main/CHANGELOG.md
34
+ Description-Content-Type: text/markdown
35
+
36
+ <p align="center">
37
+ <a href="https://luukhopman.github.io/dpm-synth/"><img src="https://luukhopman.github.io/dpm-synth/assets/logo.svg" alt="dpm-synth" width="240"></a>
38
+ </p>
39
+
40
+ <p align="center"><em>Generate synthetic EBA reports that satisfy DPM-XL validation rules.</em></p>
41
+
42
+ Generate synthetic EBA xBRL-CSV report packages that satisfy a module's
43
+ DPM-XL validation rules. Give dpm-synth a template with a module's tables,
44
+ cells and rules; it fills every cell with a plausible value, repairs the
45
+ values until the rules pass, and writes a report package that reads and
46
+ validates like a real submission. Use it to test loaders, data warehouses
47
+ and validators for EBA reports, for any module, without real supervisory
48
+ data.
49
+
50
+ dpm-synth is an independent project, not affiliated with or endorsed by the
51
+ European Banking Authority.
52
+
53
+ ## Installation
54
+
55
+ ```text
56
+ pip install dpm-synth # Python 3.11+; installs dpm-xl and dpm-xbrl
57
+ ```
58
+
59
+ ## Quickstart
60
+
61
+ ```python
62
+ from dpmxbrl import write_xbrl_csv
63
+
64
+ from dpmsynth import generate, load_template
65
+
66
+ template = load_template("minimal.json") # a module's tables, cells and rules
67
+ result = generate(template, seed=7)
68
+ print(result.passed, "of", result.rules, "rules pass; failing:", result.failed)
69
+ write_xbrl_csv(result.report, "report.zip")
70
+ ```
71
+
72
+ ```text
73
+ 2 of 2 rules pass; failing: []
74
+ ```
75
+
76
+ From the command line, with an exit status of 1 if some rules still fail:
77
+
78
+ ```console
79
+ $ dpm-synth generate minimal.json -o report.zip --seed 7
80
+ rules: 2, passed: 2, failed: 0, not evaluated: 0, not applicable: 0
81
+ rounds: 2 (failing rules per round: 1, 0)
82
+ wrote report.zip (3 facts)
83
+ ```
84
+
85
+ The sample template `minimal.json` is in
86
+ [`docs/samples`](https://github.com/luukhopman/dpm-synth/tree/main/docs/samples);
87
+ templates for real EBA modules are exported from a DPM release by any tool
88
+ that writes the
89
+ [template format](https://luukhopman.github.io/dpm-synth/reference/template-format/).
90
+
91
+ ## Features
92
+
93
+ - **Any module**: the `dpm-synth-template/1` format describes tables, open
94
+ keys, cells, data types, allowed items and DPM-XL rules.
95
+ - **Rules satisfied**: an evaluate-and-repair loop over the rules, with
96
+ [dpm-xl](https://luukhopman.github.io/dpm-xl/), repairs sums,
97
+ differences, products, sign rules, `isnull`, `in` and `match`. On EBA
98
+ DPM 4.3 COREP_OF, 999 of the 1,051 rules that apply pass, in 15 rounds;
99
+ the result names the rest.
100
+ - **Valid packages**: written by
101
+ [dpm-xbrl](https://luukhopman.github.io/dpm-xbrl/), with a fictional LEI,
102
+ and read back without broken EBA filing rules other than the file name.
103
+ - **Reproducible**: the same template, seed and versions give the same
104
+ bytes.
105
+ - **Large**: scaled packages stream open tables with millions of rows.
106
+
107
+ ## Toolkit
108
+
109
+ **Part of the dpm toolkit**, three Python packages that work together:
110
+
111
+ | Package | What it does |
112
+ | :-- | :-- |
113
+ | <a href="https://luukhopman.github.io/dpm-xl/"><img src="https://luukhopman.github.io/dpm-xl/assets/logo.svg" alt="dpm-xl" height="32"></a> | Parse and evaluate DPM-XL validation rules |
114
+ | <a href="https://luukhopman.github.io/dpm-xbrl/"><img src="https://luukhopman.github.io/dpm-xbrl/assets/logo.svg" alt="dpm-xbrl" height="32"></a> | Read, write and check xBRL-CSV and XBRL-XML reports |
115
+ | <a href="https://luukhopman.github.io/dpm-synth/"><img src="https://luukhopman.github.io/dpm-synth/assets/logo.svg" alt="dpm-synth" height="32"></a> | Generate test reports that satisfy DPM-XL rules **(this package)** |
116
+
117
+ The [toolkit tour](https://luukhopman.github.io/dpm-xl/guides/toolkit-tour/)
118
+ takes one report through all three: generated with dpm-synth, read with
119
+ dpm-xbrl, then evaluated and explained with dpm-xl.
120
+
121
+ ## Documentation
122
+
123
+ **Documentation: <https://luukhopman.github.io/dpm-synth/>**
124
+
125
+ - [Getting started](https://luukhopman.github.io/dpm-synth/getting-started/)
126
+ - [How-to guides](https://luukhopman.github.io/dpm-synth/guides/): write a
127
+ template, build one from a DPM release, load-test, reproducible
128
+ seeds, validate the output with dpm-xbrl and dpm-xl
129
+ - [Concepts](https://luukhopman.github.io/dpm-synth/concepts/): the
130
+ evaluate-and-repair loop and its limits
131
+ - [Reference](https://luukhopman.github.io/dpm-synth/reference/): the
132
+ [template format](https://luukhopman.github.io/dpm-synth/reference/template-format/),
133
+ the [CLI](https://luukhopman.github.io/dpm-synth/reference/cli/) and the
134
+ [Python API](https://luukhopman.github.io/dpm-synth/reference/api/)
135
+ - For AI agents: [llms.txt](https://luukhopman.github.io/dpm-synth/llms.txt)
136
+ and [llms-full.txt](https://luukhopman.github.io/dpm-synth/llms-full.txt)
137
+ - [Changelog](https://github.com/luukhopman/dpm-synth/blob/main/CHANGELOG.md),
138
+ [stability policy](https://luukhopman.github.io/dpm-synth/project/stability/)
139
+ and [security policy](https://luukhopman.github.io/dpm-synth/project/security/)
140
+
141
+ ## Development
142
+
143
+ ```text
144
+ uv sync
145
+ uv run pytest
146
+ uv run ruff check
147
+ uv run ruff format --check
148
+ uv run ty check
149
+ uv run python scripts/build_docs.py
150
+ ```
151
+
152
+ See [contributing](https://luukhopman.github.io/dpm-synth/project/contributing/).
153
+
154
+ ## Licence
155
+
156
+ MIT. See [LICENSE](https://github.com/luukhopman/dpm-synth/blob/main/LICENSE).
@@ -0,0 +1,12 @@
1
+ dpmsynth/__init__.py,sha256=hhwWYEgO_R-DgSWHjVV3ZoYZZfl5INmDX2HBpli9NgE,1244
2
+ dpmsynth/_version.py,sha256=DO2ekebzHMRqi9gwDm8St8crsVeRButMhe_Ft5LEVbs,244
3
+ dpmsynth/cli.py,sha256=uiN5AaNoR-LJAQUAawq0yM_Z0XxNxL3dB0kJfdEQhvY,9421
4
+ dpmsynth/generator.py,sha256=Cus-EKkoltTy8Zfx1MloqGyOaL_4cUZqKr9Whq3e1DQ,43674
5
+ dpmsynth/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ dpmsynth/scale.py,sha256=6SwQ2qv7OqnQnBPfFXfkuClkOiJkh3rUQ-c7v6zA29U,6774
7
+ dpmsynth/template.py,sha256=jttrLP2AqnavsUYK0MS7xfoE_iN4tPCfgd1NFVraqVA,17824
8
+ dpm_synth-1.0.0.dist-info/licenses/LICENSE,sha256=I10cuw8RJQykDum4d6-HLudRBtwy3Rcup1-HpxIf2ps,1068
9
+ dpm_synth-1.0.0.dist-info/WHEEL,sha256=si2p8vW03RnhyBnhWl4tj-ObJa78SpE-c9HsEtblGR0,80
10
+ dpm_synth-1.0.0.dist-info/entry_points.txt,sha256=I0M5AkPFkWDTWbuqdx_ichZxWgKM1yXIUkjvXAqb0Sw,49
11
+ dpm_synth-1.0.0.dist-info/METADATA,sha256=CH01AADNVabhYzSllvDAqFa1KPjPGUNp9dr8mkho0JU,6752
12
+ dpm_synth-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.5
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ dpm-synth = dpmsynth.cli:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luuk Hopman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
dpmsynth/__init__.py ADDED
@@ -0,0 +1,58 @@
1
+ """Generate synthetic EBA xBRL-CSV reports that satisfy DPM-XL validation rules.
2
+
3
+ Typical use::
4
+
5
+ from dpmxbrl import write_xbrl_csv
6
+
7
+ from dpmsynth import generate, load_template
8
+
9
+ result = generate(load_template("template.json"), seed=7)
10
+ write_xbrl_csv(result.report, "report.zip")
11
+ print(result.passed, result.failed)
12
+
13
+ Everything in ``__all__`` is the public API; import it from ``dpmsynth``.
14
+ """
15
+
16
+ from ._version import __version__
17
+ from .generator import (
18
+ DEFAULT_ENTITY,
19
+ DEFAULT_REFERENCE_DATE,
20
+ LEI_PREFIX,
21
+ MAX_OPEN_ROWS,
22
+ GenerationResult,
23
+ SyntheticData,
24
+ build_report,
25
+ generate,
26
+ )
27
+ from .scale import write_scaled_package
28
+ from .template import (
29
+ TEMPLATE_FORMAT,
30
+ ModuleTemplate,
31
+ OpenKey,
32
+ TemplateCell,
33
+ TemplateError,
34
+ TemplateRule,
35
+ TemplateTable,
36
+ load_template,
37
+ )
38
+
39
+ __all__ = [
40
+ "DEFAULT_ENTITY",
41
+ "DEFAULT_REFERENCE_DATE",
42
+ "LEI_PREFIX",
43
+ "MAX_OPEN_ROWS",
44
+ "TEMPLATE_FORMAT",
45
+ "GenerationResult",
46
+ "ModuleTemplate",
47
+ "OpenKey",
48
+ "SyntheticData",
49
+ "TemplateCell",
50
+ "TemplateError",
51
+ "TemplateRule",
52
+ "TemplateTable",
53
+ "__version__",
54
+ "build_report",
55
+ "generate",
56
+ "load_template",
57
+ "write_scaled_package",
58
+ ]
dpmsynth/_version.py ADDED
@@ -0,0 +1,8 @@
1
+ """The installed distribution version."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("dpm-synth")
7
+ except PackageNotFoundError: # pragma: no cover - not installed
8
+ __version__ = "0+unknown"
dpmsynth/cli.py ADDED
@@ -0,0 +1,283 @@
1
+ """Command-line interface: ``dpm-synth generate``.
2
+
3
+ Exit statuses: 0 when every evaluable rule passes, 1 when some still fail
4
+ (the package is written either way), 2 for a usage error, and 3 when the
5
+ template cannot be read or is invalid, or the package cannot be written.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import re
13
+ import sys
14
+ from collections.abc import Iterator, Sequence
15
+ from datetime import date
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from dpmxbrl import write_xbrl_csv
20
+
21
+ from ._version import __version__
22
+ from .generator import DEFAULT_ENTITY, MAX_OPEN_ROWS, GenerationResult, generate
23
+ from .scale import _MAX_ROWS, write_scaled_package
24
+ from .template import TemplateError, load_template
25
+
26
+ RULES_FAILED = 1
27
+ """Exit status when the package is written but some rules still fail."""
28
+
29
+ INVALID_INPUT = 3
30
+ """Exit status when the template is unreadable or invalid, or the package
31
+ cannot be written."""
32
+
33
+ JSON_SCHEMA_VERSION = 1
34
+ """Version of the ``generate --json`` layout; it changes only with a new major."""
35
+
36
+ _IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]*")
37
+
38
+ _EPILOG = """examples:
39
+ dpm-synth generate template.json -o report.zip
40
+ dpm-synth generate template.json -o report.zip --seed 7 --open-rows 3
41
+ dpm-synth generate template.json -o report.zip --json
42
+ dpm-synth generate template.json -o big.zip --scale-rows 1000000
43
+
44
+ exit statuses: 0 every evaluable rule passes, 1 some rules still fail (the
45
+ package is written), 2 usage error, 3 unreadable or invalid template, or the
46
+ package cannot be written.
47
+
48
+ The template format: https://luukhopman.github.io/dpm-synth/reference/template-format/"""
49
+
50
+
51
+ class _Options(argparse.HelpFormatter):
52
+ """Lists ``-o, --output ZIP`` as Python 3.13 and later do, on every version."""
53
+
54
+ def _format_action_invocation(self, action: argparse.Action) -> str:
55
+ if not action.option_strings or action.nargs == 0:
56
+ return super()._format_action_invocation(action)
57
+ value = self._format_args(
58
+ action, self._get_default_metavar_for_optional(action)
59
+ )
60
+ return f"{', '.join(action.option_strings)} {value}"
61
+
62
+
63
+ class _Main(_Options, argparse.RawDescriptionHelpFormatter):
64
+ """Keeps the line breaks of the description and the epilog."""
65
+
66
+
67
+ def _whole(minimum: int, maximum: int | None = None) -> Any:
68
+ def parse(text: str) -> int:
69
+ try:
70
+ value = int(text)
71
+ except ValueError:
72
+ raise argparse.ArgumentTypeError(
73
+ f"expected a whole number, got {text!r}"
74
+ ) from None
75
+ if value < minimum:
76
+ raise argparse.ArgumentTypeError(
77
+ f"expected a whole number of {minimum} or more, got {text!r}"
78
+ )
79
+ if maximum is not None and value > maximum:
80
+ raise argparse.ArgumentTypeError(
81
+ f"expected a whole number of at most {maximum:_}, got {text!r}"
82
+ )
83
+ return value
84
+
85
+ parse.__name__ = "number"
86
+ return parse
87
+
88
+
89
+ def _date(text: str) -> date:
90
+ try:
91
+ if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", text):
92
+ raise ValueError(text) # fromisoformat also takes 20261231
93
+ return date.fromisoformat(text)
94
+ except ValueError:
95
+ raise argparse.ArgumentTypeError(
96
+ f"expected a date as YYYY-MM-DD, got {text!r}"
97
+ ) from None
98
+
99
+
100
+ def _package(text: str) -> Path:
101
+ if not text.casefold().endswith(".zip"):
102
+ raise argparse.ArgumentTypeError(f"expected a .zip file, got {text!r}")
103
+ return Path(text)
104
+
105
+
106
+ def _entity(text: str) -> str:
107
+ if not _IDENTIFIER.fullmatch(text):
108
+ raise argparse.ArgumentTypeError(
109
+ f"expected letters and digits, such as an LEI, got {text!r}"
110
+ )
111
+ return text
112
+
113
+
114
+ def _parser() -> argparse.ArgumentParser:
115
+ parser = argparse.ArgumentParser(
116
+ prog="dpm-synth",
117
+ description="Generate synthetic EBA xBRL-CSV reports that satisfy a module's\n"
118
+ "DPM-XL validation rules.",
119
+ epilog=_EPILOG,
120
+ formatter_class=_Main,
121
+ )
122
+ parser.add_argument(
123
+ "--version", action="version", version=f"%(prog)s {__version__}"
124
+ )
125
+ commands = parser.add_subparsers(dest="command", required=True)
126
+ run = commands.add_parser(
127
+ "generate",
128
+ help="write an xBRL-CSV report package for a template",
129
+ formatter_class=_Options,
130
+ description="Generate values for every cell of a template, repair them "
131
+ "until the rules pass, and write an xBRL-CSV report package.",
132
+ )
133
+ run.add_argument("template", type=Path, help="dpm-synth-template/1 JSON file")
134
+ run.add_argument(
135
+ "-o",
136
+ "--output",
137
+ type=_package,
138
+ required=True,
139
+ metavar="ZIP",
140
+ help="report package to write (.zip)",
141
+ )
142
+ run.add_argument(
143
+ "--seed", type=int, default=0, metavar="N", help="random seed (default 0)"
144
+ )
145
+ run.add_argument(
146
+ "--open-rows",
147
+ type=_whole(1, MAX_OPEN_ROWS),
148
+ default=2,
149
+ metavar="N",
150
+ help=f"key rows per open table, at most {MAX_OPEN_ROWS} (default 2)",
151
+ )
152
+ run.add_argument(
153
+ "--max-rounds",
154
+ type=_whole(0),
155
+ default=40,
156
+ metavar="N",
157
+ help="most evaluate-and-repair rounds (default 40)",
158
+ )
159
+ run.add_argument(
160
+ "--scale-rows",
161
+ type=_whole(1, _MAX_ROWS),
162
+ metavar="N",
163
+ help="after solving, write every open table with N key rows "
164
+ "(load testing; rules over open-table totals will fail)",
165
+ )
166
+ run.add_argument(
167
+ "--reference-date",
168
+ type=_date,
169
+ metavar="YYYY-MM-DD",
170
+ help="reference date (default: the template's, else 2026-12-31)",
171
+ )
172
+ run.add_argument(
173
+ "--entity",
174
+ type=_entity,
175
+ default=DEFAULT_ENTITY,
176
+ metavar="LEI",
177
+ help=f"LEI of the reporting entity (default {DEFAULT_ENTITY})",
178
+ )
179
+ run.add_argument(
180
+ "--level",
181
+ choices=("CON", "IND"),
182
+ default="CON",
183
+ help="reporting level, the entity suffix (default CON)",
184
+ )
185
+ run.add_argument("--json", action="store_true", help="print a JSON summary")
186
+ return parser
187
+
188
+
189
+ def main(argv: Sequence[str] | None = None) -> int:
190
+ """Run the command line.
191
+
192
+ Args:
193
+ argv: Arguments without the program name; ``sys.argv[1:]`` when
194
+ ``None``.
195
+
196
+ Returns:
197
+ int: The exit status: 0 when every evaluable rule passes, 1 when some
198
+ still fail, 3 when the template or the package cannot be read or
199
+ written. Usage errors exit with status 2 through ``SystemExit``.
200
+ """
201
+ arguments = _parser().parse_args(argv)
202
+ try:
203
+ template = load_template(arguments.template)
204
+ except TemplateError as error:
205
+ return _fail(error)
206
+ result = generate(
207
+ template,
208
+ seed=arguments.seed,
209
+ open_rows=arguments.open_rows,
210
+ max_rounds=arguments.max_rounds,
211
+ entity=arguments.entity,
212
+ reporting_level=arguments.level,
213
+ reference_date=arguments.reference_date,
214
+ )
215
+ try:
216
+ if arguments.scale_rows is None:
217
+ write_xbrl_csv(result.report, arguments.output)
218
+ facts = len(result.report.facts)
219
+ else:
220
+ written = write_scaled_package(
221
+ result.data, arguments.output, rows=arguments.scale_rows
222
+ )
223
+ facts = sum(written.values())
224
+ except (OSError, ValueError) as error: # ReportError is a ValueError
225
+ return _fail(f"{arguments.output}: cannot write: {_reason(error)}")
226
+ if arguments.json:
227
+ print(json.dumps(_summary(result, arguments, facts), indent=2))
228
+ else:
229
+ for line in _lines(result):
230
+ print(line)
231
+ print(f"wrote {arguments.output} ({facts} facts)")
232
+ return RULES_FAILED if result.failed else 0
233
+
234
+
235
+ def _fail(error: object) -> int:
236
+ print(f"error: {error}", file=sys.stderr)
237
+ return INVALID_INPUT
238
+
239
+
240
+ def _reason(error: Exception) -> str:
241
+ if isinstance(error, OSError) and error.strerror:
242
+ return error.strerror
243
+ return str(error)
244
+
245
+
246
+ def _summary(
247
+ result: GenerationResult, arguments: argparse.Namespace, facts: int
248
+ ) -> dict[str, Any]:
249
+ template = result.data.template
250
+ return {
251
+ "schema_version": JSON_SCHEMA_VERSION,
252
+ "output": str(arguments.output),
253
+ "release": template.release,
254
+ "module": template.module,
255
+ "seed": arguments.seed,
256
+ "rules": result.rules,
257
+ "passed": result.passed,
258
+ "failed": result.failed,
259
+ "not_evaluated": result.not_evaluated,
260
+ "not_applicable": result.not_applicable,
261
+ "rounds": result.rounds,
262
+ "history": result.history,
263
+ "repairs": result.repairs,
264
+ "facts": facts,
265
+ }
266
+
267
+
268
+ def _lines(result: GenerationResult) -> Iterator[str]:
269
+ yield (
270
+ f"rules: {result.rules}, passed: {result.passed}, "
271
+ f"failed: {len(result.failed)}, not evaluated: {len(result.not_evaluated)}, "
272
+ f"not applicable: {len(result.not_applicable)}"
273
+ )
274
+ history = ", ".join(str(count) for count in result.history) or "-"
275
+ yield f"rounds: {result.rounds} (failing rules per round: {history})"
276
+ if result.failed:
277
+ shown = ", ".join(result.failed[:20])
278
+ more = len(result.failed) - 20
279
+ yield f"still failing: {shown}" + (f" and {more} more" if more > 0 else "")
280
+
281
+
282
+ if __name__ == "__main__": # pragma: no cover
283
+ raise SystemExit(main())