Skip to content

Commit

Permalink
Merge pull request #204 from hf-kklein/re-structure
Browse files Browse the repository at this point in the history
chore: restructure model classes and functions into separate modules
  • Loading branch information
mj0nez authored Jan 15, 2025
2 parents a3399e3 + 09c95b0 commit e106559
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 137 deletions.
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ The function `is_bdew_working_day` considers both national **and** state wide ho
```python
from datetime import date

from bdew_datetimes.periods import is_bdew_working_day
from bdew_datetimes import is_bdew_working_day

assert is_bdew_working_day(date(2023, 1, 1)) is False # Neujahr (national holiday)
assert is_bdew_working_day(date(2023, 1, 2)) is True # regular weekday
Expand All @@ -66,7 +66,7 @@ You can also get the next or previous working day for any date:
```python
from datetime import date

from bdew_datetimes.periods import get_next_working_day, get_previous_working_day
from bdew_datetimes import get_next_working_day, get_previous_working_day

assert get_next_working_day(date(2023, 1, 1)) == date(2023, 1, 2) # the next working day after Neujahr
assert get_previous_working_day(date(2023, 1, 1)) == date(2022, 12, 30) # the last working day of 2022
Expand All @@ -75,31 +75,36 @@ assert get_next_working_day(date(2023, 1, 20)) == date(2023, 1, 23) # the next

### Calculate Statutory Periods
Statutory periods define the maximum time between e.g. the EDIFACT message for the "Anmeldung" and the actual start of supply ("Lieferbeginn").

```python
from datetime import date

from bdew_datetimes.periods import DayType, EndDateType, Period, add_frist
from bdew_datetimes import add_frist
from bdew_datetimes import Period
from bdew_datetimes.enums import DayType, EndDateType

# Eingang der Anmeldung des LFN erfolgt am 04.07.2016. Der Mindestzeitraum von zehn WT
# beginnt am 05.07.2016 und endet am 18.07.2016. Frühestes zulässiges Anmeldedatum
# ist damit der 19.07.2016, sodass die Marktlokation dem LFN frühestens zum Beginn
# des vorgenannten Tages zugeordnet wird.
eingang_der_anmeldung = date(2016, 7, 4)
gesetzliche_frist = Period(
10,
DayType.WORKING_DAY,
end_date_type=EndDateType.EXCLUSIVE
# lieferbeginn is the exclusive end of the previous supply contract
10,
DayType.WORKING_DAY,
end_date_type=EndDateType.EXCLUSIVE
# lieferbeginn is the exclusive end of the previous supply contract
)
fruehest_moeglicher_lieferbeginn = add_frist(eingang_der_anmeldung, gesetzliche_frist)
assert fruehest_moeglicher_lieferbeginn == date(2016, 7, 19)
```
### Calculate "Liefer- and Fristenmonate"
Liefer- and Fristenmonat are concepts used in MaBiS and GPKE:

