Repository: bo4e/BO4E-python (language: python)

Issue:
Replace jsons with marshmallow to realise the serialisation
jsons did a great job at the beginning, but failed in the interaction with sqlalchemy.
Therefor we will use marshmallow for the serialisation.


Discussion / hints:

Fix patch:
diff --git a/docs/api/bo4e.enum.rst b/docs/api/bo4e.enum.rst
index aac41a0eb..f11a193fb 100644
--- a/docs/api/bo4e.enum.rst
+++ b/docs/api/bo4e.enum.rst
@@ -52,10 +52,10 @@ bo4e.enum.gasqualitaet module
    :undoc-members:
    :show-inheritance:
 
-bo4e.enum.gebietstyp module
----------------------------
+bo4e.enum.gebiettyp module
+--------------------------
 
-.. automodule:: bo4e.enum.gebietstyp
+.. automodule:: bo4e.enum.gebiettyp
    :members:
    :undoc-members:
    :show-inheritance:
diff --git a/src/bo4e/bo/geschaeftsobjekt.py b/src/bo4e/bo/geschaeftsobjekt.py
index e06a2e39e..41795cbf2 100644
--- a/src/bo4e/bo/geschaeftsobjekt.py
+++ b/src/bo4e/bo/geschaeftsobjekt.py
@@ -16,12 +16,12 @@ class Geschaeftsobjekt:
     """
 
     # required attributes
-    versionstruktur: int = attr.ib(default=2)
-    bo_typ: BoTyp
+    versionstruktur: str = attr.ib(default="2")
+    bo_typ: BoTyp = attr.ib(default=BoTyp.GESCHAEFTSOBJEKT)
 
     # optional attributes
     externe_referenzen: Optional[List[ExterneReferenz]] = attr.ib(
-        default=None, validator=attr.validators.instance_of((type(None), List))
+        default=[], validator=attr.validators.instance_of(List)
     )
 
 
@@ -35,7 +35,7 @@ class GeschaeftsobjektSchema(Schema, JavaScriptMixin):
     class_name = Geschaeftsobjekt
 
     # required attributes
-    versionstruktur = fields.Integer()
+    versionstruktur = fields.String()
     bo_typ = EnumField(BoTyp)
 
     # optional attributes
diff --git a/src/bo4e/bo/geschaeftspartner.py b/src/bo4e/bo/geschaeftspartner.py
index 1c10775aa..456f35988 100644
--- a/src/bo4e/bo/geschaeftspartner.py
+++ b/src/bo4e/bo/geschaeftspartner.py
@@ -2,6 +2,7 @@
 
 from marshmallow import fields
 from marshmallow_enum import EnumField
+from typing import List
 
 from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
 from bo4e.com.adresse import Adresse, AdresseSchema
@@ -21,7 +22,7 @@ class Geschaeftspartner(Geschaeftsobjekt):
     bo_typ: BoTyp = attr.ib(default=BoTyp.GESCHAEFTSPARTNER)
     name1: str
     gewerbekennzeichnung: bool
-    geschaeftspartnerrolle: Geschaeftspartnerrolle
+    geschaeftspartnerrolle: List[Geschaeftspartnerrolle]
     partneradresse: Adresse
 
     # optional attributes
@@ -30,7 +31,7 @@ class Geschaeftspartner(Geschaeftsobjekt):
     name3: str = attr.ib(default=None)
     hrnummer: str = attr.ib(default=None)
     amtsgericht: str = attr.ib(default=None)
-    kontaktweg: Kontaktart = attr.ib(default=None)
+    kontaktweg: List[Kontaktart] = attr.ib(default=[])
     umsatzsteuer_id: str = attr.ib(default=None)
     glaeubiger_id: str = attr.ib(default=None)
     e_mail_adresse: str = attr.ib(default=None)
@@ -45,7 +46,7 @@ class GeschaeftspartnerSchema(GeschaeftsobjektSchema):
     # required attributes
     name1 = fields.Str()
     gewerbekennzeichnung = fields.Bool()
-    geschaeftspartnerrolle = EnumField(Geschaeftspartnerrolle)
+    geschaeftspartnerrolle = fields.List(EnumField(Geschaeftspartnerrolle))
     partneradresse = fields.Nested(AdresseSchema)
 
     # optional attributes
@@ -54,7 +55,7 @@ class GeschaeftspartnerSchema(GeschaeftsobjektSchema):
     name3 = fields.Str(missing=None)
     hrnummer = fields.Str(missing=None)
     amtsgericht = fields.Str(missing=None)
-    kontaktweg = EnumField(Kontaktart, missing=None)
+    kontaktweg = fields.List(EnumField(Kontaktart), missing=None)
     umsatzsteuer_id = fields.Str(missing=None)
     glaeubiger_id = fields.Str(missing=None)
     e_mail_adresse = fields.Str(missing=None)
diff --git a/src/bo4e/bo/marktlokation.py b/src/bo4e/bo/marktlokation.py
index 8c38b1cab..146edc788 100644
--- a/src/bo4e/bo/marktlokation.py
+++ b/src/bo4e/bo/marktlokation.py
@@ -19,7 +19,7 @@
 from bo4e.enum.bilanzierungsmethode import Bilanzierungsmethode
 from bo4e.enum.verbrauchsart import Verbrauchsart
 from bo4e.enum.netzebene import Netzebene
-from bo4e.enum.gebietstyp import Gebietstyp
+from bo4e.enum.gebiettyp import Gebiettyp
 from bo4e.enum.gasqualitaet import Gasqualitaet
 
 _malo_id_pattern = re.compile(r"^[1-9][\d]{10}$")
@@ -57,7 +57,7 @@ def _validate_marktlokations_id(self, marktlokations_id_attribute, value):
     verbrauchsart: Verbrauchsart = attr.ib(default=None)
     unterbrechbar: bool = attr.ib(default=None)
     netzbetreibercodenr: str = attr.ib(default=None)
-    gebietstyp: Gebietstyp = attr.ib(default=None)
+    gebietstyp: Gebiettyp = attr.ib(default=None)
     netzgebietsnr: str = attr.ib(default=None)
     bilanzierungsgebiet: str = attr.ib(default=None)
     grundversorgercodenr: str = attr.ib(default=None)
@@ -127,7 +127,7 @@ class MarktlokationSchema(GeschaeftsobjektSchema):
     verbrauchsart = EnumField(Verbrauchsart, missing=None)
     unterbrechbar = fields.Bool(missing=None)
     netzbetreibercodenr = fields.Str(missing=None)
-    gebietstyp = EnumField(Gebietstyp, missing=None)
+    gebietstyp = EnumField(Gebiettyp, missing=None)
     netzgebietsnr = fields.Str(missing=None)
     bilanzierungsgebiet = fields.Str(missing=None)
     grundversorgercodenr = fields.Str(missing=None)
diff --git a/src/bo4e/enum/botyp.py b/src/bo4e/enum/botyp.py
index 97d72bc9c..e01754cdb 100644
--- a/src/bo4e/enum/botyp.py
+++ b/src/bo4e/enum/botyp.py
@@ -5,28 +5,32 @@
 from enum import Enum
 
 _boTyp = {
-    "MARKTLOKATION": "MARKTLOKATION",
-    "MESSLOKATION": "MESSLOKATION",
-    "GESCHAEFTSPARTNER": "GESCHAEFTSPARTNER",
-    "ZAEHLER": "ZAEHLER",
-    "ANSPRECHPARTNER": "ANSPRECHPARTNER",
     "ANGEBOT": "ANGEBOT",
-    "MARKTTEILNEHMER": "MARKTTEILNEHMER",
+    "ANSPRECHPARTNER": "ANSPRECHPARTNER",
+    "AUSSCHREIBUNG": "AUSSCHREIUNG",
     "ENERGIEMENGE": "ENERGIEMENGE",
+    # It is practical to use the BoTyp Enum as discriminator in the database.
+    # Therefore we added one additional entry for GESCHAEFTSOBJEKT
+    # This is not defined by the documentation!
+    "GESCHAEFTSOBJEKT": "GESCHAEFTSOBJEKT",
+    "GESCHAEFTSPARTNER": "GESCHAEFTSPARTNER",
     "KOSTEN": "KOSTEN",
-    "AUSSCHREIBUNG": "AUSSCHREIUNG",
+    "MARKTLOKATION": "MARKTLOKATION",
+    "MESSLOKATION": "MESSLOKATION",
+    "MARKTTEILNEHMER": "MARKTTEILNEHMER",
+    "NETZNUTZUNGSRECHNUNG": "NETZNUTZUNGSRECHNUNG",
     "PREISBLATT": "PREISBLATT",
-    "PREISBLATTNETZNUTZUNG": "PREISBLATTNETZNUTZUNG",
-    "PREISBLATTMESSUNG": "PREISBLATTMESSUNG",
     "PREISBLATTDIENSTLEISTUNG": "PREISBLATTDIENSTLEISTUNG",
-    "PREISBLATTUMLAGEN": "PREISBLATTUMLAGEN",
     "PREISBLATTKONZESSIONSABGABE": "PREISBLATTKONZESSIONSABGABE",
+    "PREISBLATTMESSUNG": "PREISBLATTMESSUNG",
+    "PREISBLATTNETZNUTZUNG": "PREISBLATTNETZNUTZUNG",
+    "PREISBLATTUMLAGEN": "PREISBLATTUMLAGEN",
+    "RECHNUNG": "RECHNUNG",
+    "REGIONALTARIF": "REGIONALTARIF",
     "TARIFINFO": "TARIFINFO",
     "TARIFKOSTEN": "TARIFKOSTEN",
-    "RECHNUNG": "RECHNUNG",
-    "NETZNUTZUNGSRECHNUNG": "NETZNUTZUNGSRECHNUNG",
     "TARIFPREISBLATT": "TARIFPREISBLATT",
-    "REGIONALTARIF": "REGIONALTARIF",
-    "ZEITREIHE": "ZEITREIHE"
+    "ZAEHLER": "ZAEHLER",
+    "ZEITREIHE": "ZEITREIHE",
 }
 BoTyp = Enum("BoTyp", _boTyp)
diff --git a/src/bo4e/enum/gebietstyp.py b/src/bo4e/enum/gebiettyp.py
similarity index 80%
rename from src/bo4e/enum/gebietstyp.py
rename to src/bo4e/enum/gebiettyp.py
index 3b0fcb80b..d42baedb8 100644
--- a/src/bo4e/enum/gebietstyp.py
+++ b/src/bo4e/enum/gebiettyp.py
@@ -1,9 +1,9 @@
 """
