diff --git a/README.md b/README.md index 5420357..4c78146 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -75,10 +75,13 @@ 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 @@ -86,20 +89,22 @@ from bdew_datetimes.periods import DayType, EndDateType, Period, add_frist # 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) diff --git a/src/bdew_datetimes/__init__.py b/src/bdew_datetimes/__init__.py index 516b4dc..62eeeb2 100644 --- a/src/bdew_datetimes/__init__.py +++ b/src/bdew_datetimes/__init__.py @@ -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", +] diff --git a/src/bdew_datetimes/calendar.py b/src/bdew_datetimes/calendar.py index e4b5fcb..471d451 100644 --- a/src/bdew_datetimes/calendar.py +++ b/src/bdew_datetimes/calendar.py @@ -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"] diff --git a/src/bdew_datetimes/enums.py b/src/bdew_datetimes/enums.py new file mode 100644 index 0000000..485ada4 --- /dev/null +++ b/src/bdew_datetimes/enums.py @@ -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"] diff --git a/src/bdew_datetimes/german_strom_and_gas_tag.py b/src/bdew_datetimes/german_strom_and_gas_tag.py index ea406e0..6ffa79a 100644 --- a/src/bdew_datetimes/german_strom_and_gas_tag.py +++ b/src/bdew_datetimes/german_strom_and_gas_tag.py @@ -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 @@ -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: @@ -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"] diff --git a/src/bdew_datetimes/german_time_zone.py b/src/bdew_datetimes/german_time_zone.py new file mode 100644 index 0000000..16bbde8 --- /dev/null +++ b/src/bdew_datetimes/german_time_zone.py @@ -0,0 +1,6 @@ +"""static timezone object for Berlin/Germany""" + +from pytz import timezone + +GERMAN_TIME_ZONE = timezone("Europe/Berlin") +__all__ = ["GERMAN_TIME_ZONE"] diff --git a/src/bdew_datetimes/models.py b/src/bdew_datetimes/models.py new file mode 100644 index 0000000..6e9d89c --- /dev/null +++ b/src/bdew_datetimes/models.py @@ -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"] diff --git a/src/bdew_datetimes/periods.py b/src/bdew_datetimes/periods.py index 94013b8..dd64e11 100644 --- a/src/bdew_datetimes/periods.py +++ b/src/bdew_datetimes/periods.py @@ -5,15 +5,16 @@ """ import datetime -from dataclasses import dataclass from datetime import date -from enum import Enum -from typing import Literal, Optional, Union +from typing import Optional from dateutil.relativedelta import relativedelta from holidays import SAT, SUN # type:ignore[attr-defined] -from bdew_datetimes import GERMAN_TIME_ZONE, create_bdew_calendar +from bdew_datetimes.calendar import create_bdew_calendar +from bdew_datetimes.enums import DayType, EndDateType, MonthType +from bdew_datetimes.german_time_zone import GERMAN_TIME_ZONE +from bdew_datetimes.models import Period # https://www.bundesnetzagentur.de/DE/Beschlusskammern/1_GZ/BK6-GZ/2020/BK6-20-160/Mitteilung_Nr_2/Leseversion_GPKE.pdf # pages 15 onwards @@ -24,84 +25,6 @@ """ -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. - """ - - -_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 - - def is_bdew_working_day(candidate: date) -> bool: """ Returns true if and only if the given candidate is a day relevant for the period calculation. @@ -172,31 +95,6 @@ def add_frist(start: date, period: Period) -> date: return result -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 - - def get_nth_working_day_of_month( number_of_working_day_in_month: int, month_type: MonthType = MonthType.LIEFERMONAT, @@ -233,3 +131,13 @@ def get_nth_working_day_of_month( else: raise ValueError(f"Unhandled month_type {month_type}") return result + + +# pylint:disable=duplicate-code +__all__ = [ + "is_bdew_working_day", + "get_next_working_day", + "get_previous_working_day", + "add_frist", + "get_nth_working_day_of_month", +] diff --git a/tests/test_german_strom_and_gas_tag.py b/tests/test_german_strom_and_gas_tag.py index 375297c..e51d829 100644 --- a/tests/test_german_strom_and_gas_tag.py +++ b/tests/test_german_strom_and_gas_tag.py @@ -2,14 +2,14 @@ import pytest -from bdew_datetimes import GERMAN_TIME_ZONE +from bdew_datetimes.enums import Division from bdew_datetimes.german_strom_and_gas_tag import ( - Division, has_no_utc_offset, is_gastag_limit, is_stromtag_limit, is_xtag_limit, ) +from bdew_datetimes.german_time_zone import GERMAN_TIME_ZONE @pytest.mark.parametrize( diff --git a/tests/test_period.py b/tests/test_period.py index bae512a..b359184 100644 --- a/tests/test_period.py +++ b/tests/test_period.py @@ -2,12 +2,9 @@ import pytest +from bdew_datetimes.enums import DayType, EndDateType, MonthType +from bdew_datetimes.models import Period, _DayTyp from bdew_datetimes.periods import ( - DayType, - EndDateType, - MonthType, - Period, - _DayTyp, add_frist, get_next_working_day, get_nth_working_day_of_month,