Source code for maus.edifact

"""
This module manages EDIFACT related stuff. It's basically a helper module to avoid stringly typed parameters.
"""
import datetime
import re
from enum import Enum
from typing import Dict, Optional

import attrs

_PRUEFI_REGEX = r"^[1-9]\d{4}$"
pruefidentifikator_pattern = re.compile(_PRUEFI_REGEX)


# pylint: disable=too-few-public-methods
[docs]class EdifactFormat(str, Enum): """ existing EDIFACT formats """ APERAK = "APERAK" COMDIS = "COMDIS" #: communication dispute CONTRL = "CONTRL" #: control messages IFTSTA = "IFTSTA" #: Multimodaler Statusbericht INSRPT = "INSRPT" #: Prüfbericht INVOIC = "INVOIC" #: invoice MSCONS = "MSCONS" #: meter readings ORDCHG = "ORDCHG" #: changing an order ORDERS = "ORDERS" #: orders ORDRSP = "ORDRSP" #: orders response PRICAT = "PRICAT" #: price catalogue QUOTES = "QUOTES" #: quotes REMADV = "REMADV" #: zahlungsavis REQOTE = "REQOTE" #: request quote PARTIN = "PARTIN" #: market partner data UTILMD = "UTILMD" #: utilities master data UTILTS = "UTILTS" #: formula def __str__(self): return self.value
_edifact_mapping: Dict[str, EdifactFormat] = { "99": EdifactFormat.APERAK, "29": EdifactFormat.COMDIS, "21": EdifactFormat.IFTSTA, "23": EdifactFormat.INSRPT, "31": EdifactFormat.INVOIC, "13": EdifactFormat.MSCONS, "39": EdifactFormat.ORDCHG, "17": EdifactFormat.ORDERS, "19": EdifactFormat.ORDRSP, "27": EdifactFormat.PRICAT, "15": EdifactFormat.QUOTES, "33": EdifactFormat.REMADV, "35": EdifactFormat.REQOTE, "37": EdifactFormat.PARTIN, "11": EdifactFormat.UTILMD, "25": EdifactFormat.UTILTS, "91": EdifactFormat.CONTRL, "92": EdifactFormat.APERAK, "44": EdifactFormat.UTILMD, # UTILMD for GAS since FV2310 "55": EdifactFormat.UTILMD, # UTILMD for STROM since FV2310 }
[docs]class EdifactFormatVersion(str, Enum): """ One format version refers to the period in which an AHB is valid. """ FV2104 = "FV2104" #: valid from 2021-04-01 until 2021-10-01 FV2110 = "FV2110" #: valid from 2021-10-01 until 2022-04-01 FV2210 = "FV2210" #: valid from 2022-10-01 onwards ("MaKo 2022", was 2204 previously) FV2304 = "FV2304" #: valid from 2023-04-01 onwards FV2310 = "FV2310" #: valid from 2023-10-01 onwards FV2404 = "FV2404" #: valid from 2024-04-01 onwards # whenever you add another value here, please also make sure to add its key date to get_edifact_format_version below def __str__(self): return self.value
[docs]def get_edifact_format_version(key_date: datetime.datetime) -> EdifactFormatVersion: """ :return: the edifact format version that is valid on the specified key date """ if key_date < datetime.datetime(2021, 9, 30, 22, 0, 0, 0, tzinfo=datetime.timezone.utc): return EdifactFormatVersion.FV2104 if key_date < datetime.datetime(2022, 9, 30, 22, 0, 0, 0, tzinfo=datetime.timezone.utc): return EdifactFormatVersion.FV2110 if key_date < datetime.datetime(2023, 3, 31, 22, 0, 0, 0, tzinfo=datetime.timezone.utc): return EdifactFormatVersion.FV2210 if key_date < datetime.datetime(2023, 9, 30, 22, 0, 0, 0, tzinfo=datetime.timezone.utc): return EdifactFormatVersion.FV2304 if key_date < datetime.datetime(2024, 3, 31, 22, 0, 0, 0, tzinfo=datetime.timezone.utc): return EdifactFormatVersion.FV2310 return EdifactFormatVersion.FV2404
[docs]def get_current_edifact_format_version() -> EdifactFormatVersion: """ returns the edifact_format_version that is valid as of now """ tz_aware_now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) return get_edifact_format_version(tz_aware_now)
[docs]def is_edifact_boilerplate(segment_code: Optional[str]) -> bool: """ returns true iff this segment is not relevant in a sense that it has to be validated or merged with the AHB """ if not segment_code: return True return segment_code.strip() in {"UNT", "UNZ"}
[docs]def get_format_of_pruefidentifikator(pruefidentifikator: str) -> EdifactFormat: """ returns the format corresponding to a given pruefi """ if not pruefidentifikator: raise ValueError("The pruefidentifikator must not be falsy") if not pruefidentifikator_pattern.match(pruefidentifikator): raise ValueError(f"The pruefidentifikator '{pruefidentifikator}' is invalid.") try: return _edifact_mapping[pruefidentifikator[:2]] except KeyError as key_error: raise ValueError(f"No Edifact format was found for pruefidentifikator '{pruefidentifikator}'.") from key_error
# pylint:disable=unused-argument def _check_that_pruefi_and_format_are_consistent(instance: "EdiMetaData", attribute, value: str): """ The value is the pruefidentifikator, the instance is the EdiMetaData instance. This validator raises an ValueError if the pruefidentifikator is not consistent with the instance.edifact_format. """ actual_format = instance.edifact_format expected_format = get_format_of_pruefidentifikator(value) if actual_format != expected_format: raise ValueError(f"'{value}' is incompatible with '{actual_format}'; expected '{expected_format}' instead")
[docs]@attrs.define(kw_only=True, frozen=True, auto_attribs=True) class EdiMetaData: """ a container that contains edifact-related metadata """ pruefidentifikator: str = attrs.field( validator=attrs.validators.and_( attrs.validators.instance_of(str), attrs.validators.matches_re(_PRUEFI_REGEX), _check_that_pruefi_and_format_are_consistent, ) ) """ The pruefidentifikator, e.g. '11042' """ edifact_format: EdifactFormat = attrs.field(validator=attrs.validators.instance_of(EdifactFormat)) """ The Edifact Format, e.g. 'UTILMD' """ @edifact_format.default def _get_format_from_pruefidentifikator(self): return get_format_of_pruefidentifikator(self.pruefidentifikator) edifact_format_version: EdifactFormatVersion = attrs.field( validator=attrs.validators.instance_of(EdifactFormatVersion) ) """ The Edifact Format Version, e.g. 'FV2210' """