-List of possible Gebietstypen.
+List of possible Gebiettypen.
 """
 from enum import Enum
 
-_gebietstyp = {
+_gebiettyp = {
     "REGELZONE": "REGELZONE",
     "MARKTGEBIET": "MARKTGEBIET",
     "BILANZIERUNGSGEBIET": "BILANZIERUNGSGEBIET",
@@ -14,4 +14,4 @@
     "GRUNDVERSORGUNGSGEBIET": "GRUNDVERSORGUNGSGEBIET",
     "VERSORGUNGSGEBIET": "VERSORGUNGSGEBIET",
 }
-Gebietstyp = Enum("Gebietstyp", _gebietstyp)
+Gebiettyp = Enum("Gebiettyp", _gebiettyp)
diff --git a/src/bo4e/enum/marktrolle.py b/src/bo4e/enum/marktrolle.py
index 666f76f5e..c23effb65 100644
--- a/src/bo4e/enum/marktrolle.py
+++ b/src/bo4e/enum/marktrolle.py
@@ -4,14 +4,14 @@
 from enum import Enum
 
 _marktrolle = {
-    "VNB": "NB",  # Grid Operator
+    "NB": "NB",  # Grid Operator
     "LF": "LF",  # Supplier
     "MSB": "MSB",  # Messstellenbetreiber
-    "DIENSTLEISTER": "DL",  # Dienstleister
+    "DL": "DL",  # Dienstleister
     "BKV": "BKV",  # Bilanzkreisverantwortlicher,
-    "BIKO": "BKV",  # Bilanzkoordinator / Marktgebietsverantwortlicher
+    "BKO": "BKO",  # Bilanzkoordinator / Marktgebietsverantwortlicher
     "UENB": "UENB",  # Übertragungsnetzbetreiber
-    "KUNDE_NN_SELBST": "KUNDE-NN-SELBST",  # Kunden, die Netznutzungsentgelte selbst zahlen
+    "KUNDE_SELBST_NN": "KUNDE_SELBST_NN",  # Kunden, die Netznutzungsentgelte selbst zahlen
     "MGV": "MGV",  # Marktgebietsverantwortlicher
     "EIV": "EIV",  # Einsatzverantwortlicher
     "RB": "RB",  # Registerbetreiber


Test patch:
diff --git a/tests/test_externe_referenz.py b/tests/test_externe_referenz.py
index dd17cb8b4..5b70fa362 100644
--- a/tests/test_externe_referenz.py
+++ b/tests/test_externe_referenz.py
@@ -28,7 +28,7 @@ def test_list_of_externe_referenz(self):
             name1="Duck",
             name2="Donald",
             gewerbekennzeichnung=False,
-            geschaeftspartnerrolle=Geschaeftspartnerrolle.KUNDE,
+            geschaeftspartnerrolle=[Geschaeftspartnerrolle.KUNDE],
             partneradresse=Adresse(
                 strasse="Am Geldspeicher",
                 hausnummer="17",
@@ -51,7 +51,7 @@ def test_geschaeftspartner_with_no_externe_referenz(self):
             name1="Duck",
             name2="Donald",
             gewerbekennzeichnung=False,
-            geschaeftspartnerrolle=Geschaeftspartnerrolle.KUNDE,
+            geschaeftspartnerrolle=[Geschaeftspartnerrolle.KUNDE],
             partneradresse=Adresse(
                 strasse="Am Geldspeicher",
                 hausnummer="17",
@@ -65,4 +65,5 @@ def test_geschaeftspartner_with_no_externe_referenz(self):
         gp_json = schema.dumps(gp, ensure_ascii=False)
 
         deserialized_gp: Geschaeftspartner = schema.loads(gp_json)
-        assert deserialized_gp.externe_referenzen is None
+
+        assert deserialized_gp.externe_referenzen == []
diff --git a/tests/test_geschaeftsobjekt.py b/tests/test_geschaeftsobjekt.py
index 2771ff388..434cb786c 100644
--- a/tests/test_geschaeftsobjekt.py
+++ b/tests/test_geschaeftsobjekt.py
@@ -11,7 +11,7 @@ class TestGeschaeftsobjet:
         [
             (
                 BoTyp.ENERGIEMENGE,
-                2,
+                "2",
                 [
                     ExterneReferenz(
                         ex_ref_name="HOCHFREQUENZ_HFSAP_100", ex_ref_wert="12345"
@@ -23,14 +23,14 @@ class TestGeschaeftsobjet:
             ),
             (
                 BoTyp.ENERGIEMENGE,
-                2,
+                "2",
                 [
                     ExterneReferenz(
                         ex_ref_name="HOCHFREQUENZ_HFSAP_100", ex_ref_wert="12345"
                     )
                 ],
             ),
-            (BoTyp.ENERGIEMENGE, 2, None),
+            (BoTyp.ENERGIEMENGE, "2", []),
         ],
     )
     def test_serialisation(
@@ -58,8 +58,8 @@ def test_serialisation(
     def test_initialization_with_minimal_attributs(self):
         go = Geschaeftsobjekt(bo_typ=BoTyp.ANSPRECHPARTNER)
 
-        assert go.externe_referenzen is None
-        assert go.versionstruktur == 2
+        assert go.externe_referenzen == []
+        assert go.versionstruktur == "2"
 
     def test_no_list_in_externen_referenzen(self):
         with pytest.raises(TypeError) as excinfo:
@@ -69,4 +69,4 @@ def test_no_list_in_externen_referenzen(self):
                     ex_ref_name="Schufa-ID", ex_ref_wert="aksdlakoeuhn"
                 ),
             )
-        assert "must be (<class 'NoneType'>, typing.List)" in str(excinfo.value)
+        assert "must be typing.List" in str(excinfo.value)
diff --git a/tests/test_geschaeftspartner.py b/tests/test_geschaeftspartner.py
index fab75cc06..de6411c3c 100644
--- a/tests/test_geschaeftspartner.py
+++ b/tests/test_geschaeftspartner.py
@@ -28,12 +28,12 @@ def test_serializable(self, datafiles):
             gewerbekennzeichnung=True,
             hrnummer="HRB 254466",
             amtsgericht="Amtsgericht München",
-            kontaktweg=Kontaktart.E_MAIL,
+            kontaktweg=[Kontaktart.E_MAIL],
             umsatzsteuer_id="DE267311963",
             glaeubiger_id="DE98ZZZ09999999999",
             e_mail_adresse="test@bo4e.de",
             website="bo4e.de",
-            geschaeftspartnerrolle=Geschaeftspartnerrolle.DIENSTLEISTER,
+            geschaeftspartnerrolle=[Geschaeftspartnerrolle.DIENSTLEISTER],
             partneradresse=Adresse(
                 postleitzahl=address_test_data["postleitzahl"],
                 ort=address_test_data["ort"],
diff --git a/tests/test_marktlokation.py b/tests/test_marktlokation.py
index b5e43d4c9..a6b49ae90 100644
--- a/tests/test_marktlokation.py
+++ b/tests/test_marktlokation.py
@@ -31,7 +31,7 @@ def test_serialisation_only_required_attributes(self):
             bilanzierungsmethode=Bilanzierungsmethode.PAUSCHAL,
             netzebene=Netzebene.NSP,
         )
-        assert malo.versionstruktur == 2, "versionstruktur was not automatically set"
+        assert malo.versionstruktur == "2", "versionstruktur was not automatically set"
         assert malo.bo_typ is BoTyp.MARKTLOKATION, "boTyp was not automatically set"
 
         schema = MarktlokationSchema()
@@ -62,12 +62,12 @@ def test_serialization_required_and_optional_attributes(self):
             gewerbekennzeichnung=True,
             hrnummer="HRB 254466",
             amtsgericht="Amtsgericht München",
-            kontaktweg=Kontaktart.E_MAIL,
+            kontaktweg=[Kontaktart.E_MAIL],
             umsatzsteuer_id="DE267311963",
             glaeubiger_id="DE98ZZZ09999999999",
             e_mail_adresse="test@bo4e.de",
             website="bo4e.de",
-            geschaeftspartnerrolle=Geschaeftspartnerrolle.DIENSTLEISTER,
+            geschaeftspartnerrolle=[Geschaeftspartnerrolle.DIENSTLEISTER],
             partneradresse=Adresse(
                 postleitzahl="82031",
                 ort="Grünwald",
@@ -87,7 +87,7 @@ def test_serialization_required_and_optional_attributes(self):
             netzebene=Netzebene.NSP,
             endkunde=gp,
         )
-        assert malo.versionstruktur == 2, "versionstruktur was not automatically set"
+        assert malo.versionstruktur == "2", "versionstruktur was not automatically set"
         assert malo.bo_typ == BoTyp.MARKTLOKATION, "boTyp was not automatically set"
 
         schema = MarktlokationSchema()
@@ -138,7 +138,7 @@ def test_missing_required_fields(self):
             "bilanzierungsgebiet": null,
             "geoadresse": null,
             "bilanzierungsmethode": "PAUSCHAL",
-            "versionstruktur": 2,
+            "versionstruktur": "2",
             "energierichtung": "EINSP"
             }"""
 
