Skip to content

Commit

Permalink
doc(periods): add types
Browse files Browse the repository at this point in the history
  • Loading branch information
bonjourmauko committed Sep 17, 2024
1 parent 39f0cdb commit 9c6b7c1
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 117 deletions.
2 changes: 2 additions & 0 deletions openfisca_core/periods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#
# See: https://www.python.org/dev/peps/pep-0008/#imports

from . import types
from .config import (
INSTANT_PATTERN,
date_by_instant_cache,
Expand Down Expand Up @@ -70,4 +71,5 @@
"ETERNITY",
"ISOFORMAT",
"ISOCALENDAR",
"types",
]
5 changes: 3 additions & 2 deletions openfisca_core/periods/_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
from pendulum.datetime import Date
from pendulum.parsing import ParserError

from . import types as t
from .date_unit import DateUnit
from .instant_ import Instant
from .period_ import Period

invalid_week = re.compile(r".*(W[1-9]|W[1-9]-[0-9]|W[0-5][0-9]-0)$")


def _parse_period(value: str) -> Optional[Period]:
def _parse_period(value: str) -> Optional[t.Period]:
"""Parses ISO format/calendar periods.
Such as "2012" or "2015-03".
Expand Down Expand Up @@ -57,7 +58,7 @@ def _parse_period(value: str) -> Optional[Period]:
return Period((unit, instant, 1))


def _parse_unit(value: str) -> DateUnit:
def _parse_unit(value: str) -> t.DateUnit:
"""Determine the date unit of a date string.
Args:
Expand Down
8 changes: 6 additions & 2 deletions openfisca_core/periods/config.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import re

from pendulum.datetime import Date

from . import types as t

# Matches "2015", "2015-01", "2015-01-01"
# Does not match "2015-13", "2015-12-32"
INSTANT_PATTERN = re.compile(
r"^\d{4}(-(0[1-9]|1[012]))?(-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))?$"
)

date_by_instant_cache: dict = {}
str_by_instant_cache: dict = {}
date_by_instant_cache: dict[t.Instant, Date] = {}
str_by_instant_cache: dict[t.Instant, t.InstantStr] = {}
year_or_month_or_day_re = re.compile(
r"(18|19|20)\d{2}(-(0?[1-9]|1[0-2])(-([0-2]?\d|3[0-1]))?)?$"
)
6 changes: 4 additions & 2 deletions openfisca_core/periods/date_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

from strenum import StrEnum

from . import types as t


class DateUnitMeta(EnumMeta):
@property
def isoformat(self) -> tuple[DateUnit, ...]:
def isoformat(self) -> tuple[t.DateUnit, ...]:
"""Creates a :obj:`tuple` of ``key`` with isoformat items.
Returns:
Expand All @@ -28,7 +30,7 @@ def isoformat(self) -> tuple[DateUnit, ...]:
return DateUnit.DAY, DateUnit.MONTH, DateUnit.YEAR

@property
def isocalendar(self) -> tuple[DateUnit, ...]:
def isocalendar(self) -> tuple[t.DateUnit, ...]:
"""Creates a :obj:`tuple` of ``key`` with isocalendar items.
Returns:
Expand Down
68 changes: 38 additions & 30 deletions openfisca_core/periods/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import NoReturn, Optional
from __future__ import annotations

from typing import NoReturn, Optional, overload

import datetime
import os
Expand All @@ -7,12 +9,23 @@
from pendulum.parsing import ParserError

from . import _parsers, config
from . import types as t
from .date_unit import DateUnit
from .instant_ import Instant
from .period_ import Period


def instant(instant) -> Optional[Instant]:
@overload
def instant(instant: None) -> None:
...


@overload
def instant(instant: object) -> Instant:
...


def instant(instant: object | None) -> t.Instant | None:
"""Build a new instant, aka a triple of integers (year, month, day).
Args:
Expand Down Expand Up @@ -49,6 +62,8 @@ def instant(instant) -> Optional[Instant]:
"""

result: t.Instant | tuple[int, ...]

if instant is None:
return None
if isinstance(instant, Instant):
Expand All @@ -58,27 +73,28 @@ def instant(instant) -> Optional[Instant]:
raise ValueError(
f"'{instant}' is not a valid instant. Instants are described using the 'YYYY-MM-DD' format, for instance '2015-06-15'."
)
instant = Instant(int(fragment) for fragment in instant.split("-", 2)[:3])
result = Instant(int(fragment) for fragment in instant.split("-", 2)[:3])
elif isinstance(instant, datetime.date):
instant = Instant((instant.year, instant.month, instant.day))
result = Instant((instant.year, instant.month, instant.day))
elif isinstance(instant, int):
instant = (instant,)
result = (instant,)
elif isinstance(instant, list):
assert 1 <= len(instant) <= 3
instant = tuple(instant)
result = tuple(instant)
elif isinstance(instant, Period):
instant = instant.start
result = instant.start
else:
assert isinstance(instant, tuple), instant
assert 1 <= len(instant) <= 3
if len(instant) == 1:
return Instant((instant[0], 1, 1))
if len(instant) == 2:
return Instant((instant[0], instant[1], 1))
return Instant(instant)
result = instant
if len(result) == 1:
return Instant((result[0], 1, 1))
if len(result) == 2:
return Instant((result[0], result[1], 1))
return Instant(result)


