Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/api/bo4e.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Subpackages
bo4e.bo
bo4e.com
bo4e.enum
bo4e.utils

Submodules
----------
Expand Down
150 changes: 72 additions & 78 deletions docs/uml.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,13 @@
import subprocess
from abc import ABCMeta, abstractmethod
from pathlib import Path
from re import Pattern
from typing import Any, Dict, List, Optional, Tuple, Type, cast

import networkx as nx # type: ignore[import]
import requests # type: ignore[import]

# pylint: disable=no-name-in-module
from pydantic import ConstrainedStr
from pydantic.fields import (
MAPPING_LIKE_SHAPES,
SHAPE_GENERIC,
SHAPE_LIST,
SHAPE_NAME_LOOKUP,
SHAPE_SINGLETON,
SHAPE_TUPLE,
ModelField,
)
from pydantic.main import ModelMetaclass
from pydantic.typing import display_as_type
from pydantic._internal._model_construction import ModelMetaclass
from pydantic._internal._repr import display_as_type
from pydantic.fields import FieldInfo


# pylint: disable=too-few-public-methods
Expand Down Expand Up @@ -143,7 +131,7 @@ def add_association(
self,
node1: str,
node2: str,
through_field: ModelField,
through_field: FieldInfo,
card1: Optional[Cardinality] = None,
card2: Optional[Cardinality] = None,
) -> None:
Expand Down Expand Up @@ -228,37 +216,39 @@ def get_cardinality_string(card: Optional[Cardinality]) -> Optional[str]:
return None

@staticmethod
def model_field_str(model_field: ModelField, card: Optional[Cardinality] = None) -> str:
def model_field_str(model_field: FieldInfo, card: Optional[Cardinality] = None) -> str:
"""
Parse the type of the ModelField to a printable string. Copied from pydantic.field.ModelField._type_display()
Parse the type of the ModelField to a printable string. Copied from
pydantic._internal._repr.display_as_type
https://github.com/pydantic/pydantic/blob/58ae1ef77a4bf4276aaa6214aaaaf59455f5e587/pydantic/_internal/_repr.py#L85
"""
result_str = display_as_type(model_field.type_)

result_str = display_as_type(model_field.annotation)
# todo: check if this is still necessary
# https://github.com/bo4e/BO4E-python/issues/478
# have to do this since display_as_type(self.outer_type_) is different (and wrong) on python 3.6
if model_field.shape in MAPPING_LIKE_SHAPES:
result_str = f"Mapping[{display_as_type(cast(ModelField, model_field.key_field).type_)}, {result_str}]"
elif model_field.shape == SHAPE_TUPLE:
result_str = "Tuple[" + ", ".join(
display_as_type(
sub_field.type_ for sub_field in model_field.sub_fields # type:ignore[arg-type,union-attr]
)
)
result_str += "]"
elif model_field.shape == SHAPE_GENERIC:
assert model_field.sub_fields
result_str = (
f"{display_as_type(model_field.type_)}["
f"{', '.join(display_as_type(sub_field.type_) for sub_field in model_field.sub_fields)}]"
)
elif model_field.shape not in (SHAPE_SINGLETON, SHAPE_LIST):
result_str = SHAPE_NAME_LOOKUP[model_field.shape].format(result_str)

if isinstance(model_field.outer_type_, type) and issubclass(model_field.outer_type_, ConstrainedStr):
if isinstance(model_field.outer_type_.regex, Pattern):
result_str = f"str<{model_field.outer_type_.regex.pattern}>"
elif isinstance(model_field.outer_type_.regex, str):
result_str = f"str<{model_field.outer_type_.regex}>"

# if model_field.shape in MAPPING_LIKE_SHAPES:
# result_str = f"Mapping[{display_as_type(cast(ModelField, model_field.key_field).type_)}, {result_str}]"
# elif model_field.shape == SHAPE_TUPLE:
# result_str = "Tuple[" + ", ".join(
# display_as_type(
# sub_field.type_ for sub_field in model_field.sub_fields # type:ignore[arg-type,union-attr]
# )
# )
# result_str += "]"
# elif model_field.shape == SHAPE_GENERIC:
# assert model_field.sub_fields
# result_str = (
# f"{display_as_type(model_field.type_)}["
# f"{', '.join(display_as_type(sub_field.type_) for sub_field in model_field.sub_fields)}]"
# )
# elif model_field.shape not in (SHAPE_SINGLETON, SHAPE_LIST):
# result_str = SHAPE_NAME_LOOKUP[model_field.shape].format(result_str)
#
# if is_constrained_str(model_field):
# if isinstance(model_field.outer_type_.regex, Pattern):
# result_str = f"str<{model_field.outer_type_.regex.pattern}>"
# elif isinstance(model_field.outer_type_.regex, str):
# result_str = f"str<{model_field.outer_type_.regex}>"
assert card is not None
return f"{result_str} [{_UMLNetworkABC.get_cardinality_string(card)}]"