diff --git a/tests/test_marktteilnehmer.py b/tests/test_marktteilnehmer.py
index 73bac9ccc..5cbc9489d 100644
--- a/tests/test_marktteilnehmer.py
+++ b/tests/test_marktteilnehmer.py
@@ -12,13 +12,13 @@ class TestMarktteilnehmer:
     def test_serialization(self):
         mt = Marktteilnehmer(
             # required attributes of Marktteilnehmer only
-            marktrolle=Marktrolle.DIENSTLEISTER,
+            marktrolle=Marktrolle.DL,
             rollencodenummer="9903916000000",
             rollencodetyp=Rollencodetyp.BDEW,
             # required attributes inherited from Geschaeftspartner
             name1="Netze BW GmbH",
             gewerbekennzeichnung=True,
-            geschaeftspartnerrolle=Geschaeftspartnerrolle.DIENSTLEISTER,
+            geschaeftspartnerrolle=[Geschaeftspartnerrolle.DIENSTLEISTER],
             partneradresse=Adresse(
                 strasse="Schelmenwasenstraße",
                 hausnummer="15",
@@ -27,7 +27,7 @@ def test_serialization(self):
             ),
         )
 
-        assert mt.versionstruktur == 2, "versionstruktur was not automatically set"
+        assert mt.versionstruktur == "2", "versionstruktur was not automatically set"
         assert mt.bo_typ == BoTyp.MARKTTEILNEHMER, "boTyp was not automatically set"
 
         schema = MarktteilnehmerSchema()