Skip to content

Commit c285a28

Browse files
authored
feat: Add CLI Command xml2json (#55)
1 parent b4882fc commit c285a28

File tree

8 files changed

+143
-3
lines changed

8 files changed

+143
-3
lines changed

.github/workflows/unittests.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ jobs:
1010
strategy:
1111
matrix:
1212
python-version: ["3.11", "3.12", "3.13"]
13-
pydantic: [true, false]
13+
pydantic: [install_pydantic, skip_pydantic]
14+
cli: [install_typer, skip_typer]
1415
os: [ubuntu-latest]
1516
steps:
1617
- uses: actions/checkout@v4
@@ -23,8 +24,11 @@ jobs:
2324
python -m pip install --upgrade pip
2425
pip install tox
2526
- name: install pydantic if requested
26-
if: matrix.run_step == 'true'
27+
if: matrix.run_step == 'install_pydantic'
2728
run: pip install .[pydantic]
29+
- name: install typer if requested
30+
if: matrix.run_step == 'install_typer'
31+
run: pip install .[cli]
2832
- name: Run the Unit Tests via Tox
2933
run: |
3034
tox -e tests

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@ Das Ergebnis sieht dann so aus:
124124
"codes": []
125125
},
126126
```
127+
### CLI Tool für XML➡️JSON Konvertierung
128+
Mit
129+
```bash
130+
pip install fundamend[cli]
131+
```
132+
Kann ein CLI-Tool in der entsprechenden venv installiert werden, das einzelne MIG- und AHB-XML-Dateien in entsprechende JSONs konvertiert:
133+
```bash
134+
(myvenv): xml2json path/to/mig.xml
135+
```
136+
erzeugt `path/to/mig.json`. Und
137+
```bash
138+
(myvenv): xml2json path/to/my/directory
139+
```
140+
konvertiert alle XML-Dateien im entsprechenden Verzeichnis.
127141

128142
### JSON Schemas
129143
Das fundamend Datenmodell ist auch als JSON Schema verfügbar: [`json_schemas`](json_schemas).

domain-specific-terms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ als
88
paket
99
beginn
1010
referenz
11+
alle

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ pydantic = [
3535
"pydantic>=2"
3636
# if you install fundamend[pydantic], the dataclasses from pydantic will be used
3737
]
38+
cli = [
39+
"fundamend[pydantic]",
40+
"typer" # if you install fundamend[cli], the cli commands are available via typer
41+
]
3842
spellcheck = [
3943
"codespell==2.3.0"
4044
]
@@ -65,6 +69,12 @@ profile = "black"
6569
[tool.pylint."MESSAGES CONTROL"]
6670
max-line-length = 120
6771

72+
[project.scripts]
73+
xml2json = "fundamend.cli:main"
74+
# fundamend is the package in the src directory
75+
# .cli means the cli.py module inside the fundamend package
76+
# :main means the def main() function inside the cli.py module
77+
6878
[mypy]
6979
truethy-bool = true
7080

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# This file is autogenerated by pip-compile with Python 3.11
2+
# This file is autogenerated by pip-compile with Python 3.12
33
# by the following command:
44
#
55
# pip-compile pyproject.toml

src/fundamend/cli.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""contains the entrypoint for the command line interface"""
2+
3+
import json
4+
import sys
5+
from pathlib import Path
6+
7+
import typer
8+
from pydantic import RootModel
9+
from rich.console import Console
10+
11+
from fundamend import AhbReader, Anwendungshandbuch, MessageImplementationGuide, MigReader
12+
13+
app = typer.Typer(help="Convert XML(s) by BDEW to JSON(s)")
14+
err_console = Console(stderr=True) # https://typer.tiangolo.com/tutorial/printing/#printing-to-standard-error
15+
16+
17+
def _convert_to_json_file(xml_file_path: Path) -> Path:
18+
"""converts the given XML file to a JSON file and returns the path of the latter"""
19+
if not xml_file_path.is_file():
20+
raise ValueError(f"The given path {xml_file_path.absolute()} is not a file")
21+
is_ahb = "ahb" in xml_file_path.stem.lower()
22+
is_mig = "mig" in xml_file_path.stem.lower()
23+
if is_ahb and is_mig:
24+
raise ValueError(f"Cannot detect if {xml_file_path} is an AHB or MIG")
25+
root_model: RootModel[Anwendungshandbuch] | RootModel[MessageImplementationGuide]
26+
if is_ahb:
27+
ahb_model = AhbReader(xml_file_path).read()
28+
root_model = RootModel[Anwendungshandbuch](ahb_model)
29+
elif is_mig:
30+
mig_model = MigReader(xml_file_path).read()
31+
root_model = RootModel[MessageImplementationGuide](mig_model)
32+
else:
33+
raise ValueError(f"Seems like {xml_file_path} is neither an AHB nor a MIG")
34+
out_dict = root_model.model_dump(mode="json")
35+
json_file_path = xml_file_path.with_suffix(".json")
36+
with open(json_file_path, encoding="utf-8", mode="w") as outfile:
37+
json.dump(out_dict, outfile, indent=True, ensure_ascii=False)
38+
print(f"Successfully converted {xml_file_path} file to JSON {json_file_path}")
39+
return json_file_path
40+
41+
42+
@app.command()
43+
def main(xml_in_path: Path) -> None:
44+
"""
45+
converts the xml file from xml_in_path to a json file next to the .xml
46+
"""
47+
if not xml_in_path.exists():
48+
err_console.print(f"The path {xml_in_path.absolute()} does not exist")
49+
sys.exit(1)
50+
if xml_in_path.is_dir():
51+
for xml_path in xml_in_path.rglob("*.xml"):
52+
_convert_to_json_file(xml_path)
53+
else:
54+
_convert_to_json_file(xml_in_path)

tox.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ commands = python -m pytest --basetemp={envtmpdir} {posargs}
2323
deps =
2424
{[testenv:tests]deps}
2525
.[linting]
26+
.[cli]
2627
# add your fixtures like e.g. pytest_datafiles here
2728
setenv = PYTHONPATH = {toxinidir}/src
2829
commands =
@@ -37,6 +38,7 @@ deps =
3738
{[testenv:tests]deps}
3839
.[type_check]
3940
.[pydantic]
41+
.[cli]
4042
commands =
4143
mypy --show-error-codes src/fundamend --strict
4244
mypy --show-error-codes unittests --strict
@@ -60,6 +62,7 @@ deps =
6062
{[testenv:tests]deps}
6163
.[coverage]
6264
.[pydantic]
65+
.[cli]
6366
setenv = PYTHONPATH = {toxinidir}/src
6467
commands =
6568
coverage run -m pytest --basetemp={envtmpdir} {posargs}

unittests/test_cli.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
_SKIP_TESTS = False
6+
try:
7+
from typer.testing import CliRunner
8+
9+
from fundamend.cli import app
10+
except ImportError:
11+
_SKIP_TESTS = True
12+
13+
14+
def _copy_xml_file(inpath: Path, outpath: Path) -> None:
15+
with open(outpath, encoding="utf-8", mode="w") as outfile:
16+
with open(inpath, encoding="utf-8", mode="r") as infile:
17+
outfile.write(infile.read())
18+
19+
20+
def test_cli_single_file_mig(tmp_path: Path) -> None:
21+
if _SKIP_TESTS:
22+
pytest.skip("Seems like typer is not installed")
23+
original_mig_file = Path(__file__).parent / "example_files" / "UTILTS_MIG_1.1c_Lesefassung_2023_12_12.xml"
24+
tmp_mig_path = tmp_path / "my_mig.xml"
25+
_copy_xml_file(original_mig_file, tmp_mig_path)
26+
runner = CliRunner()
27+
runner.invoke(app, [str(tmp_mig_path)])
28+
assert (tmp_path / "my_mig.json").exists()
29+
30+
31+
def test_cli_single_file_ahb(tmp_path: Path) -> None:
32+
if _SKIP_TESTS:
33+
pytest.skip("Seems like typer is not installed")
34+
original_ahb_file = Path(__file__).parent / "example_files" / "UTILTS_AHB_1.1d_Konsultationsfassung_2024_04_02.xml"
35+
tmp_ahb_path = tmp_path / "my_ahb.xml"
36+
_copy_xml_file(original_ahb_file, tmp_ahb_path)
37+
runner = CliRunner()
38+
runner.invoke(app, [str(tmp_ahb_path)])
39+
assert (tmp_path / "my_ahb.json").exists()
40+
41+
42+
def test_cli_directory(tmp_path: Path) -> None:
43+
if _SKIP_TESTS:
44+
pytest.skip("Seems like typer is not installed")
45+
original_mig_file = Path(__file__).parent / "example_files" / "UTILTS_MIG_1.1c_Lesefassung_2023_12_12.xml"
46+
tmp_mig_path = tmp_path / "my_mig.xml"
47+
original_ahb_file = Path(__file__).parent / "example_files" / "UTILTS_AHB_1.1d_Konsultationsfassung_2024_04_02.xml"
48+
tmp_ahb_path = tmp_path / "my_ahb.xml"
49+
_copy_xml_file(original_ahb_file, tmp_ahb_path)
50+
_copy_xml_file(original_mig_file, tmp_mig_path)
51+
runner = CliRunner()
52+
runner.invoke(app, [str(tmp_path)])
53+
assert (tmp_path / "my_mig.json").exists()
54+
assert (tmp_path / "my_ahb.json").exists()

0 commit comments

Comments
 (0)