Skip to content

Commit

Permalink
refactor(model): Use pydantic always and explicitly (#75)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: DeltaDaniel <[email protected]>
Co-authored-by: konstantin <[email protected]>
  • Loading branch information
3 people authored Jan 22, 2025
1 parent 20f1f75 commit de261cf
Show file tree
Hide file tree
Showing 9 changed files with 48 additions and 72 deletions.
4 changes: 0 additions & 4 deletions .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ jobs:
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
pydantic: [install_pydantic, skip_pydantic]
cli: [install_typer, skip_typer]
os: [ubuntu-latest]
steps:
Expand All @@ -28,9 +27,6 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install tox
- name: install pydantic if requested
if: matrix.run_step == 'install_pydantic'
run: pip install .[pydantic]
- name: install typer if requested
if: matrix.run_step == 'install_typer'
run: pip install .[cli]
Expand Down
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,8 @@ Die vollständigen Beispiele finden sich in den [unittests](unittests):
- Beispiel [AHB UTILTS](unittests/example_ahb_utilts_11d.py)
- Beispiel [MIG UTILTS](https://github.com/Hochfrequenz/xml-fundamend-python/blob/main/unittests/example_migs.py)

### Verwendung mit Pydantic
Per default verwendet fundamend die [dataclasses aus der Python-Standardlibrary](https://docs.python.org/3/library/dataclasses.html).
Es lässt sich aber auch direkt mit [Pydantic](https://docs.pydantic.dev/latest/) und den [Pydantic dataclasses](https://docs.pydantic.dev/2.7/concepts/dataclasses/) verwenden.
Wenn entweder pydantic schon installiert ist, oder mittels
```bash
pip install fundamend[pydantic]
```
mit installiert wird, dann sind Datenmodelle, die von `AhbReader` und `MigReader` zurückgegeben werden, automatisch pydantic Objekte.
### Pydantic
Die Datenmodelle, die von `AhbReader` und `MigReader` zurückgegeben werden, sind pydantic Objekte.

Mit Pydantic können die Ergebnisse auch leicht bspw. als JSON exportiert werden (was auch über ein CLI-Tool im nächsten Abschnitt) noch einfacher möglich ist.
```python
Expand Down
9 changes: 3 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [] # add all the fundamend dependencies here, None so far
dependencies = [
"pydantic>=2"
]
dynamic = ["readme", "version"]

[project.optional-dependencies]
Expand All @@ -31,12 +33,7 @@ formatting = [
linting = [
"pylint==3.3.3"
]
pydantic = [
"pydantic>=2"
# if you install fundamend[pydantic], the dataclasses from pydantic will be used
]
cli = [
"fundamend[pydantic]",
"typer" # if you install fundamend[cli], the cli commands are available via typer
]
spellcheck = [
Expand Down
12 changes: 11 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,15 @@
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile pyproject.toml
# pip-compile '.\pyproject.toml'
#
annotated-types==0.7.0
# via pydantic
pydantic==2.10.5
# via fundamend (pyproject.toml)
pydantic-core==2.27.2
# via pydantic
typing-extensions==4.12.2
# via
# pydantic
# pydantic-core
14 changes: 0 additions & 14 deletions src/fundamend/models/_dataclass_wrapper.py

This file was deleted.

32 changes: 11 additions & 21 deletions src/fundamend/models/anwendungshandbuch.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
from datetime import date
from typing import Union

from ._dataclass_wrapper import dataclass
from fundamend.models.base import FundamendBaseModel


@dataclass(kw_only=True, eq=True, frozen=True)
class Code:
class Code(FundamendBaseModel):
"""
A single code element inside an AHB DataElement, indicated by the `<Code>` tag.
"""
Expand All @@ -24,8 +23,7 @@ class Code:
ahb_status: str #: e.g. 'X' # new for AHB


@dataclass(kw_only=True, eq=True, frozen=True)
class DataElement:
class DataElement(FundamendBaseModel):
"""
A single data element, German 'Datenelement' inside an AHB Segment, indicated by the `<D_xxxx>` tag.
This element can contain a single or multiple Code elements.
Expand All @@ -40,8 +38,7 @@ class DataElement:
codes: list[Code]


@dataclass(eq=True, kw_only=True, frozen=True)
class DataElementGroup:
class DataElementGroup(FundamendBaseModel):
"""
A group of data elements, German 'Datenelementgruppe' inside the AHB, indicated by the `<C_xxxx>` tag.
This model can contain both the 'Datenelement' and the 'Gruppendatenelement'
Expand All @@ -65,8 +62,7 @@ class DataElementGroup:
data_elements: list[DataElement]


@dataclass(frozen=True, eq=True, unsafe_hash=True, kw_only=True)
class Segment:
class Segment(FundamendBaseModel):
"""
A segment inside an AHB, indicated by the `<S_xxxx>` tag.
This model can contain both data elements and data element groups.
Expand All @@ -91,8 +87,7 @@ class Segment:
data_elements: list[DataElement | DataElementGroup]


@dataclass(kw_only=True, eq=True, frozen=True)
class SegmentGroup:
class SegmentGroup(FundamendBaseModel):
"""
A 'Segmentgruppe' inside an AHB, indicated by the `<G_xxxx>` tag.
This model can contain both Segments and segment groups.
Expand All @@ -117,8 +112,7 @@ class SegmentGroup:
elements: list[Union[Segment, "SegmentGroup"]]


@dataclass(kw_only=True, eq=True, frozen=True)
class Anwendungsfall:
class Anwendungsfall(FundamendBaseModel):
"""
One 'Anwendungsfall', indicated by `<AWF>` tag, corresponds to one Prüfidentifikator or type of Message
"""
Expand All @@ -138,16 +132,14 @@ class Anwendungsfall:
elements: list[Union[Segment, SegmentGroup]]


@dataclass(kw_only=True, eq=True, frozen=True)
class Bedingung:
class Bedingung(FundamendBaseModel):
"""Ein ConditionKeyConditionText Mapping"""

nummer: str #: e.g. '1'
text: str #: e.g. 'Nur MP-ID aus Sparte Strom'


@dataclass(kw_only=True, eq=True, frozen=True)
class UbBedingung:
class UbBedingung(FundamendBaseModel):
"""Eine UB-Bedingung"""

# Example:
Expand All @@ -156,8 +148,7 @@ class UbBedingung:
text: str #: e.g. '([931] ∧ [932] [490]) ⊻ ([931] ∧ [933] [491])'


@dataclass(kw_only=True, eq=True, frozen=True)
class Paket:
class Paket(FundamendBaseModel):
"""Ein Bedingungspaket/PackageKeyConditionText Mapping"""

# Example:
Expand All @@ -166,8 +157,7 @@ class Paket:
text: str #: e.g. '--'


@dataclass(kw_only=True, eq=True, frozen=True)
class Anwendungshandbuch:
class Anwendungshandbuch(FundamendBaseModel):
"""
Ein Anwendungshandbuch, indicated by the `<AHB` tag, bündelt verschiedene Nachrichtentypen/Anwendungsfälle im
selben Format oder mit der selben regulatorischen Grundlage und stellt gemeinsame Pakete & Bedingungen bereit.
Expand Down
13 changes: 13 additions & 0 deletions src/fundamend/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
Base class for all models in the fundamend package.
"""

from pydantic import BaseModel, ConfigDict


class FundamendBaseModel(BaseModel):
"""
Base class for all models in the fundamend package. Defines all models as frozen.
"""

model_config = ConfigDict(frozen=True)
20 changes: 7 additions & 13 deletions src/fundamend/models/messageimplementationguide.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from enum import StrEnum
from typing import Union

from ._dataclass_wrapper import dataclass
from .base import FundamendBaseModel

# I didn't invent the data model ;)
# pylint:disable=too-many-instance-attributes
Expand All @@ -23,8 +23,7 @@ class MigStatus(StrEnum):
O = "O"


@dataclass(kw_only=True, eq=True)
class Code:
class Code(FundamendBaseModel):
"""
A single code element inside a MIG data element, indicated by the `<Code>` tag.
"""
Expand All @@ -36,8 +35,7 @@ class Code:
value: str | None # e.g. 'UTILTS'


@dataclass(kw_only=True, eq=True)
class DataElement:
class DataElement(FundamendBaseModel):
"""
A single data element inside a MIG Segment.
This models both the 'Datenelement' and the 'Gruppendatenelement', indicated by the `<D_xxxx` tag.
Expand All @@ -59,8 +57,7 @@ class DataElement:
codes: list[Code]


@dataclass(eq=True, kw_only=True)
class DataElementGroup:
class DataElementGroup(FundamendBaseModel):
"""
A group of data elements, German 'Datenelementgruppe', indicated by the `<C_xxxx>` tag.
Are able to contain a single or multiple data elements.
Expand Down Expand Up @@ -91,8 +88,7 @@ class DataElementGroup:
data_elements: list[DataElement]


@dataclass(frozen=True, eq=True, order=True, unsafe_hash=True, kw_only=True)
class Segment:
class Segment(FundamendBaseModel):
"""
A segment inside a MIG, indicated by the `<S_xxxx>` tag. A segment contains data elements and data element groups.
"""
Expand Down Expand Up @@ -126,8 +122,7 @@ class Segment:
data_elements: list[DataElement | DataElementGroup]


@dataclass(kw_only=True, eq=True)
class SegmentGroup:
class SegmentGroup(FundamendBaseModel):
"""
A 'Segmentgruppe' inside a MIG, indicated by the `<G_xxx>` tag. A segment contains segments and segments groups.
"""
Expand Down Expand Up @@ -160,8 +155,7 @@ class SegmentGroup:
elements: list[Union[Segment, "SegmentGroup"]]


@dataclass(kw_only=True, eq=True)
class MessageImplementationGuide:
class MessageImplementationGuide(FundamendBaseModel):
"""
message implementation guide (MIG)
"""
Expand Down
6 changes: 1 addition & 5 deletions unittests/test_pydantic_features.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from pathlib import Path

import pytest

try:
from pydantic import RootModel, TypeAdapter
except ImportError:
pytest.skip("Only available with pydantic", allow_module_level=True)
from pydantic import RootModel, TypeAdapter

from fundamend import AhbReader, Anwendungshandbuch, MessageImplementationGuide, MigReader

Expand Down

0 comments on commit de261cf

Please sign in to comment.