Move towards a more object-oriented API

This commit is contained in:
Fabian Müller 2023-02-18 04:19:38 +01:00
parent 70a069fe95
commit 00b74e8782
2 changed files with 73 additions and 57 deletions

View File

@ -10,7 +10,7 @@ import recurring_ical_events
import yaml import yaml
from asyncache import cached from asyncache import cached
from cachetools import TTLCache from cachetools import TTLCache, LRUCache
from icalendar import Calendar from icalendar import Calendar
from fabcal.models import CalendarEvent from fabcal.models import CalendarEvent
@ -22,8 +22,20 @@ class ConfiguredCalendar(NamedTuple):
default_color: str = None default_color: str = None
# this is not ideal, since it will read the file (at most) every 2 minutes, but there is no good way to cache the class CalendarClient:
# settings, e.g., by storing it in the FastAPI object def __init__(self, url: str):
self.url = url
async def get(self, session):
async with session.get(self.url) as response:
response.raise_for_status()
assert response.content_type.lower() == "text/calendar"
return await response.text()
# this will be cached permanently, i.e., the server process needs to be restarted to apply config changes
@cached(LRUCache(100))
def read_calendars_from_config_file(): def read_calendars_from_config_file():
with open("config.yml") as f: with open("config.yml") as f:
data = yaml.safe_load(f) data = yaml.safe_load(f)
@ -32,25 +44,21 @@ def read_calendars_from_config_file():
yield ConfiguredCalendar(**calendar) yield ConfiguredCalendar(**calendar)
@cached(TTLCache(maxsize=100, ttl=int(os.environ.get("FABCAL_CACHE_EXPIRE", 120))))
async def get_data() -> Dict[ConfiguredCalendar, str]:
calendars = list(read_calendars_from_config_file())
class CombinedCalendarClient:
def __init__(self, configured_calendars: List[ConfiguredCalendar]):
# make sure it's a list since read_calendars_from_config_file() returns an iterator
self.configured_calendars = list(configured_calendars)
async def fetch_calendars(self) -> Dict[ConfiguredCalendar, str]:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
calendar_clients = [CalendarClient(calendar.url) for calendar in self.configured_calendars]
responses = await asyncio.gather(*[calendar.get(session) for calendar in calendar_clients])
async def get(url: str) -> str: return dict(zip(self.configured_calendars, responses))
async with session.get(url) as response:
response.raise_for_status()
assert response.content_type.lower() == "text/calendar"
return await response.text() @staticmethod
def combine_calendars(data: Dict[ConfiguredCalendar, str]) -> Calendar:
responses = await asyncio.gather(*[get(calendar.url) for calendar in calendars])
return dict(zip(calendars, responses))
def combined_calendar(data: Dict[ConfiguredCalendar, str]) -> Calendar:
combined_calendar = Calendar() combined_calendar = Calendar()
combined_calendar.add("prodid", "-//FabCal//NONSGML//EN") combined_calendar.add("prodid", "-//FabCal//NONSGML//EN")
@ -83,12 +91,22 @@ def combined_calendar(data: Dict[ConfiguredCalendar, str]) -> Calendar:
return combined_calendar return combined_calendar
async def fetch_and_combine_calendars(self) -> Calendar:
return self.combine_calendars(await self.fetch_calendars())
@cached(TTLCache(maxsize=100, ttl=int(os.environ.get("FABCAL_CACHE_EXPIRE", 120))))
async def get_data() -> Calendar:
client = CombinedCalendarClient(read_calendars_from_config_file())
combined_calendar = await client.fetch_and_combine_calendars()
return combined_calendar
def get_tzinfo(): def get_tzinfo():
return timezone(timedelta(hours=1)) return timezone(timedelta(hours=1))
def get_events_from_calendar_string(cal: Calendar) -> List[CalendarEvent]: def get_events_from_calendar(cal: Calendar) -> List[CalendarEvent]:
""" """
Generate list of events from calendar vevents. Generate list of events from calendar vevents.
Expands recurring events for +- one year. Expands recurring events for +- one year.
@ -147,9 +165,7 @@ def get_events_from_calendar_string(cal: Calendar) -> List[CalendarEvent]:
async def get_future_events(): async def get_future_events():
cal = combined_calendar(await get_data()) events = get_events_from_calendar(await get_data())
events = get_events_from_calendar_string(cal)
today = datetime.today().date() today = datetime.today().date()

View File

@ -1,7 +1,7 @@
from fastapi import APIRouter from fastapi import APIRouter
from starlette.responses import Response from starlette.responses import Response
from fabcal.calendar_client import combined_calendar, get_data from fabcal.calendar_client import get_data
router = APIRouter() router = APIRouter()
@ -10,7 +10,7 @@ router = APIRouter()
@router.get("/events.ics") @router.get("/events.ics")
async def events_ics(): async def events_ics():
return Response( return Response(
combined_calendar(await get_data()).to_ical(True), (await get_data()).to_ical(True),
headers={ headers={
"content-type": "text/calendar", "content-type": "text/calendar",
}, },