diff --git a/api/advent22_api/core/depends.py b/api/advent22_api/core/depends.py index 1307a3e..514bb23 100644 --- a/api/advent22_api/core/depends.py +++ b/api/advent22_api/core/depends.py @@ -8,7 +8,7 @@ from PIL import Image, ImageFont from .advent_image import _XY, AdventImage from .calendar_config import CalendarConfig, get_calendar_config from .config import Config, get_config -from .helpers import EventDays, Random, list_images_auto, load_image, set_len +from .helpers import EventDates, Random, list_images_auto, load_image, set_len from .webdav import WebDAV @@ -22,18 +22,18 @@ async def get_all_sorted_days( return sorted(set(door.day for door in cal_cfg.doors)) -async def get_all_event_days( +async def get_all_event_dates( days: list[int] = Depends(get_all_sorted_days), -) -> EventDays: +) -> EventDates: """ Aktueller Kalender-Zeitraum """ - return EventDays.get( + return EventDates( today=date.today(), begin_month=12, begin_day=1, - events_after=[day - 1 for day in days], + events=[day - 1 for day in days], closing_after=90, ) diff --git a/api/advent22_api/core/helpers.py b/api/advent22_api/core/helpers.py index cb124b0..6ba0b87 100644 --- a/api/advent22_api/core/helpers.py +++ b/api/advent22_api/core/helpers.py @@ -7,7 +7,6 @@ from typing import Any, Self, Sequence, TypeVar from fastapi.responses import StreamingResponse from PIL import Image -from pydantic import BaseModel from .config import get_config from .webdav import WebDAV @@ -72,25 +71,37 @@ async def api_return_image(img: Image.Image) -> StreamingResponse: ) -class EventDays2: +class EventDates: """ Events in einem Ereigniszeitraum """ __overall_duration: timedelta - __events: dict[int, date] + dates: list[date] + @property def first(self) -> date: """Datum des ersten Ereignisses""" - return self.__events[min(self.__events.keys())] + return self.dates[0] + def get_next(self, *, today: date) -> date | None: + """Datum des nächsten Ereignisses""" + return next((event for event in self.dates if event > today), None) + + @property + def next(self) -> date | None: + """Datum des nächsten Ereignisses""" + return self.get_next(today=datetime.today().date()) + + @property def last(self) -> date: """Datum des letzten Ereignisses""" - return self.__events[max(self.__events.keys())] + return self.dates[-1] + @property def end(self) -> date: """Letztes Datum des Ereigniszeitraums""" - return self.__events[max(self.__events.keys())] + return self.first + self.__overall_duration def __init__( self, @@ -100,14 +111,15 @@ class EventDays2: # month/day when events begin begin_month: int, begin_day: int, - # events: e.g. a 2 means there is an event 2 days after begin - # -> assume sorted (asc) - events_after: list[int], + # events: e.g. a 2 means there is an event on the 2nd day + # i.e. 1 day after begin + # - assume sorted (ascending) + events: list[int], # countdown to closing begins after last event closing_after: int, ) -> None: # account for the last event, then add closing period - self.__overall_duration = timedelta(days=events_after[-1] + closing_after) + self.__overall_duration = timedelta(days=events[-1] - 1 + closing_after) # the events may begin last year, this year or next year maybe_begin = ( @@ -121,65 +133,4 @@ class EventDays2: ) # all event dates - self.__events = { - event_after: begin + timedelta(days=event_after - 1) - for event_after in events_after - } - - -class EventDays(BaseModel): - """ - Kenndaten eines Ereigniszeitraums: - - - `first`: Datum des ersten Ereignisses - - `next`: Datum des nächsten Ereignisses - - `last`: Datum des letzten Ereignisses - - `end`: Letztes Datum des Ereigniszeitraums - """ - - first: date - next: date | None - last: date - end: date - - @classmethod - def get( - cls, - *, - # current date - today: date, - # month/day when events begin - begin_month: int, - begin_day: int, - # events: e.g. a 2 means there is an event 2 days after begin - # -> assume sorted (asc) - events_after: list[int], - # countdown to closing begins after last event - closing_after: int, - ) -> Self: - """ - Kenndaten des aktuellen (laufenden oder zukünftigen) Ereigniszeitraums bestimmen - """ - - # account for the last event, then add closing period - duration = timedelta(days=events_after[-1] + closing_after) - - # the events may begin last year, this year or next year - maybe_begin = ( - datetime(today.year + year_diff, begin_month, begin_day).date() - for year_diff in (-1, 0, +1) - ) - - # find the first begin where the end date is in the future - begin = next(begin for begin in maybe_begin if today <= (begin + duration)) - - # all event dates - events = [begin + timedelta(days=event_after) for event_after in events_after] - - # return relevant event dates - return cls( - first=events[0], - next=next((event for event in events if event > today), None), - last=events[-1], - end=begin + duration, - ) + self.dates = [begin + timedelta(days=event - 1) for event in events] diff --git a/api/advent22_api/routers/admin.py b/api/advent22_api/routers/admin.py index 73021f1..7104f03 100644 --- a/api/advent22_api/routers/admin.py +++ b/api/advent22_api/routers/admin.py @@ -3,11 +3,11 @@ from datetime import date from fastapi import APIRouter, Depends from pydantic import BaseModel -from advent22_api.core.helpers import EventDays +from advent22_api.core.helpers import EventDates from ..core.calendar_config import CalendarConfig, DoorsSaved, get_calendar_config from ..core.config import Config, get_config -from ..core.depends import get_all_event_days, get_all_image_names, get_all_parts +from ..core.depends import get_all_event_dates, get_all_image_names, get_all_parts from ..core.settings import SETTINGS from ._security import require_admin, user_is_admin @@ -59,7 +59,7 @@ async def get_config_model( _: None = Depends(require_admin), cfg: Config = Depends(get_config), cal_cfg: CalendarConfig = Depends(get_calendar_config), - event_days: EventDays = Depends(get_all_event_days), + event_dates: EventDates = Depends(get_all_event_dates), ) -> ConfigModel: """ Kombiniert aus privaten `settings`, `config` und `calendar_config` @@ -69,10 +69,10 @@ async def get_config_model( { "puzzle": { "solution": cfg.puzzle.solution, - "first": event_days.first, - "next": event_days.next, - "last": event_days.last, - "end": event_days.end, + "first": event_dates.first, + "next": event_dates.next, + "last": event_dates.last, + "end": event_dates.end, "seed": cfg.puzzle.random_seed, }, "calendar": { diff --git a/api/advent22_api/routers/user.py b/api/advent22_api/routers/user.py index bb0f992..c522f6c 100644 --- a/api/advent22_api/routers/user.py +++ b/api/advent22_api/routers/user.py @@ -5,8 +5,8 @@ from fastapi.responses import StreamingResponse from PIL import Image from ..core.calendar_config import CalendarConfig, DoorsSaved, get_calendar_config -from ..core.depends import get_all_event_days, get_day_image -from ..core.helpers import EventDays, api_return_image, load_image +from ..core.depends import get_all_event_dates, get_day_image +from ..core.helpers import EventDates, api_return_image, load_image from ._security import user_can_view_day, user_is_admin router = APIRouter(prefix="/user", tags=["user"]) @@ -58,16 +58,16 @@ async def get_doors( @router.get("/next_door") async def get_next_door( - event_days: EventDays = Depends(get_all_event_days), + event_dates: EventDates = Depends(get_all_event_dates), ) -> int | None: """ Zeit in ms, bis das nächste Türchen öffnet """ - if event_days.next is None: + if event_dates.next is None: return None - dt = datetime.combine(event_days.next, datetime.min.time()) + dt = datetime.combine(event_dates.next, datetime.min.time()) td = dt - datetime.now() return int(td.total_seconds() * 1000) diff --git a/api/test/test_event_dates.py b/api/test/test_event_dates.py new file mode 100644 index 0000000..ad55ea1 --- /dev/null +++ b/api/test/test_event_dates.py @@ -0,0 +1,88 @@ +from datetime import date + +from advent22_api.core.helpers import EventDates + + +def test_get_before(): + today = date(2023, 11, 30) + + ed = EventDates( + today=today, + begin_month=12, + begin_day=1, + events=list(range(1, 25)), + closing_after=5, + ) + + assert ed.first == date(2023, 12, 1) + assert ed.get_next(today=today) == date(2023, 12, 1) + assert ed.last == date(2023, 12, 24) + assert ed.end == date(2023, 12, 29) + + +def test_get_after(): + today = date(2023, 12, 30) + + ed = EventDates( + today=today, + begin_month=12, + begin_day=1, + events=list(range(1, 25)), + closing_after=5, + ) + + assert ed.first == date(2024, 12, 1) + assert ed.get_next(today=today) == date(2024, 12, 1) + assert ed.last == date(2024, 12, 24) + assert ed.end == date(2024, 12, 29) + + +def test_get_during_events(): + today = date(2023, 12, 10) + + ed = EventDates( + today=today, + begin_month=12, + begin_day=1, + events=list(range(1, 25)), + closing_after=5, + ) + + assert ed.first == date(2023, 12, 1) + assert ed.get_next(today=today) == date(2023, 12, 11) + assert ed.last == date(2023, 12, 24) + assert ed.end == date(2023, 12, 29) + + +def test_get_during_closing(): + today = date(2023, 12, 29) + + ed = EventDates( + today=today, + begin_month=12, + begin_day=1, + events=list(range(1, 25)), + closing_after=5, + ) + + assert ed.first == date(2023, 12, 1) + assert ed.get_next(today=today) is None + assert ed.last == date(2023, 12, 24) + assert ed.end == date(2023, 12, 29) + + +def test_get_during_wrap(): + today = date(2024, 1, 1) + + ed = EventDates( + today=today, + begin_month=12, + begin_day=1, + events=list(range(1, 25)), + closing_after=8, + ) + + assert ed.first == date(2023, 12, 1) + assert ed.get_next(today=today) is None + assert ed.last == date(2023, 12, 24) + assert ed.end == date(2024, 1, 1) diff --git a/api/test/test_helpers.py b/api/test/test_helpers.py deleted file mode 100644 index ea25dda..0000000 --- a/api/test/test_helpers.py +++ /dev/null @@ -1,80 +0,0 @@ -from datetime import datetime - -from advent22_api.core.helpers import EventDays - - -class TestEventDays: - @staticmethod - def test_get_before(): - ed = EventDays.get( - today=datetime(2023, 11, 30).date(), - begin_month=12, - begin_day=1, - events_after=list(range(24)), - closing_after=5, - ) - - assert ed.first == datetime(2023, 12, 1).date() - assert ed.next == datetime(2023, 12, 1).date() - assert ed.last == datetime(2023, 12, 24).date() - assert ed.end == datetime(2023, 12, 29).date() - - @staticmethod - def test_get_after(): - ed = EventDays.get( - today=datetime(2023, 12, 30).date(), - begin_month=12, - begin_day=1, - events_after=list(range(24)), - closing_after=5, - ) - - assert ed.first == datetime(2024, 12, 1).date() - assert ed.next == datetime(2024, 12, 1).date() - assert ed.last == datetime(2024, 12, 24).date() - assert ed.end == datetime(2024, 12, 29).date() - - @staticmethod - def test_get_during_events(): - ed = EventDays.get( - today=datetime(2023, 12, 10).date(), - begin_month=12, - begin_day=1, - events_after=list(range(24)), - closing_after=5, - ) - - assert ed.first == datetime(2023, 12, 1).date() - assert ed.next == datetime(2023, 12, 11).date() - assert ed.last == datetime(2023, 12, 24).date() - assert ed.end == datetime(2023, 12, 29).date() - - @staticmethod - def test_get_during_closing(): - ed = EventDays.get( - today=datetime(2023, 12, 29).date(), - begin_month=12, - begin_day=1, - events_after=list(range(24)), - closing_after=5, - ) - - assert ed.first == datetime(2023, 12, 1).date() - assert ed.next is None - assert ed.last == datetime(2023, 12, 24).date() - assert ed.end == datetime(2023, 12, 29).date() - - @staticmethod - def test_get_during_wrap(): - ed = EventDays.get( - today=datetime(2024, 1, 1).date(), - begin_month=12, - begin_day=1, - events_after=list(range(24)), - closing_after=8, - ) - - assert ed.first == datetime(2023, 12, 1).date() - assert ed.next is None - assert ed.last == datetime(2023, 12, 24).date() - assert ed.end == datetime(2024, 1, 1).date()