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

Issue:
Implement BO Region
https://www.bo4e.de/dokumentation/geschaeftsobjekte/bo-region
- #184 



Discussion / hints:

Fix patch:
diff --git a/src/bo4e/bo/region.py b/src/bo4e/bo/region.py
new file mode 100644
index 000000000..b2f4b9393
--- /dev/null
+++ b/src/bo4e/bo/region.py
@@ -0,0 +1,59 @@
+"""
+Contains Region class and corresponding marshmallow schema for de-/serialization
+"""
+from typing import List, Optional
+
+import attr
+from marshmallow import fields
+
+from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
+from bo4e.com.regionskriterium import Regionskriterium, RegionskriteriumSchema
+from bo4e.enum.botyp import BoTyp
+from bo4e.validators import check_list_length_at_least_one
+
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Region(Geschaeftsobjekt):
+    """
+    Modellierung einer Region als Menge von Kriterien, die eine Region beschreiben
+    """
+
+    # required attributes
+    bo_typ: BoTyp = attr.ib(default=BoTyp.REGION)
+    #: Bezeichnung der Region
+    bezeichnung: str = attr.ib(validator=attr.validators.instance_of(str))
+
+    #: Positivliste der Kriterien zur Definition der Region
+    positiv_liste: List[Regionskriterium] = attr.ib(
+        validator=attr.validators.deep_iterable(
+            member_validator=attr.validators.instance_of(Regionskriterium),
+            iterable_validator=check_list_length_at_least_one,
+        )
+    )
+
+    # optional attributes
+    #: Negativliste der Kriterien zur Definition der Region
+    negativ_liste: Optional[List[Regionskriterium]] = attr.ib(
+        default=None,
+        validator=attr.validators.optional(
+            attr.validators.deep_iterable(
+                member_validator=attr.validators.instance_of(Regionskriterium),
+                iterable_validator=attr.validators.instance_of(list),  # no min length for negativListe
+            )
+        ),
+    )
+
+
+class RegionSchema(GeschaeftsobjektSchema):
+    """
+    Schema for de-/serialization of Region
+    """
+
+    class_name = Region
+    # required attributes
+    bezeichnung = fields.Str()
+    positiv_liste = fields.List(fields.Nested(RegionskriteriumSchema))
+
+    # optional attributes
+    negativ_liste = fields.List(fields.Nested(RegionskriteriumSchema), load_default=None)
diff --git a/src/bo4e/enum/botyp.py b/src/bo4e/enum/botyp.py
index fd57e50b5..217e3c103 100644
--- a/src/bo4e/enum/botyp.py
+++ b/src/bo4e/enum/botyp.py
@@ -31,6 +31,7 @@ class BoTyp(StrEnum):
     PREISBLATTNETZNUTZUNG = "PREISBLATTNETZNUTZUNG"
     PREISBLATTUMLAGEN = "PREISBLATTUMLAGEN"
     RECHNUNG = "RECHNUNG"
+    REGION = "REGION"
     REGIONALTARIF = "REGIONALTARIF"
     STANDORTEIGENSCHAFTEN = "STANDORTEIGENSCHAFTEN"
     TARIFINFO = "TARIFINFO"


Test patch:
diff --git a/tests/test_region.py b/tests/test_region.py
new file mode 100644
index 000000000..70cc61ec7
--- /dev/null
+++ b/tests/test_region.py
@@ -0,0 +1,42 @@
+import pytest  # type:ignore[import]
+
+from bo4e.bo.region import Region, RegionSchema
+from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
+from tests.test_regionskriterium import example_regionskriterium  # type:ignore[import]
+
+
+class TestRegion:
+    @pytest.mark.parametrize(
+        "region",
+        [
+            pytest.param(
+                Region(
+                    bezeichnung="Bikini Bottom",
+                    positiv_liste=[example_regionskriterium],
+                    negativ_liste=[example_regionskriterium],
+                ),
+                id="max attributes",
+            ),
+            pytest.param(
+                Region(bezeichnung="Bikini Bottom", positiv_liste=[example_regionskriterium]), id="min attributes"
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, region: Region):
+        assert_serialization_roundtrip(region, RegionSchema())
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Region()
+
+        assert "missing 2 required" in str(excinfo.value)
+
+    def test_region_positiv_liste_required_and_negativ_liste_not_required(self):
+        with pytest.raises(ValueError) as excinfo:
+            _ = Region(
+                bezeichnung="Bikini Bottom",
+                positiv_liste=[],
+                negativ_liste=[],
+            )
+
+        assert "List positiv_liste must not be empty." in str(excinfo.value)
diff --git a/tests/test_regionskriterium.py b/tests/test_regionskriterium.py
index 8ada2874c..4ba2b1ab2 100644
--- a/tests/test_regionskriterium.py
+++ b/tests/test_regionskriterium.py
@@ -5,17 +5,19 @@
 from bo4e.enum.regionskriteriumtyp import Regionskriteriumtyp
 from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
 
+example_regionskriterium = Regionskriterium(
+    regionskriteriumtyp=Regionskriteriumtyp.REGELGEBIET_NAME,
+    gueltigkeitstyp=Gueltigkeitstyp.NICHT_IN,
+    wert="Was ist ein Regionskriterium?",
+)
+
 
 class TestRegionskriterium:
     @pytest.mark.parametrize(
         "regionskriterium, expected_json_dict",
         [
             pytest.param(
-                Regionskriterium(
-                    regionskriteriumtyp=Regionskriteriumtyp.REGELGEBIET_NAME,
-                    gueltigkeitstyp=Gueltigkeitstyp.NICHT_IN,
-                    wert="Was ist ein Regionskriterium?",
-                ),
+                example_regionskriterium,
                 {
                     "gueltigkeitstyp": "NICHT_IN",
                     "regionskriteriumtyp": "REGELGEBIET_NAME",