Skip to content

Commit 8fd818c

Browse files
authoredMar 1, 2024··
feat: retrieve lieferbeginn from ermittlungsauftrag (#11)
1 parent b7a01eb commit 8fd818c

9 files changed

+153
-6
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# specific requirements for the tox type_check environment
22
mypy
33
mypy[pydantic]
4+
types-pytz

‎dev_requirements/requirements-type_check.txt

+2
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ mypy[pydantic]==1.8.0
88
# via -r requirements-type_check.in
99
mypy-extensions==1.0.0
1010
# via mypy
11+
types-pytz==2024.1.0.20240203
12+
# via -r requirements-type_check.in
1113
typing-extensions==4.10.0
1214
# via mypy

‎domain-specific-terms.txt

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ prozess
77
autor
88
titel
99
offen
10+
ist

‎pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ classifiers = [
1919
dependencies = [
2020
"pydantic>=2.0.0",
2121
"aiohttp[speedups]>=3.9.3",
22-
"more-itertools"
22+
"more-itertools",
23+
"pytz"
2324
] # add all the dependencies here
2425
dynamic = ["readme", "version"]
2526

‎requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pydantic==2.6.2
3030
# via bssclient (pyproject.toml)
3131
pydantic-core==2.16.3
3232
# via pydantic
33+
pytz==2024.1
34+
# via bssclient (pyproject.toml)
3335
typing-extensions==4.10.0
3436
# via
3537
# pydantic

‎src/bssclient/models/ermittlungsauftrag.py

+21
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22
models for Ermittlungsauftrag/Investigation Order
33
"""
44

5+
from datetime import datetime
56
from typing import Literal
67
from uuid import UUID
8+
from xml.etree.ElementTree import Element
79

10+
import pytz
811
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, RootModel
912

1013
from bssclient.models.prozess import Prozess
1114

15+
_berlin = pytz.timezone("Europe/Berlin")
16+
1217

1318
class Notiz(BaseModel):
1419
"""
@@ -52,6 +57,22 @@ class Ermittlungsauftrag(BaseModel):
5257
kategorie: Literal["Ermittlungsauftrag"]
5358
prozess: Prozess
5459

60+
def get_vertragsbeginn_from_boneycomb_or_topcom(self) -> AwareDatetime:
61+
"""
62+
reads the vertragsbeginn from the boneycomb or topcom data (nested deep into the prozess)
63+
"""
64+
ausloeser = self.prozess.deserialized_ausloeser
65+
if isinstance(ausloeser, dict):
66+
return datetime.fromisoformat(ausloeser["transaktionsdaten"]["vertragsbeginn"])
67+
if isinstance(ausloeser, Element):
68+
date_str = ausloeser.find("lieferbeginn").text
69+
if date_str is None:
70+
raise ValueError("lieferbeginn not found in topcom XML")
71+
naive_date = datetime.strptime(date_str, "%Y-%m-%d")
72+
aware_date = _berlin.localize(naive_date) # everyone hates implicit timezones
73+
return pytz.utc.normalize(aware_date)
74+
raise NotImplementedError(f"ausloeser {ausloeser} is not implemented")
75+
5576

5677
class _ListOfErmittlungsauftraege(RootModel[list[Ermittlungsauftrag]]):
5778
pass

‎src/bssclient/models/prozess.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22
models for BSS Prozess
33
"""
44

5+
import json
6+
import logging
7+
import xml.etree.ElementTree as ET
58
from uuid import UUID
69

7-
from pydantic import BaseModel, Field
10+
from pydantic import BaseModel, ConfigDict, Field, computed_field
11+
12+
_logger = logging.getLogger(__name__)
813

914

1015
class Prozess(BaseModel):
1116
"""
1217
a bss prozess
1318
"""
1419

20+
model_config = ConfigDict(arbitrary_types_allowed=True)
21+
1522
id: UUID
1623
status: str
1724
status_text: str = Field(alias="statusText")
@@ -26,3 +33,16 @@ class Prozess(BaseModel):
2633
einheit: str | None = None
2734
messlokation: str | None = None
2835
zaehlernummer: str | None = None
36+
37+
@computed_field
38+
# @property
39+
def deserialized_ausloeser(self) -> dict | ET.Element:
40+
"""
41+
the self.ausloeser_daten is either a json (boneycomb) or a topcom XML.
42+
"""
43+
try:
44+
return json.loads(self.ausloeser_daten)
45+
except json.decoder.JSONDecodeError:
46+
# if the xml parsing throws an exception, too, I don't know what to do with it yet
47+
xml_element: ET.Element = ET.fromstring(self.ausloeser_daten)
48+
return xml_element
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
[
2+
{
3+
"einheitId": "66221516-01e8-4bb4-b6fe-f827d64bec05",
4+
"flatId": "0040507652",
5+
"lagezusatz": "Part.",
6+
"geschaeftspartner": null,
7+
"marktlokationId": "28617269885",
8+
"messlokationId": "DE0001112223334445556667778889991",
9+
"zaehlernummer": null,
10+
"vertragId": "01c39415-8973-4a25-9d32-c28ed8bab551",
11+
"lieferbeginn": "2020-04-17T22:00:00+00:00",
12+
"lieferende": "2022-05-17T22:00:00+00:00",
13+
"lieferadresse": {
14+
"timestamp": "2020-09-14T19:52:02.130486+00:00",
15+
"guid": "df6486f8-5f48-4087-8557-ac635cdd05af",
16+
"postleitzahl": "12119",
17+
"ort": "Berlin",
18+
"strasse": "Marktstr.",
19+
"hausnummer": "150",
20+
"postfach": null,
21+
"adresszusatz": null,
22+
"coErgaenzung": null,
23+
"landescode": "DE",
24+
"ortsteil": "Mitte",
25+
"versionStruktur": 1
26+
},
27+
"notizen": [
28+
{
29+
"autor": "System",
30+
"zeitpunkt": "2022-05-27T10:49:51.209224+00:00",
31+
"inhalt": "Der Ermittlungsauftrag wurde erstellt und geht in die Wartezeit über.",
32+
"timestamp": "2022-05-27T10:49:51.28446+00:00",
33+
"guid": "e64fb509-651c-42db-aa99-487954a4dc9a",
34+
"IstHistorisch": true,
35+
"historisch": true
36+
},
37+
{
38+
"autor": "System",
39+
"zeitpunkt": "2022-05-27T11:08:18.886305+00:00",
40+
"inhalt": "Der Ermittlungsauftrag konnte bisher nicht automatisiert gelöst werden und ist jetzt zur manuellen Bearbeitung freigegeben.",
41+
"timestamp": "2022-05-27T11:08:18.911074+00:00",
42+
"guid": "98b240b2-a5ea-4c5b-8c1f-f3919f27915d",
43+
"IstHistorisch": true,
44+
"historisch": true
45+
}
46+
],
47+
"verwalterName": null,
48+
"verwalterMobil": null,
49+
"verwalterId": "00000000-0000-0000-0000-000000000000",
50+
"id": "de63bd8c-42b4-42fb-9ba5-50ff7967c414",
51+
"agentId": null,
52+
"prozess": {
53+
"id": "b2790c31-5d68-4263-82ec-130167fb030a",
54+
"status": "Angelegt",
55+
"statusText": "Eigentuemer muss identifizert werden",
56+
"typ": "Leerstand",
57+
"ausloeser": "Queue",
58+
"externeId": "TC99999999",
59+
"marktlokation": "28617269885",
60+
"ideReferenz": "123458213203910IDEIDEIDEIASDASDASD",
61+
"transaktionsgrund": "Z36",
62+
"ausloeserDaten": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><lieferbeginnEOG><sparte>ELECTRICITY</sparte><externeID>TC99999999</externeID><nachrichtendatum>2020-09-30T12:00:00.000+02:00</nachrichtendatum><sender><MPID>9900123456789</MPID><MPIDcode>500</MPIDcode></sender><empfaenger><MPID>9987654321032</MPID><MPIDcode>500</MPIDcode></empfaenger><vorgang>123458213203910IDEIDEIDEIASDASDASD</vorgang><lieferbeginn>2020-04-18</lieferbeginn><bilanzierungsbeginn>2020-05-01</bilanzierungsbeginn><transaktionsgrund>Z36</transaktionsgrund><marktlokation><marktlokation>28617269885</marktlokation><geplanteTurnusablesung><wert>1011</wert><format>MMDD</format></geplanteTurnusablesung><geplanteTurnusablesung><wert>09041003</wert><format>MMWW-MMWW</format></geplanteTurnusablesung><turnusableseintervall>12</turnusableseintervall><netznutzungsabrechnung><wert>1011</wert><format>MMDD</format></netznutzungsabrechnung><netznutzungsabrechnung><wert>09041003</wert><format>MMWW-MMWW</format></netznutzungsabrechnung><naechstenetznutzungsabrechnung>2020</naechstenetznutzungsabrechnung><netznutzungsabrechnungsintervall>12</netznutzungsabrechnungsintervall><veranschlagtejahresmenge><wert>1310</wert><einheit>KWH</einheit></veranschlagtejahresmenge><lieferrichtung>Z07</lieferrichtung><uenb><MPID>1234567890123</MPID><MPIDcode>14</MPIDcode></uenb><msb><MPID>9922992299331</MPID><MPIDcode>500</MPIDcode></msb><regelzone>10YDE-EON------1</regelzone><bilanzkreis>11XSWASDLKMA</bilanzkreis><bilanzierungsgebiet>11ASDLKMAL-ASD</bilanzierungsgebiet><zeitreihentyp><kategorie>Z21</kategorie><code>SLS</code></zeitreihentyp><spannungsebene>E06</spannungsebene><messtechnischeeinordnung>Z53</messtechnischeeinordnung><aggregationsverantwortung>ZA8</aggregationsverantwortung><prognosegrundlage><typ>ZA6</typ><details>E02</details></prognosegrundlage><gruppenzuordnung>Z15</gruppenzuordnung><netznutzungsvertrag>Z09</netznutzungsvertrag><netznutzungszahler>Z11</netznutzungszahler><netznutzungsabrechnungsvariante>Z14</netznutzungsabrechnungsvariante><netznutzungsabrechnungsgrundlage>Z12</netznutzungsabrechnungsgrundlage><singulaergenutztebetriebsmittelnna>Z07</singulaergenutztebetriebsmittelnna><obisdaten><obis><kennzahl>1-1:1.9.0</kennzahl><schwachlastfaehig>Z59</schwachlastfaehig><verwendungszwecke><verwendungszweck>Z47</verwendungszweck><verwendungszweck>Z84</verwendungszweck><verwendungszweck>Z86</verwendungszweck></verwendungszwecke><verbrauchsart>Z64</verbrauchsart><unterbrechbarkeit>Z63</unterbrechbarkeit></obis></obisdaten><konzessionsabgabedaten><konzessionsabgabe><referenzObis>1-1:1.9.0</referenzObis><qual>Z08</qual><code>TA</code></konzessionsabgabe></konzessionsabgabedaten><gemeinderabatt>0.00</gemeinderabatt></marktlokation><messlokationen><messlokation><messlokation>DE0001112223334445556667778889991</messlokation><abrechnungmessstellenbetriebnna>JA</abrechnungmessstellenbetriebnna><msb><MPID>9922992299331</MPID><MPIDcode>500</MPIDcode></msb><spannungsebene>E06</spannungsebene><geraete><zaehleinrichtungsdaten><zaehleinrichtung><zaehlertyp>AHZ</zaehlertyp><zaehlernummer>1EYZAEHLER1234556</zaehlernummer><tarifanzahl>ETZ</tarifanzahl><energierichtung>ERZ</energierichtung><fernschaltung>Z07</fernschaltung><messwerteerfassung>MMR</messwerteerfassung></zaehleinrichtung></zaehleinrichtungsdaten></geraete><obisdaten><obis><kennzahl>1-1:1.8.0</kennzahl><referenzGeraetenummer>1EYZAEHLER1234556</referenzGeraetenummer><vorkommastellen>6</vorkommastellen><nachkommastellen>1</nachkommastellen><schwachlastfaehig>Z59</schwachlastfaehig></obis></obisdaten><lastprofil><klassentyp>Z02</klassentyp><verfahren>Z10</verfahren><profilcode>H0</profilcode></lastprofil><ablesekarten><ablesekarte><kundenTyp>Z02</kundenTyp><name>Mein Versorger GmbH</name><zusaetzlicheNamensangabe>Ersatzversorgung</zusaetzlicheNamensangabe><strasse>Stadtwerkgasse</strasse><hausnummer>2</hausnummer><plz>12007</plz><ort>Berlin</ort><ortsteil>Linden-Mitte</ortsteil><land>DE</land></ablesekarte></ablesekarten></messlokation></messlokationen><letztverbraucher><kundenTyp>Z02</kundenTyp><name>Mein Versorger GmbH</name><zusaetzlicheNamensangabe>Ersatzversorgung</zusaetzlicheNamensangabe></letztverbraucher><korrespondenzanschrift><kundenTyp>Z02</kundenTyp><name>Mein Versorger GmbH</name><zusaetzlicheNamensangabe>Ersatzversorgung</zusaetzlicheNamensangabe><strasse>Stadtwerkgasse</strasse><hausnummer>2</hausnummer><plz>12007</plz><ort>Berlin</ort><ortsteil>Linden-Mitte</ortsteil><land>DE</land></korrespondenzanschrift><lieferanschrift><strasse>Marktstr.</strasse><hausnummer>150</hausnummer><plz>12119</plz><ort>Berlin</ort><ortsteil>Mitte</ortsteil><land>DE</land></lieferanschrift></lieferbeginnEOG>",
63+
"datumErzeugt": "2020-09-30T10:04:00.80505+00:00",
64+
"datumGeaendert": "2021-04-22T09:38:28.029785+00:00",
65+
"datumBeendet": null,
66+
"antwortStatus": "E15",
67+
"einheit": null,
68+
"messlokation": "DE0001112223334445556667778889991",
69+
"zaehlernummer": "1EYZAEHLER1234556",
70+
"adresse": null
71+
},
72+
"verwandteAufgaben": [],
73+
"status": "Offen",
74+
"statusGrund": "",
75+
"bearbeiter": "System",
76+
"text": "Leerstand wartet auf Eigentümeridentifikation.",
77+
"datumErzeugt": "2022-05-27T10:49:51.198879+00:00",
78+
"datumGeaendert": "2022-05-27T10:49:51.239148+00:00",
79+
"datumAblauf": "2022-05-27T11:04:51.198925+00:00",
80+
"datumMaximaleFrist": "2020-05-28T22:00:00+00:00",
81+
"kategorie": "Ermittlungsauftrag",
82+
"aufgabenDaten": []
83+
}
84+
]

‎unittests/test_ermittlungsauftraege.py

+19-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from datetime import datetime, timezone
23
from pathlib import Path
34
from unittest.mock import AsyncMock, Mock
45

@@ -15,17 +16,31 @@ class TestErmittlungsauftraege:
1516

1617
async def test_get_ermittlungsauftraege(self, bss_client_with_default_auth):
1718
ermittlungsauftraege_json_file = Path(__file__).parent / "example_data" / "list_of_1_ermittlungsauftraege.json"
18-
with open(ermittlungsauftraege_json_file, "r", encoding="utf-8") as infile:
19-
ermittlungsauftraege = json.load(infile)
19+
ermittlungsauftraege_json_file2 = (
20+
Path(__file__).parent / "example_data" / "list_of_1_ermittlungsauftrag_from_topcom.json"
21+
)
22+
with (
23+
open(ermittlungsauftraege_json_file, "r", encoding="utf-8") as infile1,
24+
open(ermittlungsauftraege_json_file2, "r", encoding="utf-8") as infile2,
25+
):
26+
ermittlungsauftraege = json.load(infile1) + json.load(infile2)
2027
client, bss_config = bss_client_with_default_auth
2128
with aioresponses() as mocked_bss:
2229
mocked_get_url = (
23-
f"{bss_config.server_url}api/Aufgabe/ermittlungsauftraege?includeDetails=true&limit=1&offset=0"
30+
f"{bss_config.server_url}api/Aufgabe/ermittlungsauftraege?includeDetails=true&limit=2&offset=0"
2431
)
2532
mocked_bss.get(mocked_get_url, status=200, payload=ermittlungsauftraege)
26-
actual = await client.get_ermittlungsauftraege(limit=1)
33+
actual = await client.get_ermittlungsauftraege(limit=2)
2734
assert isinstance(actual, list)
35+
assert len(actual) == 2
2836
assert all(isinstance(x, Ermittlungsauftrag) for x in actual)
37+
assert isinstance(actual[0].prozess.deserialized_ausloeser, dict)
38+
assert actual[0].get_vertragsbeginn_from_boneycomb_or_topcom() == datetime(
39+
2023, 10, 31, 23, 0, 0, tzinfo=timezone.utc
40+
)
41+
assert actual[1].get_vertragsbeginn_from_boneycomb_or_topcom() == datetime(
42+
2020, 4, 17, 22, 0, tzinfo=timezone.utc
43+
)
2944

3045
async def test_get_stats(self, bss_client_with_default_auth):
3146
stats_json_file = Path(__file__).parent / "example_data" / "aufgabe_stats.json"

0 commit comments

Comments
 (0)
Please sign in to comment.