Skip to content

Commit 4366ff5

Browse files
authored
Add AHB Models + Logic (#8)
1 parent b289075 commit 4366ff5

12 files changed

+121496
-9
lines changed

README.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Das Paket ist auf PyPI verfügbar und kann mit pip installiert werden:
2727
pip install fundamend
2828
```
2929

30+
### Message Implementation Guides (MIG) deserialisieren
3031
```python
3132
from pathlib import Path
3233
from fundamend import MigReader, MessageImplementationGuide
@@ -44,10 +45,40 @@ mig = reader.read()
4445
assert isinstance(mig, MessageImplementationGuide)
4546
assert mig.format == "UTILTS"
4647
```
47-
Das vollständige Beispiel findet sich in den [unittests](unittests).
4848

49-
Aktuell (Version 0.1) können nur MIGs gelesen werden.
50-
Der AHB-Teil soll aber folgen.
49+
### Anwendungshandbuch (AHB) deserialisieren
50+
```python
51+
from pathlib import Path
52+
from fundamend import AhbReader, Anwendungshandbuch
53+
54+
# Angenommen, ahb_utilts.xml enthält:
55+
# <?xml version="1.0" encoding="UTF-8"?>
56+
# <AHB Versionsnummer="1.1d"
57+
# Veroeffentlichungsdatum="02.04.2024"
58+
# Author="BDEW">
59+
# <AWF Pruefidentifikator="25001" Beschreibung="Berechnungsformel" Kommunikation_von="NB an MSB / LF">
60+
# ...
61+
# </AWF>
62+
# </AHB>
63+
64+
reader = AhbReader(Path("pfad/zur/ahb_utils.xml"))
65+
ahb = reader.read()
66+
assert isinstance(ahb, Anwendungshandbuch)
67+
assert {awf.pruefidentifikator for awf in ahb.anwendungsfaelle} == {
68+
"25001",
69+
"25002",
70+
"25003",
71+
"25004",
72+
"25005",
73+
"25006",
74+
"25007",
75+
"25008",
76+
"25009",
77+
}
78+
```
79+
80+
Die vollständigen Beispiele finden sich in den [unittests](unittests).
81+
5182

5283
## Verwendung und Mitwirken
5384
Der Code ist MIT-lizenziert und kann daher frei verwendet werden.

domain-specific-terms.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ oder
66
ist
77
als
88
paket
9+
beginn
10+
referenz

src/fundamend/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
pip install xml-fundamend
44
"""
55

6-
from .models import MessageImplementationGuide
7-
from .reader import MigReader
6+
from .models import Anwendungshandbuch, MessageImplementationGuide
7+
from .reader import AhbReader, MigReader
88

9-
__all__ = ["MigReader", "MessageImplementationGuide"]
9+
__all__ = ["MigReader", "MessageImplementationGuide", "AhbReader", "Anwendungshandbuch"]

src/fundamend/models/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""contains model classes representing MIGs and AHBs"""
22

3+
from .anwendungshandbuch import Anwendungshandbuch
34
from .messageimplementationguide import MessageImplementationGuide
45

5-
__all__ = ["MessageImplementationGuide"]
6+
__all__ = ["MessageImplementationGuide", "Anwendungshandbuch"]
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
""""model classes for Anwendungshandbücher (AHB)"""
2+
3+
# pylint:disable=duplicate-code
4+
# the structures are similar, still we decided against inheritance, so there's naturally a little bit of duplication
5+
6+
from dataclasses import dataclass
7+
from datetime import date
8+
9+
10+
@dataclass(kw_only=True, eq=True, frozen=True)
11+
class Code:
12+
"""
13+
a single code element inside an AHB DataElement
14+
"""
15+
16+
# Example:
17+
# <Code Name="Netznutzungszeiten-Nachricht" Description="" AHB_Status="X">UTILTS</Code>
18+
# properties are similar to MIG data model, still we like to keep them separate instead of inheriting
19+
name: str # e.g. 'Netznutzungszeiten-Nachricht'
20+
description: str | None = None # e.g. ''
21+
value: str | None # e.g. 'UTILTS'
22+
ahb_status: str #: e.g. 'X' # new for AHB
23+
24+
25+
@dataclass(kw_only=True, eq=True, frozen=True)
26+
class DataElement:
27+
"""
28+
A single data element inside a AHB Segment.
29+
This models both the 'Datenelement' and the 'Gruppendatenelement'
30+
"""
31+
32+
# Example:
33+
# <D_0065 Name="Nachrichtentyp-Kennung">
34+
# <Code Name="Netznutzungszeiten-Nachricht" Description="" AHB_Status="X">UTILTS</Code>
35+
# </D_0065>
36+
id: str # e.g. 'D_0065'
37+
name: str # e.g. 'Nachrichtentyp-Kennung'
38+
codes: list[Code]
39+
40+
41+
@dataclass(eq=True, kw_only=True, frozen=True)
42+
class DataElementGroup:
43+
"""
44+
a group of data elements, German 'Datenelementgruppe' inside the AHB
45+
"""
46+
47+
# "Die Datenelementgruppe C0829 enthält mehrere Gruppendatenelemente. Diese Datenelementgruppe enthält das
48+
# Gruppendatenelement DE3039, hier wird die MP-ID angegeben, sowie das DE3055, welches die code-vergebende Stelle
49+
# definiert. Diese Datenelementgruppe enthält des Weiteren das Gruppendatenelement DE113110, welches nur in der MIG
50+
# und nicht im AHB aufgeführt wird, um den Aufbau der EDIFACT Nachricht korrekt umsetzen zu können."
51+
# Quelle: Allgemeine Festlegungen Kapitel 6.1, Seite 52
52+
53+
# Example:
54+
# <C_C002 Name="Dokumenten-/Nachrichtenname">
55+
# <D_1001 Name="Dokumentenname, Code">
56+
# <Code Name="Berechnungsformel" Description="" AHB_Status="X">Z36</Code>
57+
# </D_1001>
58+
# </C_C002>
59+
60+
id: str # e.g. 'C_C082'
61+
name: str # e.g. 'Dokumenten-/Nachrichtenname'
62+
data_elements: list[DataElement]
63+
64+
65+
@dataclass(frozen=True, eq=True, unsafe_hash=True, kw_only=True)
66+
class Segment:
67+
"""
68+
a segment inside an AHB
69+
"""
70+
71+
# Example:
72+
# pylint:disable=line-too-long
73+
# <S_BGM Name="Beginn der Nachricht" Number="00002" AHB_Status="Muss">
74+
# <C_C002 Name="Dokumenten-/Nachrichtenname">
75+
# <D_1001 Name="Dokumentenname, Code">
76+
# <Code Name="Berechnungsformel" Description="" AHB_Status="X">Z36</Code>
77+
# </D_1001>
78+
# </C_C002>
79+
# <C_C106 Name="Dokumenten-/Nachrichten-Identifikation">
80+
# <D_1004 Name="Dokumentennummer" AHB_Status="X"/>
81+
# </C_C106>
82+
# </S_BGM>
83+
id: str #: e.g. 'BGM'
84+
name: str #: e.g. 'Beginn der Nachricht'
85+
number: str #: e.g. '00002'
86+
ahb_status: str | None #: e.g. 'Muss'
87+
data_elements: list[DataElement | DataElementGroup]
88+
89+
90+
@dataclass(kw_only=True, eq=True, frozen=True)
91+
class SegmentGroup:
92+
"""
93+
a "Segmentgruppe" inside an AHB
94+
"""
95+
96+
# Example:
97+
# <G_SG6 Name="Prüfidentifikator" AHB_Status="Muss">
98+
# <S_RFF Name="Prüfidentifikator" Number="00012" AHB_Status="Muss">
99+
# <C_C506 Name="Referenz">
100+
# <D_1153 Name="Referenz, Qualifier">
101+
# <Code Name="Prüfidentifikator" Description="" AHB_Status="X">Z13</Code>
102+
# </D_1153>
103+
# <D_1154 Name="Referenz, Identifikation">
104+
# <Code Name="Berechnungsformel" Description="" AHB_Status="X">25001</Code>
105+
# </D_1154>
106+
# </C_C506>
107+
# </S_RFF>
108+
# </G_SG6>
109+
id: str #: e.g. 'SG6'
110+
name: str #: e.g. 'Prüfidentifikator'
111+
ahb_status: str | None #: e.g. 'Muss'
112+
segments: list[Segment]
113+
segment_groups: list["SegmentGroup"]
114+
115+
116+
@dataclass(kw_only=True, eq=True, frozen=True)
117+
class Anwendungsfall:
118+
"""One Anwendungsfall "AWF" corresponds to one Prüfidentifikator or Type of Message"""
119+
120+
# Example:
121+
# <AWF Pruefidentifikator="25001" Beschreibung="Berechnungsformel" Kommunikation_von="NB an MSB / LF">
122+
# <M_UTILTS>
123+
# <S_UNH Name="Nachrichten-Kopfsegment" Number="00001" AHB_Status="Muss">
124+
# ...
125+
# </S_UNT>
126+
# </M_UTILTS>
127+
# </AWF>
128+
pruefidentifikator: str #: e.g. '25001'
129+
beschreibung: str #: e.g. 'Berechnungsformel'
130+
kommunikation_von: str #: e.g. 'NB an MSB / LF'
131+
format: str #: e.g. 'UTILTS'
132+
segments: list[Segment]
133+
segment_groups: list[SegmentGroup]
134+
135+
136+
@dataclass(kw_only=True, eq=True, frozen=True)
137+
class Bedingung:
138+
"""Ein ConditionKeyConditionText Mapping"""
139+
140+
nummer: str #: e.g. '1'
141+
text: str #: e.g. 'Nur MP-ID aus Sparte Strom'
142+
143+
144+
@dataclass(kw_only=True, eq=True, frozen=True)
145+
class UbBedingung:
146+
"""Eine UB-Bedingung"""
147+
148+
# Example:
149+
# <UB_Bedingung Nummer="[UB1]">([931] ∧ [932] [490]) ⊻ ([931] ∧ [933] [491])</UB_Bedingung>
150+
nummer: str #: e.g. 'UB1'
151+
text: str #: e.g. '([931] ∧ [932] [490]) ⊻ ([931] ∧ [933] [491])'
152+
153+
154+
@dataclass(kw_only=True, eq=True, frozen=True)
155+
class Paket:
156+
"""Ein Bedingungspaket/PackageKeyConditionText Mapping"""
157+
158+
# Example:
159+
# <Paket Nummer="[1P]">--</Paket>
160+
nummer: str #: e.g. '1P'
161+
text: str #: e.g. '--'
162+
163+
164+
@dataclass(kw_only=True, eq=True, frozen=True)
165+
class Anwendungshandbuch:
166+
"""
167+
Ein Anwendungshandbuch bündelt verschiedene Nachrichtentypen/Anwendungsfälle im selben Format oder mit der selben
168+
regulatorischen Grundlage und stellt gemeinsame Pakete & Bedingungen bereit.
169+
"""
170+
171+
# Example:
172+
# <AHB Versionsnummer="1.1d" Veroeffentlichungsdatum="02.04.2024" Author="BDEW">
173+
# <AWF Pruefidentifikator="25001" Beschreibung="Berechnungsformel" Kommunikation_von="NB an MSB / LF">
174+
# </AWF>
175+
# </AHB>
176+
veroeffentlichungsdatum: date
177+
"""publishing date"""
178+
179+
autor: str
180+
"""author, most likely 'BDEW'"""
181+
182+
versionsnummer: str
183+
"""e.g. '1.1d'"""
184+
anwendungsfaelle: list[Anwendungsfall] #: die einzelnen Prüfidendifikatoren
185+
bedingungen: list[Bedingung]
186+
ub_bedingungen: list[UbBedingung]
187+
pakete: list[Paket]

src/fundamend/reader/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""classes for reading xml documents"""
22

3+
from .ahbreader import AhbReader
34
from .migreader import MigReader
45

5-
__all__ = ["MigReader"]
6+
__all__ = ["MigReader", "AhbReader"]

0 commit comments

Comments
 (0)