```python
from datetime import date

from bdew_datetimes.periods import get_nth_working_day_of_month, MonthType
from bdew_datetimes import get_nth_working_day_of_month
from bdew_datetimes.enums import MonthType

# returns the 18th working day of the current month in Germany
get_nth_working_day_of_month(18)
Expand Down
26 changes: 21 additions & 5 deletions src/bdew_datetimes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,25 @@
bdew_datetimes is a package that models the BDEW holiday, which is relevant for German utilities
"""

from pytz import timezone
from .calendar import BdewDefinedHolidays, create_bdew_calendar
from .german_time_zone import GERMAN_TIME_ZONE
from .models import Period
from .periods import (
add_frist,
get_next_working_day,
get_nth_working_day_of_month,
get_previous_working_day,
is_bdew_working_day,
)

from .calendar import create_bdew_calendar

GERMAN_TIME_ZONE = timezone("Europe/Berlin")
__all__ = ["GERMAN_TIME_ZONE", "create_bdew_calendar"]
__all__ = [
"create_bdew_calendar",
"BdewDefinedHolidays",
"Period",
"is_bdew_working_day",
"get_next_working_day",
"get_previous_working_day",
"add_frist",
"get_nth_working_day_of_month",
"GERMAN_TIME_ZONE",
]
3 changes: 3 additions & 0 deletions src/bdew_datetimes/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,6 @@ def create_bdew_calendar() -> HolidaySum:
# https://github.com/vacanza/python-holidays/blob/v0.53/holidays/holiday_base.py#L1164
result.language = original_language_before_adding_subdivisions
return result


__all__ = ["BdewDefinedHolidays", "create_bdew_calendar"]
69 changes: 69 additions & 0 deletions src/bdew_datetimes/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""enums used inside the package"""

from enum import Enum


class Division(Enum):
"""
Allows to distinguish divisions used by German utilities, German "Sparte".
"""

STROM = 1 #: electricity
GAS = 2 #: gas


class DayType(str, Enum):
"""
An enum to differentiate between calendar days and working days.
"""

WORKING_DAY = "WT" #: working day, German "Werktag"
CALENDAR_DAY = "KT" #: calendar day, German "Kalendertag"


class EndDateType(Enum):
"""
An enum to distinguish inclusive and exclusive end dates.
"""

INCLUSIVE = 1
"""
If a contract ends with the year 2022 and the end date is denoted as "2022-12-31",
then the end date is inclusive. Most dates in human (spoken) communication are meant
inclusively.
"""

EXCLUSIVE = 2
"""
If a contract ends with the year 2022 and the end date is denoted as "2023-01-01",
then the end date is exclusive. Most end dates handled by technical systems are meant
exclusively.
"""


class MonthType(Enum):
"""
When calculating periods defined as 'nth working day of a month' the
BNetzA regulations distinguish between two types of month which are
modelled in this enum.
Some periods refer to the "Liefermonat", others to the "Fristenmonat".
"""

LIEFERMONAT = 1
"""
The "Liefermonat" is the month in which the supply starts.
"""
FRISTENMONAT = 2
"""
The grid operators prefer a key date based handling of supply contracts.
The key date in these cases is usually expressed as a specific working day
in the so called "Fristenmonat".
The "Fristenmonat" starts at the first day of the month
_before_ the "Liefermonat".
Quote: 'Nach der Festlegung BK6-06-009 (GPKE) der Monat vor dem Liefermonat.'
"""
# pylint:disable=line-too-long
# source: https://www.bundesnetzagentur.de/DE/Beschlusskammern/1_GZ/BK6-GZ/_bis_2010/2006/BK6-06-009/BK6-06-009_Beschluss_download.pdf?__blob=publicationFile&v=5


__all__ = ["Division", "EndDateType", "MonthType", "DayType"]
15 changes: 5 additions & 10 deletions src/bdew_datetimes/german_strom_and_gas_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""

from datetime import datetime, time
from enum import Enum
from typing import Callable

# The problem with the stdlib zoneinfo is, that the availability of timezones
Expand All @@ -14,16 +13,9 @@
# datasource for timezone information.
from pytz import utc

from bdew_datetimes import GERMAN_TIME_ZONE
from bdew_datetimes.enums import Division


class Division(Enum):
"""
Allows to distinguish divisions used by German utilities, German "Sparte".
"""

STROM = 1 #: electricity
GAS = 2 #: gas
from .german_time_zone import GERMAN_TIME_ZONE


def _get_german_local_time(date_time: datetime) -> time:
Expand Down Expand Up @@ -102,3 +94,6 @@ def is_xtag_limit(date_time: datetime, division: Division) -> bool:
f"The division must either be 'Strom' or 'Gas': '{division}'"
)
return xtag_evaluator(date_time)


__all__ = ["is_gastag_limit", "is_stromtag_limit"]
6 changes: 6 additions & 0 deletions src/bdew_datetimes/german_time_zone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""static timezone object for Berlin/Germany"""

from pytz import timezone

GERMAN_TIME_ZONE = timezone("Europe/Berlin")
__all__ = ["GERMAN_TIME_ZONE"]
57 changes: 57 additions & 0 deletions src/bdew_datetimes/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""model classes used in this package"""

from dataclasses import dataclass
from typing import Literal, Union

from bdew_datetimes.enums import DayType, EndDateType

_DayTyp = Union[DayType, Literal["WT", "KT"]]


@dataclass
class Period:
"""
A period is a German "Frist": A tuple that consists of a number of days and a day type.
"""

number_of_days: int
"""
number of days (might be any value <0, >0 or ==0)
"""
day_type: DayType
"""
the kind of days to add/subtract
"""

def __init__(
self,
number_of_days: int,
day_type: _DayTyp,
end_date_type: EndDateType = EndDateType.EXCLUSIVE,
):
"""
Initialize the Period by providing a number of days and a day_type which define the period.
"""
self.number_of_days = number_of_days
# If the Period is about something ending (e.g. a contract), then the user may
# provide an end_date_type.
# Internally we handle all end dates as exclusive, because:
# https://hf-kklein.github.io/exclusive_end_dates.github.io/
if end_date_type == EndDateType.INCLUSIVE:
if self.number_of_days > 0:
self.number_of_days = self.number_of_days - 1
elif self.number_of_days < 0:
self.number_of_days = self.number_of_days + 1
if isinstance(day_type, DayType):
pass
elif isinstance(day_type, str):
day_type = DayType(day_type)
else:
raise ValueError(
f"'{day_type}' is not an allowed value; Check the typing"
)
self.day_type: DayType = day_type


__all__ = ["Period"]
Loading

0 comments on commit e106559

Please sign in to comment.