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.
- dpm_synth-1.0.0.dist-info/METADATA +156 -0
- dpm_synth-1.0.0.dist-info/RECORD +12 -0
- dpm_synth-1.0.0.dist-info/WHEEL +4 -0
- dpm_synth-1.0.0.dist-info/entry_points.txt +3 -0
- dpm_synth-1.0.0.dist-info/licenses/LICENSE +21 -0
- dpmsynth/__init__.py +58 -0
- dpmsynth/_version.py +8 -0
- dpmsynth/cli.py +283 -0
- dpmsynth/generator.py +1133 -0
- dpmsynth/py.typed +0 -0
- dpmsynth/scale.py +180 -0
- dpmsynth/template.py +504 -0
|
@@ -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,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
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())
|