def instant_date(instant: Optional[Instant]) -> Optional[datetime.date]:
def instant_date(instant: Optional[t.Instant]) -> Optional[datetime.date]:
"""Returns the date representation of an :class:`.Instant`.
Args:
Expand All @@ -105,7 +121,7 @@ def instant_date(instant: Optional[Instant]) -> Optional[datetime.date]:
return instant_date


def period(value) -> Period:
def period(value: object) -> t.Period:
"""Build a new period, aka a triple (unit, start_instant, size).
Args:
Expand All @@ -125,7 +141,7 @@ def period(value) -> Period:
Period((<DateUnit.DAY: 'day'>, Instant((2021, 1, 1)), 1))
>>> period(DateUnit.ETERNITY)
Period((<DateUnit.ETERNITY: 'eternity'>, Instant((1, 1, 1)), inf))
Period((<DateUnit.ETERNITY: 'eternity'>, Instant((1, 1, 1)), 0))
>>> period(2021)
Period((<DateUnit.YEAR: 'year'>, Instant((2021, 1, 1)), 1))
Expand Down Expand Up @@ -164,15 +180,9 @@ def period(value) -> Period:
return Period((DateUnit.DAY, instant(value), 1))

# We return an "eternity-period", for example
# ``<Period(('eternity', <Instant(1, 1, 1)>, inf))>``.
# ``<Period(('eternity', <Instant(1, 1, 1)>, 0))>``.
if str(value).lower() == DateUnit.ETERNITY:
return Period(
(
DateUnit.ETERNITY,
instant(datetime.date.min),
float("inf"),
)
)
return Period.eternity()

# For example ``2021`` gives
# ``<Period(('year', <Instant(2021, 1, 1)>, 1))>``.
Expand All @@ -181,7 +191,7 @@ def period(value) -> Period:

# Up to this point, if ``value`` is not a :obj:`str`, we desist.
if not isinstance(value, str):
_raise_error(value)
_raise_error(str(value))

# There can't be empty strings.
if not value:
Expand Down Expand Up @@ -267,7 +277,7 @@ def _raise_error(value: str) -> NoReturn:
raise ValueError(message)


def key_period_size(period: Period) -> str:
def key_period_size(period: t.Period) -> str:
"""Define a key in order to sort periods by length.
It uses two aspects: first, ``unit``, then, ``size``.
Expand All @@ -291,12 +301,10 @@ def key_period_size(period: Period) -> str:
"""

unit, start, size = period

return f"{unit_weight(unit)}_{size}"
return f"{unit_weight(period.unit)}_{period.size}"


def unit_weights() -> dict[str, int]:
def unit_weights() -> dict[t.DateUnit, int]:
"""Assign weights to date units.
Examples:
Expand All @@ -315,7 +323,7 @@ def unit_weights() -> dict[str, int]:
}


def unit_weight(unit: str) -> int:
def unit_weight(unit: t.DateUnit) -> int:
"""Retrieves a specific date unit weight.
Examples:
Expand Down
41 changes: 25 additions & 16 deletions openfisca_core/periods/instant_.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from __future__ import annotations

import pendulum
from pendulum.datetime import Date

from . import config
from . import types as t
from .date_unit import DateUnit


class Instant(tuple):
class Instant(tuple[int, int, int]):
"""An instant in time (year, month, day).
An :class:`.Instant` represents the most atomic and indivisible
Expand All @@ -13,10 +17,6 @@ class Instant(tuple):
Current implementation considers this unit to be a day, so
:obj:`instants <.Instant>` can be thought of as "day dates".
Args:
(tuple(tuple(int, int, int))):
The ``year``, ``month``, and ``day``, accordingly.
Examples:
>>> instant = Instant((2021, 9, 13))
Expand Down Expand Up @@ -78,35 +78,41 @@ class Instant(tuple):
"""

def __repr__(self):
def __repr__(self) -> str:
return f"{self.__class__.__name__}({super().__repr__()})"

def __str__(self):
def __str__(self) -> t.InstantStr:
instant_str = config.str_by_instant_cache.get(self)

if instant_str is None:
config.str_by_instant_cache[self] = instant_str = self.date.isoformat()
instant_str = t.InstantStr(self.date.isoformat())
config.str_by_instant_cache[self] = instant_str

return instant_str

@property
def date(self):
def date(self) -> Date:
instant_date = config.date_by_instant_cache.get(self)

if instant_date is None:
config.date_by_instant_cache[self] = instant_date = pendulum.date(*self)
instant_date = pendulum.date(*self)
config.date_by_instant_cache[self] = instant_date

return instant_date

@property
def day(self):
def day(self) -> int:
return self[2]

@property
def month(self):
def month(self) -> int:
return self[1]

def offset(self, offset, unit):
@property
def year(self) -> int:
return self[0]

def offset(self, offset: str | int, unit: t.DateUnit) -> t.Instant | None:
"""Increments/decrements the given instant with offset units.
Args:
Expand Down Expand Up @@ -193,6 +199,9 @@ def offset(self, offset, unit):
date = date.add(days=offset)
return self.__class__((date.year, date.month, date.day))

@property
def year(self):
return self[0]
return None

@classmethod
def eternity(cls) -> t.Instant:
"""Return an eternity instant."""
return cls((1, 1, 1))
Loading

0 comments on commit 9c6b7c1

Please sign in to comment.