diff --git a/Dockerfile b/Dockerfile index 18913e3..3b4d217 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,4 +18,4 @@ COPY main.py /app/ COPY static/ /app/static/ COPY templates/ /app/templates/ -ENTRYPOINT ["poetry", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000", "--use-colors"] +ENTRYPOINT ["poetry", "run", "uvicorn", "fabcal.app:app", "--host", "0.0.0.0", "--port", "5000", "--use-colors"] diff --git a/fabcal/__init__.py b/fabcal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fabcal/app.py b/fabcal/app.py new file mode 100644 index 0000000..d172c12 --- /dev/null +++ b/fabcal/app.py @@ -0,0 +1,33 @@ +import locale + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi_cache import FastAPICache +from fastapi_cache.backends.inmemory import InMemoryBackend + +from fabcal.routers import api_v1, frontend + + +app = FastAPI() + +app.mount("/static", StaticFiles(directory="static"), name="static") + +app.include_router(api_v1.router, prefix="/api/v1") +app.include_router(frontend.router, prefix="") + +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "https://fablab-altmuehlfranken.de", + "https://www.fablab-altmuehlfranken.de", + ], +) + + +locale.setlocale(locale.LC_TIME, locale.getlocale()) + + +@app.on_event("startup") +async def startup(): + FastAPICache.init(InMemoryBackend()) diff --git a/main.py b/fabcal/calendar_client.py similarity index 59% rename from main.py rename to fabcal/calendar_client.py index 63c0d09..9f64eee 100644 --- a/main.py +++ b/fabcal/calendar_client.py @@ -1,53 +1,16 @@ -import base64 -import locale import os from collections import OrderedDict from datetime import date, datetime, timedelta, timezone -from typing import List, NamedTuple - +from typing import List import aiohttp -import babel.dates import recurring_ical_events -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware -from fastapi.requests import Request -from fastapi.responses import Response -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates -from fastapi_cache import FastAPICache -from fastapi_cache.backends.inmemory import InMemoryBackend from fastapi_cache.decorator import cache from icalendar import Calendar, vText - -app = FastAPI() - -app.mount("/static", StaticFiles(directory="static"), name="static") - -templates = Jinja2Templates(directory="templates") - -app.add_middleware( - CORSMiddleware, - allow_origins=[ - "https://fablab-altmuehlfranken.de", - "https://www.fablab-altmuehlfranken.de", - ], -) - - -locale.setlocale(locale.LC_TIME, locale.getlocale()) - - -def get_calendar_url(): - url = os.environ["CALENDAR_URL"] - - # convenience feature - url = url.replace("webcal://", "https://") - - return url +from fabcal.models import CalendarEvent def sanitize(data: str): @@ -60,6 +23,15 @@ def sanitize(data: str): return cal.to_ical() +def get_calendar_url(): + url = os.environ["CALENDAR_URL"] + + # convenience feature + url = url.replace("webcal://", "https://") + + return url + + # caching strings works better than caching calendar objects @cache(expire=120) async def get_data() -> str: @@ -71,18 +43,6 @@ async def get_data() -> str: return sanitize(await response.text()) -class CalendarEvent(NamedTuple): - start: datetime - end: datetime - # just a convenience thing, we want to keep start/date as datetime and save the client from guessing this themselves - all_day_event: bool - summary: str - description: str - location: str - color: str - uid: str - - def get_tzinfo(): return timezone(timedelta(hours=1)) @@ -163,16 +123,6 @@ async def get_future_events(): return future_events -@app.get("/events.ics") -async def ics(): - return Response( - await get_data(), - headers={ - "content-type": "text/calendar", - }, - ) - - def group_by_date(events: List[CalendarEvent]): grouped_events: OrderedDict[date, List[CalendarEvent]] = OrderedDict() @@ -185,41 +135,3 @@ def group_by_date(events: List[CalendarEvent]): grouped_events[start_date].append(event) return grouped_events - - -@app.get("/embed-sidebar.html") -async def embed(request: Request, max_width: str = None): - # await asyncio.sleep(1) - - events = await get_future_events() - - grouped_events = list(group_by_date(events).items()) - - # couple of helpers - def localized_abbreviated_month(dt: datetime): - return babel.dates.format_datetime(dt, format="%b", locale="de_DE") - - # couple of helpers - def localized_abbreviated_weekday(dt: datetime): - return babel.dates.format_datetime(dt, format="%b", locale="de_DE") - - def base64_encode(s: str): - return base64.b64encode(s.encode()).decode() - - return templates.TemplateResponse( - "embed-sidebar.html", - context={ - "request": request, - "grouped_events": grouped_events, - "dir": dir, - "localized_abbreviated_month": localized_abbreviated_month, - "localized_abbreviated_weekday": localized_abbreviated_weekday, - "base64_encode": base64_encode, - "max_width": max_width, - }, - ) - - -@app.on_event("startup") -async def startup(): - FastAPICache.init(InMemoryBackend()) diff --git a/fabcal/models.py b/fabcal/models.py new file mode 100644 index 0000000..1c8fb6c --- /dev/null +++ b/fabcal/models.py @@ -0,0 +1,14 @@ +from datetime import datetime +from typing import NamedTuple + + +class CalendarEvent(NamedTuple): + start: datetime + end: datetime + # just a convenience thing, we want to keep start/date as datetime and save the client from guessing this themselves + all_day_event: bool + summary: str + description: str + location: str + color: str + uid: str diff --git a/fabcal/routers/__init__.py b/fabcal/routers/__init__.py new file mode 100644 index 0000000..0b59582 --- /dev/null +++ b/fabcal/routers/__init__.py @@ -0,0 +1,4 @@ +from starlette.templating import Jinja2Templates + + +templates = Jinja2Templates(directory="templates") diff --git a/fabcal/routers/api_v1.py b/fabcal/routers/api_v1.py new file mode 100644 index 0000000..f8404b3 --- /dev/null +++ b/fabcal/routers/api_v1.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter +from starlette.responses import Response + +from fabcal.calendar_client import get_data + + +router = APIRouter() + + +@router.get("/events.ics") +async def events(): + return Response( + await get_data(), + headers={ + "content-type": "text/calendar", + }, + ) diff --git a/fabcal/routers/frontend.py b/fabcal/routers/frontend.py new file mode 100644 index 0000000..268f3fe --- /dev/null +++ b/fabcal/routers/frontend.py @@ -0,0 +1,45 @@ +import base64 + +from datetime import datetime + +import babel.dates + +from fastapi import APIRouter +from fastapi.requests import Request +from fastapi.responses import HTMLResponse + +from fabcal.calendar_client import get_future_events, group_by_date +from fabcal.routers import templates + + +router = APIRouter() + + +@router.get("/embed-sidebar.html", response_class=HTMLResponse) +async def embed(request: Request, max_width: str = None): + events = await get_future_events() + + grouped_events = list(group_by_date(events).items()) + + # couple of helpers + def localized_abbreviated_month(dt: datetime): + return babel.dates.format_datetime(dt, format="%b", locale="de_DE") + + # couple of helpers + def localized_abbreviated_weekday(dt: datetime): + return babel.dates.format_datetime(dt, format="%b", locale="de_DE") + + def base64_encode(s: str): + return base64.b64encode(s.encode()).decode() + + return templates.TemplateResponse( + "embed-sidebar.html", + context={ + "request": request, + "grouped_events": grouped_events, + "dir": dir, + "localized_abbreviated_month": localized_abbreviated_month, + "localized_abbreviated_weekday": localized_abbreviated_weekday, + "base64_encode": base64_encode, + }, + )