Expand Down Expand Up @@ -291,13 +281,14 @@ def _node_to_str(self, node: str, detailed: bool = True, **kwargs: Any) -> str:
if detailed:
cls_str += " {\n"
for field_dict in self.nodes[node]["fields"].values():
model_field = field_dict["model_field"]
type_modl_namespace = f"{model_field.type_.__module__}.{model_field.type_.__name__}"
model_field: FieldInfo = field_dict["model_field"]
assert model_field.annotation is not None
type_modl_namespace = f"{model_field.annotation.__module__}.{model_field.annotation.__name__}"
if type_modl_namespace in self[node]:
# Skip the fields which will appear as references in the graph
continue
type_str = _UMLNetworkABC.model_field_str(model_field, field_dict["card"])
if model_field.required:
if model_field.is_required():
cls_str += f"\t{model_field.alias} : {type_str}\n"
else:
cls_str += f"\t{model_field.alias} : {type_str} = {model_field.default}\n"
Expand Down Expand Up @@ -457,37 +448,37 @@ def write_class_umls(uml_network: _UMLNetworkABC, namespaces_to_parse: List[str]
return path_list


def model_field_str(model_field: ModelField) -> str:
def model_field_str(model_field: FieldInfo) -> str:
"""
Parse the type of the ModelField to a printable string. Copied from pydantic.field.ModelField._type_display()
"""
result_str = display_as_type(model_field.type_)

result_str = display_as_type(model_field.annotation)
# todo: check if this is still necessary
# https://github.com/bo4e/BO4E-python/issues/478
# have to do this since display_as_type(self.outer_type_) is different (and wrong) on python 3.6
if model_field.shape in MAPPING_LIKE_SHAPES:
result_str = f"Mapping[{display_as_type(cast(ModelField, model_field.key_field).type_)}, {result_str}]"
elif model_field.shape == SHAPE_TUPLE:
result_str = "Tuple[" + ", ".join(
display_as_type(
sub_field.type_ for sub_field in model_field.sub_fields # type:ignore[arg-type,union-attr]
)
)
result_str += "]"
elif model_field.shape == SHAPE_GENERIC:
assert model_field.sub_fields
result_str = (
f"{display_as_type(model_field.type_)}["
f"{', '.join(display_as_type(sub_field.type_) for sub_field in model_field.sub_fields)}]"
)
elif model_field.shape != SHAPE_SINGLETON:
result_str = SHAPE_NAME_LOOKUP[model_field.shape].format(result_str)

if model_field.allow_none and (model_field.shape != SHAPE_SINGLETON or not model_field.sub_fields):
result_str = f"Optional[{result_str}]"
# if model_field.shape in MAPPING_LIKE_SHAPES:
# result_str = f"Mapping[{display_as_type(cast(ModelField, model_field.key_field).type_)}, {result_str}]"
# elif model_field.shape == SHAPE_TUPLE:
# result_str = "Tuple[" + ", ".join(
# display_as_type(
# sub_field.type_ for sub_field in model_field.sub_fields # type:ignore[arg-type,union-attr]
# )
# )
# result_str += "]"
# elif model_field.shape == SHAPE_GENERIC:
# assert model_field.sub_fields
# result_str = (
# f"{display_as_type(model_field.type_)}["
# f"{', '.join(display_as_type(sub_field.type_) for sub_field in model_field.sub_fields)}]"
# )
# elif model_field.shape != SHAPE_SINGLETON:
# result_str = SHAPE_NAME_LOOKUP[model_field.shape].format(result_str)
# if model_field.allow_none and (model_field.shape != SHAPE_SINGLETON or not model_field.sub_fields):
# result_str = f"Optional[{result_str}]"
return result_str


def get_cardinality(model_field: ModelField) -> Cardinality:
def get_cardinality(model_field: FieldInfo) -> Cardinality:
"""
Determines the cardinality of a field. This field can either contain a reference to another node in the graph or
be of another arbitrary type.
Expand All @@ -500,10 +491,12 @@ def get_cardinality(model_field: ModelField) -> Cardinality:
if type_str.startswith("List[") or type_str.startswith("Optional[List["):
card1 = "0"
card2 = "*"
if hasattr(model_field.outer_type_, "max_items") and model_field.outer_type_.max_items:
card2 = str(model_field.outer_type_.max_items)
if hasattr(model_field.outer_type_, "min_items") and model_field.outer_type_.min_items:
card1 = str(model_field.outer_type_.min_items)
# todo: re-add min_length / max_length interpretation
# https://github.com/bo4e/BO4E-python/issues/477
# if hasattr(model_field.outer_type_, "max_items") and model_field.outer_type_.max_items:
# card2 = str(model_field.outer_type_.max_items)
# if hasattr(model_field.outer_type_, "min_items") and model_field.outer_type_.min_items:
# card1 = str(model_field.outer_type_.min_items)
return card1, card2


Expand Down Expand Up @@ -558,14 +551,15 @@ def _recursive_add_class(
# ------------------------------------------------------------------------------------------------------------------
# ------ determine references in fields which pass `regex_incl_network` and `regex_excl_network` -------------------
for field_dict in uml_network.nodes[modl_namespace]["fields"].values():
model_field: ModelField = field_dict["model_field"]
model_field: FieldInfo = field_dict["model_field"]
# Add cardinality information to the field
field_card = get_cardinality(model_field)
field_dict["card"] = field_card
type_modl_namespace = f"{model_field.type_.__module__}.{model_field.type_.__name__}"
assert model_field.annotation is not None
type_modl_namespace = f"{model_field.annotation.__module__}.{model_field.annotation.__name__}"
if re.match(regex_incl_network, type_modl_namespace) and not re.match(regex_excl_network, type_modl_namespace):
if not uml_network.has_node(type_modl_namespace):
_recursive_add_class(model_field.type_, type_modl_namespace, uml_network)
_recursive_add_class(model_field.annotation, type_modl_namespace, uml_network) # type:ignore[arg-type]

uml_network.add_association(
modl_namespace,
Expand Down
2 changes: 1 addition & 1 deletion json_schemas/generate_json_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@
# this sanitizing step is necessary since python 3.11
definition["description"] = definition["description"].strip()
with open(file_path, "w+", encoding="utf-8") as json_schema_file:
json_schema_file.write(json.dumps(schema_json_dict, ensure_ascii=False, indent=4))
json_schema_file.write(json.dumps(schema_json_dict, indent=4))
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
iso3166
pydantic==1.*
pydantic>=2.0.0
pyhumps
16 changes: 11 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile requirements.in
#
annotated-types==0.5.0
# via pydantic
iso3166==2.1.1
# via -r requirements.in
pydantic==1.10.5
pydantic==2.0.0
# via -r requirements.in
pydantic-core==2.0.1
# via pydantic
pyhumps==3.8.0
# via -r requirements.in
typing-extensions==4.2.0
# via pydantic
typing-extensions==4.7.1
# via
# pydantic
# pydantic-core
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ include_package_data = True
python_requires = >=3.10
install_requires =
iso3166
pydantic==1.*
pydantic>=2.0.0
pyhumps

[options.packages.find]
Expand Down
4 changes: 2 additions & 2 deletions src/bo4e/bo/angebot.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class Angebot(Geschaeftsobjekt):
bo_typ: BoTyp = BoTyp.ANGEBOT
# required attributes
#: Eindeutige Nummer des Angebotes
angebotsnummer: constr(strict=True, regex=r"^\d+$") # type: ignore[valid-type]
angebotsnummer: constr(strict=True, pattern=r"^\d+$") # type: ignore[valid-type]
#: Erstellungsdatum des Angebots
angebotsdatum: datetime
#: Sparte, für die das Angebot abgegeben wird (Strom/Gas)
Expand All @@ -48,7 +48,7 @@ class Angebot(Geschaeftsobjekt):
#: Empfänger des Angebots
angebotsnehmer: Geschaeftspartner

varianten: conlist(Angebotsvariante, min_items=1) # type: ignore[valid-type]
varianten: conlist(Angebotsvariante, min_length=1) # type: ignore[valid-type]
""" Eine oder mehrere Varianten des Angebots mit den Angebotsteilen;
Ein Angebot besteht mindestens aus einer Variante."""

Expand Down
2 changes: 1 addition & 1 deletion src/bo4e/bo/ausschreibung.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class Ausschreibung(Geschaeftsobjekt):
Es muss daher entweder eine Dauer oder ein Zeitraum in Form von Start und Ende angegeben sein
"""
#: Die einzelnen Lose, aus denen sich die Ausschreibung zusammensetzt
lose: conlist(Ausschreibungslos, min_items=1) # type: ignore[valid-type]
lose: conlist(Ausschreibungslos, min_length=1) # type: ignore[valid-type]

# optional attributes
#: Aufzählung der unterstützten Ausschreibungsportale
Expand Down
2 changes: 1 addition & 1 deletion src/bo4e/bo/energiemenge.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ class Energiemenge(Geschaeftsobjekt):
lokationstyp: Lokationstyp

#: Gibt den Verbrauch in einer Zeiteinheit an
energieverbrauch: conlist(Verbrauch, min_items=1) # type: ignore[valid-type]
energieverbrauch: conlist(Verbrauch, min_length=1) # type: ignore[valid-type]
# there are no optional attributes
2 changes: 1 addition & 1 deletion src/bo4e/bo/geschaeftsobjekt.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ class Config:
"""

alias_generator = camelize
allow_population_by_field_name = True
populate_by_name = True
extra = "allow"
json_encoders = {Decimal: str}
2 changes: 1 addition & 1 deletion src/bo4e/bo/kosten.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class Kosten(Geschaeftsobjekt):
#: Für diesen Zeitraum wurden die Kosten ermittelt
gueltigkeit: Zeitraum
#: In Kostenblöcken werden Kostenpositionen zusammengefasst. Beispiele: Netzkosten, Umlagen, Steuern etc
kostenbloecke: conlist(Kostenblock, min_items=1) # type: ignore[valid-type]
kostenbloecke: conlist(Kostenblock, min_length=1) # type: ignore[valid-type]

# optional attributes
#: Die Gesamtsumme über alle Kostenblöcke und -positionen
Expand Down
4 changes: 2 additions & 2 deletions src/bo4e/bo/lastgang.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class _LastgangBody(Geschaeftsobjekt):
#: Versionsnummer des Lastgangs
version: Optional[str] = None
#: Die OBIS-Kennzahl für den Wert, die festlegt, welche Größe mit dem Stand gemeldet wird, z.B. '1-0:1.8.1'
obis_kennzahl: Optional[constr(strict=True, regex=OBIS_PATTERN)] = None # type: ignore[valid-type]
obis_kennzahl: Optional[constr(strict=True, pattern=OBIS_PATTERN)] = None # type: ignore[valid-type]


# pylint: disable=too-many-instance-attributes, too-few-public-methods
Expand Down Expand Up @@ -89,4 +89,4 @@ class Lastgang(_LastgangBody):
bo_typ: BoTyp = BoTyp.LASTGANG

#: Die im Lastgang enthaltenen Messwerte
werte: conlist(Zeitreihenwert, min_items=1) # type: ignore[valid-type]
werte: conlist(Zeitreihenwert, min_length=1) # type: ignore[valid-type]
14 changes: 8 additions & 6 deletions src/bo4e/bo/marktlokation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
"""

# pylint: disable=too-many-instance-attributes, too-few-public-methods
from typing import Any, Dict, Optional
from typing import Optional

# pylint: disable=no-name-in-module
from pydantic import conlist, validator
from pydantic import conlist, field_validator
from pydantic_core.core_schema import ValidationInfo

from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt
from bo4e.bo.geschaeftspartner import Geschaeftspartner
Expand Down Expand Up @@ -44,7 +45,7 @@ class Marktlokation(Geschaeftsobjekt):
bo_typ: BoTyp = BoTyp.MARKTLOKATION
#: Identifikationsnummer einer Marktlokation, an der Energie entweder verbraucht, oder erzeugt wird.
marktlokations_id: str
_marktlokations_id_check = validator("marktlokations_id", allow_reuse=True)(validate_marktlokations_id)
_marktlokations_id_check = field_validator("marktlokations_id")(validate_marktlokations_id)
#: Sparte der Marktlokation, z.B. Gas oder Strom
sparte: Sparte
#: Kennzeichnung, ob Energie eingespeist oder entnommen (ausgespeist) wird
Expand Down Expand Up @@ -114,15 +115,16 @@ class Marktlokation(Geschaeftsobjekt):
Flurstück erfolgen.
"""

kundengruppen: conlist(Kundentyp, min_items=0) = None # type: ignore[valid-type]
kundengruppen: Optional[conlist(Kundentyp, min_length=0)] = None # type: ignore[valid-type]
#: Kundengruppen der Marktlokation

# pylint:disable=unused-argument, no-self-argument
@validator("katasterinformation", always=True)
@field_validator("katasterinformation")
def validate_address_info(
cls, katasterinformation: Optional[Katasteradresse], values: Dict[str, Any]
cls, katasterinformation: Optional[Katasteradresse], validation_info: ValidationInfo
) -> Optional[Katasteradresse]:
"""Checks that there is one and only one valid adress given."""
values = validation_info.data # type:ignore[attr-defined]
all_address_attributes = [
values["lokationsadresse"],
values["geoadresse"],
Expand Down
2 changes: 1 addition & 1 deletion src/bo4e/bo/marktteilnehmer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Marktteilnehmer(Geschaeftspartner):
#: Gibt im Klartext die Bezeichnung der Marktrolle an
marktrolle: Marktrolle
#: Gibt die Codenummer der Marktrolle an
rollencodenummer: constr(strict=True, regex=r"^\d{13}$") # type: ignore[valid-type]
rollencodenummer: constr(strict=True, pattern=r"^\d{13}$") # type: ignore[valid-type]
#: Gibt den Typ des Codes an
rollencodetyp: Rollencodetyp
#: Sparte des Marktteilnehmers, z.B. Gas oder Strom
Expand Down
Loading