From 066d67d4b84cb155884baf4988aa6cddb0a8a649 Mon Sep 17 00:00:00 2001 From: Ben Brown <9870007+brownben@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:43:39 +0100 Subject: [PATCH] feat: generate calendars for league events --- backend/pyproject.toml | 1 + backend/requirements.txt | 17 +++++++++++++ backend/src/routes/leagues.py | 45 +++++++++++++++++++++++++++++++++++ backend/src/types/ics.pyi | 22 +++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 backend/src/types/ics.pyi diff --git a/backend/pyproject.toml b/backend/pyproject.toml index fd84b741..ec78247b 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "fastapi~=0.110.1", "google-auth~=2.29.0", "httpx~=0.27.0", + "ics~=0.7.2", "piccolo==0.119.0", "uvicorn~=0.29.0", "requests~=2.31.0" diff --git a/backend/requirements.txt b/backend/requirements.txt index 74da6789..42ea3841 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,9 +4,13 @@ anyio==4.3.0 # via # httpx # starlette +arrow==1.3.0 + # via ics async-timeout==4.0.3 # via asyncpg asyncpg==0.29.0 +attrs==23.2.0 + # via ics black==24.3.0 # via piccolo cachetools==5.3.3 @@ -43,6 +47,7 @@ h11==0.14.0 httpcore==1.0.5 # via httpx httpx==0.27.0 +ics==0.7.2 idna==3.6 # via # anyio @@ -74,9 +79,17 @@ pydantic==1.10.15 # via # fastapi # piccolo +python-dateutil==2.9.0.post0 + # via + # arrow + # ics requests==2.31.0 rsa==4.9 # via google-auth +six==1.16.0 + # via + # ics + # python-dateutil sniffio==1.3.1 # via # anyio @@ -85,6 +98,10 @@ starlette==0.37.2 # via fastapi targ==0.4.0 # via piccolo +tatsu==5.12.0 + # via ics +types-python-dateutil==2.9.0.20240316 + # via arrow typing-extensions==4.11.0 # via # fastapi diff --git a/backend/src/routes/leagues.py b/backend/src/routes/leagues.py index d8e4ee83..281c18a6 100644 --- a/backend/src/routes/leagues.py +++ b/backend/src/routes/leagues.py @@ -2,7 +2,11 @@ from typing import Awaitable, Iterable from fastapi import Depends, Path +from fastapi.responses import StreamingResponse from fastapi.routing import APIRouter +from ics import Calendar +from ics import Event as CalendarEvent +from ics import Organizer as CalendarOrganizer from ..database import ( Competitors, @@ -166,6 +170,47 @@ async def get_league_events( return events +@router.get("/{league_name}/events/calendar") +async def get_league_events_calendar( + league_name: str = Path( + title="League Name", + description="Name of the league to get the results for", + example="Sprintelope 2021", + ), +) -> StreamingResponse: + league, events = await asyncio.gather( + Leagues.get_by_name(league_name), + Events.get_by_league(league_name), + ) + + if not league: + raise HTTP_404(f"Couldn't find league with name `{league_name}`") + + calendar = Calendar() + + for event in events: + description = "" + + if event.part_of: + description += f"Part of {event.part_of}\n" + if event.more_information: + description += f"\n{event.more_information}" + + calendar_event = CalendarEvent( + name=f"{league.name} - {event.name}", + begin=event.date, + description=description, + location=event.name, + url=event.website, + organizer=CalendarOrganizer(email="", common_name=event.organiser), + ) + calendar_event.make_all_day() + + calendar.events.add(calendar_event) + + return StreamingResponse(calendar.serialize_iter(), media_type="text/calendar") + + def get_results_for_event( event: LeagueEvent, league_class: LeagueClass ) -> Awaitable[Iterable[Result]]: diff --git a/backend/src/types/ics.pyi b/backend/src/types/ics.pyi new file mode 100644 index 00000000..254416f5 --- /dev/null +++ b/backend/src/types/ics.pyi @@ -0,0 +1,22 @@ +from datetime import date +from typing import Iterable + +class Calendar: + events: set[Event] + def __init__(self) -> None: ... + def serialize_iter(self) -> Iterable[str]: ... + +class Event: + def __init__( + self, + name: str | None, + begin: date | None, + description: str | None, + location: str | None, + url: str | None, + organizer: Organizer | None, + ) -> None: ... + def make_all_day(self) -> None: ... + +class Organizer: + def __init__(self, email: str | None, common_name: str | None) -> None: ...