Skip to content

Commit

Permalink
🔼Bump attrs from 21.2.0 to 21.4.0 (#16)
Browse files Browse the repository at this point in the history
* Bump attrs from 21.2.0 to 21.4.0

Bumps [attrs](https://github.com/python-attrs/attrs) from 21.2.0 to 21.4.0.
- [Release notes](https://github.com/python-attrs/attrs/releases)
- [Changelog](https://github.com/python-attrs/attrs/blob/main/CHANGELOG.rst)
- [Commits](python-attrs/attrs@21.2.0...21.4.0)

---
updated-dependencies:
- dependency-name: attrs
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

* replacements to use modern attrs

attr.ib ➡ attrs.field
attr.s ➡ attrs.define
attr.validators ➡ attrs.validators

* ignore pylint warnings

stack overflow says this is due to a known bug in pylint: pylint-dev/pylint#1694

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: konstantin <[email protected]>
  • Loading branch information
dependabot[bot] and hf-kklein authored Feb 9, 2022
1 parent 73f86c9 commit 8c30dcb
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 84 deletions.
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
# pip-compile requirements.in
#
attrs==21.2.0
attrs==21.4.0
# via -r requirements.in
lxml==4.7.1
# via -r requirements.in
Expand Down
64 changes: 32 additions & 32 deletions src/maus/models/anwendungshandbuch.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,62 +10,59 @@
from typing import List, Optional, Sequence
from uuid import UUID

import attr
import attrs
from marshmallow import Schema, fields, post_load # type:ignore[import]

from maus.models.edifact_components import SegmentGroup, SegmentGroupSchema


# pylint:disable=too-many-instance-attributes
@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class AhbLine:
"""
An AhbLine is a single line inside the machine readable, flat AHB.
"""

guid: Optional[
UUID
# pylint: disable=line-too-long
] = attr.ib(
validator=attr.validators.optional(attr.validators.instance_of(UUID))
guid: Optional[UUID] = attrs.field(
validator=attrs.validators.optional(attrs.validators.instance_of(UUID))
) #: optional key
# because the combination (segment group, segment, data element, name) is not guaranteed to be unique
# yes, it's actually that bad already
segment_group_key: Optional[str] = attr.ib(
validator=attr.validators.optional(validator=attr.validators.instance_of(str))
segment_group_key: Optional[str] = attrs.field(
validator=attrs.validators.optional(validator=attrs.validators.instance_of(str))
)
""" the segment group, e.g. 'SG5' """

segment_code: Optional[str] = attr.ib(
validator=attr.validators.optional(validator=attr.validators.instance_of(str))
segment_code: Optional[str] = attrs.field(
validator=attrs.validators.optional(validator=attrs.validators.instance_of(str))
)
"""the segment, e.g. 'IDE'"""

data_element: Optional[str] = attr.ib(
validator=attr.validators.optional(validator=attr.validators.instance_of(str))
data_element: Optional[str] = attrs.field(
validator=attrs.validators.optional(validator=attrs.validators.instance_of(str))
)
""" the data element ID, e.g. '3224' """

value_pool_entry: Optional[str] = attr.ib(
validator=attr.validators.optional(validator=attr.validators.instance_of(str))
value_pool_entry: Optional[str] = attrs.field(
validator=attrs.validators.optional(validator=attrs.validators.instance_of(str))
)
""" one of (possible multiple) allowed values, e.g. 'E01' or '293' """

name: Optional[str] = attr.ib(validator=attr.validators.optional(validator=attr.validators.instance_of(str)))
name: Optional[str] = attrs.field(validator=attrs.validators.optional(validator=attrs.validators.instance_of(str)))
"""the name, e.g. 'Meldepunkt'. It can be both the description of a field but also its meaning"""

# Check the unittest test_csv_file_reading_11042 to see the different values of name. It's not only the grey fields
# where user input is expected but also the meanings / values of value pool entries. This means the exact meaning of
# name can only be determined in the context in which it is used. This is one of many shortcoming of the current AHB
# structure: Things in the same column don't necessarily mean the same thing.
ahb_expression: Optional[str] = attr.ib(
validator=attr.validators.optional(validator=attr.validators.instance_of(str))
ahb_expression: Optional[str] = attrs.field(
validator=attrs.validators.optional(validator=attrs.validators.instance_of(str))
)
"""a requirement indicator + an optional condition ("ahb expression"), f.e. 'Muss [123] O [456]' """
# note: to parse expressions from AHBs consider using AHBicht: https://github.com/Hochfrequenz/ahbicht/

section_name: Optional[str] = attr.ib(
validator=attr.validators.optional(validator=attr.validators.instance_of(str)), default=None
section_name: Optional[str] = attrs.field(
validator=attrs.validators.optional(validator=attrs.validators.instance_of(str)), default=None
)
"""
The section name describes the purpose of a segment, f.e. "Nachrichten-Kopfsegment" or "Beginn der Nachricht"
Expand All @@ -76,6 +73,8 @@ def holds_any_information(self) -> bool:
Returns true iff the line holds any information exception for just a GUID.
This is useful to filter out empty lines which are artefacts remaining from the scraping process.
"""
# https://stackoverflow.com/questions/47972143/using-attr-with-pylint
# pylint: disable=no-member
return (
(self.segment_group_key is not None and len(self.segment_group_key.strip()) > 0)
or (self.segment_code is not None and len(self.segment_code.strip()) > 0)
Expand Down Expand Up @@ -125,7 +124,7 @@ def deserialize(self, data, **kwargs) -> AhbLine:
return AhbLine(**data)


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class AhbMetaInformation:
"""
Meta information about an AHB like f.e. its title, Prüfidentifikator, possible sender and receiver roles
Expand All @@ -151,20 +150,21 @@ def deserialize(self, data, **kwargs) -> AhbMetaInformation:
return AhbMetaInformation(**data)


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class FlatAnwendungshandbuch:
"""
A flat Anwendungshandbuch (AHB) models an Anwendungshandbuch as combination of some meta data and an ordered list
of `.class:`.FlatAhbLine s. Basically a flat Anwendungshandbuch is the result of a simple scraping approach.
You can create instances of this class without knowing anything about the "under the hood" structure of AHBs or MIGs
"""

meta: AhbMetaInformation = attr.ib(validator=attr.validators.instance_of(AhbMetaInformation))
meta: AhbMetaInformation = attrs.field(validator=attrs.validators.instance_of(AhbMetaInformation))
"""information about this AHB"""

lines: List[AhbLine] = attr.ib(
validator=attr.validators.deep_iterable(
member_validator=attr.validators.instance_of(AhbLine), iterable_validator=attr.validators.instance_of(list)
lines: List[AhbLine] = attrs.field(
validator=attrs.validators.deep_iterable(
member_validator=attrs.validators.instance_of(AhbLine),
iterable_validator=attrs.validators.instance_of(list),
)
) #: ordered list lines as they occur in the AHB

Expand Down Expand Up @@ -250,19 +250,19 @@ def deserialize(self, data, **kwargs) -> FlatAnwendungshandbuch:
return FlatAnwendungshandbuch(**data)


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class DeepAnwendungshandbuch:
"""
The data of the AHB nested as described in the MIG.
"""

meta: AhbMetaInformation = attr.ib(validator=attr.validators.instance_of(AhbMetaInformation))
meta: AhbMetaInformation = attrs.field(validator=attrs.validators.instance_of(AhbMetaInformation))
"""information about this AHB"""

lines: List[SegmentGroup] = attr.ib(
validator=attr.validators.deep_iterable(
member_validator=attr.validators.instance_of(SegmentGroup),
iterable_validator=attr.validators.instance_of(list),
lines: List[SegmentGroup] = attrs.field(
validator=attrs.validators.deep_iterable(
member_validator=attrs.validators.instance_of(SegmentGroup),
iterable_validator=attrs.validators.instance_of(list),
)
) #: the nested data

Expand Down
84 changes: 44 additions & 40 deletions src/maus/models/edifact_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@
from abc import ABC
from typing import Dict, List, Optional, Type

import attr
import attrs
from marshmallow import Schema, fields, post_dump, post_load, pre_dump, pre_load # type:ignore[import]


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class DataElement(ABC):
"""
A data element holds specific kinds of data. It is defined in EDIFACT.
At least for the German energy market communication any data element has a 4 digit key.
For example in UTILMD the data element that holds the 13 digit market partner ID is data element '3039'
"""

discriminator: str = attr.ib(validator=attr.validators.instance_of(str))
discriminator: str = attrs.field(validator=attrs.validators.instance_of(str))
""" The discriminator uniquely identifies the data element. This _might_ be its key """
# but could also be a reference or a name
#: the the ID of the data element (f.e. "0062") for the Nachrichten-Referenznummer
data_element_id: str = attr.ib(validator=attr.validators.matches_re(r"\d{4}"))
data_element_id: str = attrs.field(validator=attrs.validators.matches_re(r"\d{4}"))


class DataElementSchema(Schema):
Expand All @@ -35,16 +35,16 @@ class DataElementSchema(Schema):
data_element_id = fields.String(required=True)


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class DataElementFreeText(DataElement):
"""
A DataElementFreeText is a data element that allows entering arbitrary data.
This is the main difference to the :class:`DataElementValuePool` which has a finite set of allowed values attached.
"""

ahb_expression: str = attr.ib(validator=attr.validators.instance_of(str))
ahb_expression: str = attrs.field(validator=attrs.validators.instance_of(str))
"""any freetext data element has an ahb expression attached. Could be 'X' but also 'M [13]'"""
entered_input: Optional[str] = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(str)))
entered_input: Optional[str] = attrs.field(validator=attrs.validators.optional(attrs.validators.instance_of(str)))
"""If the message contains data for this data element, this is not None."""


Expand All @@ -65,7 +65,7 @@ def deserialize(self, data, **kwargs) -> DataElementFreeText:
return DataElementFreeText(**data)


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class DataElementValuePool(DataElement):
"""
A DataElementValuePool is a data element with a finite set of allowed values.
Expand Down Expand Up @@ -109,11 +109,11 @@ def __init__(
self.free_text = free_text
self.value_pool = value_pool

free_text: Optional[DataElementFreeText] = attr.ib(
validator=attr.validators.optional(attr.validators.instance_of(DataElementFreeText))
free_text: Optional[DataElementFreeText] = attrs.field(
validator=attrs.validators.optional(attrs.validators.instance_of(DataElementFreeText))
)
value_pool: Optional[DataElementValuePool] = attr.ib(
validator=attr.validators.optional(attr.validators.instance_of(DataElementValuePool))
value_pool: Optional[DataElementValuePool] = attrs.field(
validator=attrs.validators.optional(attrs.validators.instance_of(DataElementValuePool))
)


Expand Down Expand Up @@ -187,7 +187,7 @@ def prepare_for_serialization(self, data, **kwargs) -> _FreeTextOrValuePool:
raise NotImplementedError(f"Data type of {data} is not implemented for JSON serialization")


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class SegmentLevel(ABC):
"""
SegmentLevel describes @annika: what does it describe?
Expand All @@ -206,15 +206,15 @@ class SegmentLevelSchema(Schema):
ahb_expression = fields.String(required=True)


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class Segment(SegmentLevel):
"""
A Segment contains multiple data elements.
"""

data_elements: List[DataElement]
section_name: Optional[str] = attr.ib(
validator=attr.validators.optional(attr.validators.instance_of(str)), default=None
section_name: Optional[str] = attrs.field(
validator=attrs.validators.optional(attrs.validators.instance_of(str)), default=None
)
"""
For the MIG matching it might be necessary to know the section in which the data element occured in the AHB.
Expand All @@ -240,19 +240,19 @@ def deserialize(self, data, **kwargs) -> Segment:
return Segment(**data)


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class SegmentGroup(SegmentLevel):
"""
A segment group that contains segments and nested groups.
On root level of a message there might be a "virtual" segment group of all segments that formally don't have a group
This group has the key "root".
"""

segments: Optional[List[Segment]] = attr.ib(
validator=attr.validators.optional(
attr.validators.deep_iterable(
member_validator=attr.validators.instance_of(Segment),
iterable_validator=attr.validators.instance_of(list),
segments: Optional[List[Segment]] = attrs.field(
validator=attrs.validators.optional(
attrs.validators.deep_iterable(
member_validator=attrs.validators.instance_of(Segment),
iterable_validator=attrs.validators.instance_of(list),
)
)
) #: the segments inside this very group
Expand Down Expand Up @@ -282,36 +282,38 @@ def deserialize(self, data, **kwargs) -> SegmentGroup:
return SegmentGroup(**data)


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class EdifactStackLevel:
"""
The EDIFACT stack level describes the hierarchy level of information inside an EDIFACT message.
"""

#: the name of the level, f.e. 'Dokument' or 'Nachricht' or 'Meldepunkt'
name: str = attr.ib(validator=attr.validators.instance_of(str))
name: str = attrs.field(validator=attrs.validators.instance_of(str))
#: describes if this level is groupable / if there are multiple instances of this level within the same message
is_groupable: bool = attr.ib(validator=attr.validators.instance_of(bool))
is_groupable: bool = attrs.field(validator=attrs.validators.instance_of(bool))
#: the index if present (f.e. 0)
index: Optional[int] = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(int)))
index: Optional[int] = attrs.field(
default=None, validator=attrs.validators.optional(attrs.validators.instance_of(int))
)


#: a pattern that matches parts of the json path: https://regex101.com/r/iQzdXK/1
_level_pattern = re.compile(r"\[\"(?P<level_name>[^\[\]]+?)\"\](?:\[(?P<index>\d+)\])?")


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class EdifactStack:
"""
The EdifactStack describes where inside an EDIFACT message data are found.
The stack is independent of the actual implementation used to create the EDIFACT (be it XML, JSON whatever).
"""

#: levels describe the nesting inside an edifact message
levels: List[EdifactStackLevel] = attr.ib(
validator=attr.validators.deep_iterable(
member_validator=attr.validators.instance_of(EdifactStackLevel),
iterable_validator=attr.validators.instance_of(list),
levels: List[EdifactStackLevel] = attrs.field(
validator=attrs.validators.deep_iterable(
member_validator=attrs.validators.instance_of(EdifactStackLevel),
iterable_validator=attrs.validators.instance_of(list),
)
)

Expand Down Expand Up @@ -356,6 +358,8 @@ def to_json_path(self) -> str:
Transforms this instance into a JSON Path.
"""
result: str = "$"
# https://stackoverflow.com/questions/47972143/using-attr-with-pylint
# pylint: disable=not-an-iterable
for level in self.levels:
result += '["' + level.name + '"]'
if level.index is not None:
Expand All @@ -365,31 +369,31 @@ def to_json_path(self) -> str:
return result


@attr.s(auto_attribs=True, kw_only=True)
@attrs.define(auto_attribs=True, kw_only=True)
class EdifactStackQuery:
"""
The EdifactStackQuery contains the data you need to provide to a MIG reader to return you the :class:`EdifactStack`
of an element
"""

#: the key of the segment group, f.e. 'root' or 'SG5' or 'SG12'
segment_group_key: str = attr.ib(validator=attr.validators.instance_of(str))
segment_group_key: str = attrs.field(validator=attrs.validators.instance_of(str))
#: the segment code, f.e. 'NAD' or 'DTM'
segment_code: str = attr.ib(validator=attr.validators.matches_re("^[A-Z]+$"))
segment_code: str = attrs.field(validator=attrs.validators.matches_re("^[A-Z]+$"))
#: the data element id, f.e. '0068'
data_element_id: str = attr.ib(validator=attr.validators.matches_re(r"^\d{4}$"))
data_element_id: str = attrs.field(validator=attrs.validators.matches_re(r"^\d{4}$"))
#: the name of the element, f.e. "MP-ID" or "Kundennummer" or "Identifikator"; Is None for Value Pools
name: Optional[str] = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(str)))
predecessor_qualifier: Optional[str] = attr.ib(
default=None, validator=attr.validators.optional(attr.validators.matches_re(r"^[A-Z\d]+$"))
name: Optional[str] = attrs.field(validator=attrs.validators.optional(attrs.validators.instance_of(str)))
predecessor_qualifier: Optional[str] = attrs.field(
default=None, validator=attrs.validators.optional(attrs.validators.matches_re(r"^[A-Z\d]+$"))
)
"""
Some names are not really unique. F.e. all date time fields carry more or less the same name in the AHB.
So to distinguish between them you may provide the predecissing qualifier.
In case of 'DTM+137++what_youre_looking_for' the predecessor qualifier is '137'
"""
section_name: Optional[str] = attr.ib(
default=None, validator=attr.validators.optional(attr.validators.instance_of(str))
section_name: Optional[str] = attrs.field(
default=None, validator=attrs.validators.optional(attrs.validators.instance_of(str))
)
"""
The section name (f.e. 'Nachrichten-Kopfsegment') might also be used for MIG<->AHB matching
Expand Down
Loading

0 comments on commit 8c30dcb

Please sign in to comment.