Skip to content

Commit 4e34b8a

Browse files
hf-kkleinKonstantin
andauthored
feat: add SQLModels for FlatAnwendungshandbuch; Add PoC for SQLite (#557)
--------- Co-authored-by: Konstantin <[email protected]>
1 parent ffc8583 commit 4e34b8a

File tree

4 files changed

+184
-0
lines changed

4 files changed

+184
-0
lines changed

pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,20 @@ dynamic = ["readme", "version"]
4949
kohlrahbi = "kohlrahbi:cli"
5050

5151
[project.optional-dependencies]
52+
sqlmodels = [
53+
"sqlmodel>=0.0.22",
54+
"sqlalchemy[mypy]>=2.0.37"
55+
]
5256
dev = [
57+
"kohlrahbi[sqlmodels]",
5358
"kohlrahbi[test]",
5459
"kohlrahbi[lint]",
5560
"kohlrahbi[typecheck]",
5661
"kohlrahbi[formatting]",
5762
]
5863
lint = ["pylint==3.3.3", "pylint-pydantic==0.3.5"]
5964
test = [
65+
"kohlrahbi[sqlmodels]",
6066
"coverage==7.6.10",
6167
"dictdiffer==0.9.0",
6268
"freezegun==1.5.1",
@@ -65,6 +71,7 @@ test = [
6571
"syrupy==4.8.1",
6672
]
6773
typecheck = [
74+
"kohlrahbi[sqlmodels]",
6875
"mypy==1.11.2",
6976
"networkx-stubs==0.0.1",
7077
"pandas-stubs==2.2.3.241126",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""
2+
sql models to store Anwendungshandbuecher in a database
3+
you need to install kohlrahbi[sqlmodels] to use this sub-package
4+
"""
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""model classes copy-pasted from kohlrahbi"""
2+
3+
# pylint: disable=too-few-public-methods
4+
5+
try:
6+
from sqlalchemy import UniqueConstraint
7+
from sqlmodel import Field, Relationship, SQLModel
8+
except ImportError as import_error:
9+
import_error.msg += "; Did you install kohlrahbi[sqlmodels]?"
10+
# sqlmodel is only an optional dependency when kohlrahbi is used to fill a database
11+
raise
12+
import uuid
13+
from typing import Optional
14+
from uuid import UUID
15+
16+
from efoli import EdifactFormat, EdifactFormatVersion
17+
18+
# the models here do NOT inherit from the original models, because I didn't manage to fix the issue that arise:
19+
# https://github.com/Hochfrequenz/kohlrahbi/issues/556
20+
# <frozen abc>:106: in __new__
21+
# ???
22+
# E TypeError: FlatAnwendungshandbuch.__init_subclass__() takes no keyword arguments
23+
#
24+
# or
25+
#
26+
# ..\.tox\dev\Lib\site-packages\sqlmodel\main.py:697: in get_sqlalchemy_type
27+
# raise ValueError(f"{type_} has no matching SQLAlchemy type")
28+
# E ValueError: <class 'kohlrahbi.models.anwendungshandbuch.AhbMetaInformation'> has no matching SQLAlchemy type
29+
30+
31+
class FlatAnwendungshandbuch(SQLModel, table=True): # team
32+
"""
33+
A flat Anwendungshandbuch (AHB) models an Anwendungshandbuch as combination of some meta data and an ordered list
34+
of `.class:`.FlatAhbLine s. Basically a flat Anwendungshandbuch is the result of a simple scraping approach.
35+
You can create instances of this class without knowing anything about the "under the hood" structure of AHBs or MIGs
36+
"""
37+
38+
id: UUID = Field(primary_key=True, default_factory=uuid.uuid4, description="optional key")
39+
meta: Optional["AhbMetaInformation"] = Relationship(back_populates="flatanwendungshandbuch")
40+
lines: list["AhbLine"] = Relationship(back_populates="flatanwendungshandbuch")
41+
42+
43+
class AhbLine(SQLModel, table=True):
44+
"""
45+
An AhbLine is a single line inside the machine-readable, flat AHB.
46+
"""
47+
48+
__table_args__ = (UniqueConstraint("ahb_id", "position_inside_ahb", name="IX_position_once_per_ahb"),)
49+
id: UUID = Field(primary_key=True, default_factory=uuid.uuid4, description="optional key")
50+
# yes, it's actually that bad already
51+
52+
position_inside_ahb: int = Field(index=True)
53+
ahb_id: UUID | None = Field(default=None, foreign_key="flatanwendungshandbuch.id")
54+
flatanwendungshandbuch: FlatAnwendungshandbuch | None = Relationship(back_populates="lines")
55+
56+
# fields copy-pasted from original model:
57+
guid: Optional[UUID]
58+
segment_group_key: Optional[str]
59+
segment_code: Optional[str]
60+
data_element: Optional[str]
61+
segment_id: Optional[str]
62+
value_pool_entry: Optional[str]
63+
name: Optional[str]
64+
ahb_expression: Optional[str]
65+
conditions: Optional[str]
66+
section_name: Optional[str]
67+
index: Optional[int]
68+
69+
70+
class AhbMetaInformation(SQLModel, table=True):
71+
"""
72+
Meta information about an AHB like e.g. its title, Prüfidentifikator, possible sender and receiver roles
73+
"""
74+
75+
__table_args__ = (
76+
UniqueConstraint("pruefidentifikator", "edifact_format_version", name="IX_pruefi_once_per_format_version"),
77+
)
78+
id: UUID = Field(primary_key=True, default_factory=uuid.uuid4, description="optional key")
79+
edifact_format: EdifactFormat = Field(index=True)
80+
edifact_format_version: EdifactFormatVersion = Field(index=True)
81+
flatanwendungshandbuch: Optional[FlatAnwendungshandbuch] = Relationship(
82+
back_populates="meta", sa_relationship_kwargs={"uselist": False}
83+
)
84+
ahb_id: UUID | None = Field(default=None, foreign_key="flatanwendungshandbuch.id")
85+
86+
# copy-pasted fields from original model:
87+
pruefidentifikator: str
88+
maus_version: Optional[str]
89+
description: Optional[str]
90+
direction: Optional[str]

unittests/test_sqlmodels.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""
2+
we try to fill a database using kohlrahbi[sqlmodels] and the data from the machine-readable AHB submodule
3+
"""
4+
5+
import json
6+
from pathlib import Path
7+
from typing import Generator
8+
9+
import pytest
10+
from efoli import EdifactFormat, EdifactFormatVersion
11+
from sqlmodel import Session, SQLModel, create_engine, select
12+
13+
from kohlrahbi.ahb import process_pruefi
14+
from kohlrahbi.enums.ahbexportfileformat import AhbExportFileFormat
15+
from kohlrahbi.models.sqlmodels.anwendungshandbuch import AhbLine, AhbMetaInformation, FlatAnwendungshandbuch
16+
17+
18+
@pytest.fixture()
19+
def sqlite_session(tmp_path: Path) -> Generator[Session, None, None]:
20+
database_path = tmp_path / "test.db"
21+
engine = create_engine(f"sqlite:///{database_path}")
22+
SQLModel.metadata.drop_all(engine)
23+
SQLModel.metadata.create_all(engine)
24+
with Session(bind=engine) as session:
25+
yield session
26+
27+
28+
def _load_flat_ahb_to_db(
29+
session: Session, json_path: Path, edifact_format: EdifactFormat, edifact_format_version: EdifactFormatVersion
30+
) -> str:
31+
"""returns the pruefi"""
32+
# you may use this kind of function to eventually load _all_ AHBs into a DB
33+
# if you e.g. check out our machine-readable AHB repository,
34+
# for json_path in _mr_ahb_submodule_path.rglob("FV*/**/*.json"):
35+
# with open(json_path, "r", encoding="utf-8") as json_file:
36+
# edifact_format = EdifactFormat(json_path.parent.parent.name)
37+
# edifact_format_version = EdifactFormatVersion(json_path.parent.parent.parent.name)
38+
# _load_flat_ahb_to_db(...)
39+
with open(json_path, "r", encoding="utf-8") as json_file:
40+
file_body = json.loads(json_file.read())
41+
flat_ahb = FlatAnwendungshandbuch.model_validate(file_body)
42+
edifact_format = EdifactFormat.UTILMD
43+
edifact_format_version = EdifactFormatVersion.FV2504
44+
file_body["meta"]["edifact_format"] = str(edifact_format)
45+
file_body["meta"]["edifact_format_version"] = str(edifact_format_version)
46+
meta = AhbMetaInformation.model_validate(file_body["meta"])
47+
session.add(meta)
48+
flat_ahb.meta = meta
49+
for line_index, raw_line in enumerate(file_body["lines"]):
50+
raw_line["position_inside_ahb"] = line_index
51+
line = AhbLine.model_validate(raw_line)
52+
flat_ahb.lines.append(line)
53+
session.add(line)
54+
session.add(flat_ahb)
55+
session.commit()
56+
session.flush()
57+
return meta.pruefidentifikator
58+
59+
60+
def test_sqlmodels(sqlite_session: Session, tmp_path: Path) -> None:
61+
flat_ahb_dir_path = tmp_path
62+
docx_file_path = (
63+
Path(__file__).parent.parent
64+
/ "edi_energy_mirror"
65+
/ "edi_energy_de"
66+
/ "FV2504"
67+
/ "UTILMDAHBStrom-informatorischeLesefassung2.1_99991231_20250404.docx"
68+
)
69+
process_pruefi("55001", docx_file_path, flat_ahb_dir_path, (AhbExportFileFormat.FLATAHB,))
70+
flat_ahb_path = flat_ahb_dir_path / "UTILMD" / "flatahb" / "55001.json"
71+
assert flat_ahb_path.exists()
72+
pruefi = _load_flat_ahb_to_db(sqlite_session, flat_ahb_path, EdifactFormat.UTILMD, EdifactFormatVersion.FV2504)
73+
ahbs_from_db = sqlite_session.exec(
74+
select(FlatAnwendungshandbuch)
75+
.join(AhbMetaInformation, FlatAnwendungshandbuch.id == AhbMetaInformation.ahb_id) # type:ignore[arg-type]
76+
.join(AhbLine, FlatAnwendungshandbuch.id == AhbLine.ahb_id) # type:ignore[arg-type]
77+
.where(AhbMetaInformation.pruefidentifikator == "55001")
78+
).all()
79+
ahb_from_db: FlatAnwendungshandbuch = ahbs_from_db[0]
80+
assert isinstance(ahb_from_db, FlatAnwendungshandbuch)
81+
assert any(ahb_from_db.lines)
82+
assert ahb_from_db.meta is not None
83+
assert ahb_from_db.meta.pruefidentifikator == pruefi

0 commit comments

Comments
 (0)