from __future__ import annotations from datetime import datetime, timezone from typing import Optional import calendar as pycalendar from quart import request from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload, with_loader_criteria from models.calendars import Calendar, CalendarSlot def parse_int_arg(name: str, default: Optional[int] = None) -> Optional[int]: """Parse an integer query parameter from the request.""" val = request.args.get(name, "").strip() if not val: return default try: return int(val) except ValueError: return default def add_months(year: int, month: int, delta: int) -> tuple[int, int]: """Add (or subtract) months to a given year/month, handling year overflow.""" new_month = month + delta new_year = year + (new_month - 1) // 12 new_month = ((new_month - 1) % 12) + 1 return new_year, new_month def build_calendar_weeks(year: int, month: int) -> list[list[dict]]: """ Build a calendar grid for the given year and month. Returns a list of weeks, where each week is a list of 7 day dictionaries. """ today = datetime.now(timezone.utc).date() cal = pycalendar.Calendar(firstweekday=0) # 0 = Monday weeks: list[list[dict]] = [] for week in cal.monthdatescalendar(year, month): week_days = [] for d in week: week_days.append( { "date": d, "in_month": (d.month == month), "is_today": (d == today), } ) weeks.append(week_days) return weeks async def get_calendar_by_post_and_slug( session: AsyncSession, post_id: int, calendar_slug: str, ) -> Optional[Calendar]: """ Fetch a calendar by post_id and slug, with slots eagerly loaded. Returns None if not found. """ result = await session.execute( select(Calendar) .options( selectinload(Calendar.slots), with_loader_criteria(CalendarSlot, CalendarSlot.deleted_at.is_(None)), ) .where( Calendar.post_id == post_id, Calendar.slug == calendar_slug, Calendar.deleted_at.is_(None), ) ) return result.scalar_one_or_none() async def get_calendar_by_slug( session: AsyncSession, calendar_slug: str, ) -> Optional[Calendar]: """ Fetch a calendar by slug only (for standalone events service). With slots eagerly loaded. Returns None if not found. """ result = await session.execute( select(Calendar) .options( selectinload(Calendar.slots), with_loader_criteria(CalendarSlot, CalendarSlot.deleted_at.is_(None)), ) .where( Calendar.slug == calendar_slug, Calendar.deleted_at.is_(None), ) ) return result.scalar_one_or_none() async def update_calendar_description( calendar: Calendar, description: Optional[str], ) -> None: """Update calendar description (in-place on the calendar object).""" calendar.description = description or None