"""REST API over the scraped Fundbureau events. Datasource is the JSON file produced by ``scraper/scraper.py`` (``fundi-scraped-output.json`` by default; override with the DATA_FILE env var). Run with: uvicorn main:app --reload # from inside the api/ folder """ import datetime from typing import List, Optional from fastapi import FastAPI, HTTPException, Query from datasource import EventStore, iter_artist_names, parse_event_date from models import Event, EventList app = FastAPI( title="Fundi Scraper API", description="REST API over the scraped upcoming/past events at the Fundbureau Hamburg.", version="1.0.0", ) store = EventStore() def _matches( event: Event, free: Optional[bool], name: Optional[str], artist: Optional[str], event_date: Optional[str], upcoming: Optional[bool], today: datetime.date, ) -> bool: if free is not None and event.event_free is not free: return False if name is not None and name.lower() not in event.event_name.lower(): return False if artist is not None: needle = artist.lower() if not any(needle in n.lower() for n in iter_artist_names(event)): return False if event_date is not None and event.event_date != event_date: return False if upcoming is not None: parsed = parse_event_date(event.event_date) if parsed is None: return False is_upcoming = parsed >= today if is_upcoming is not upcoming: return False return True @app.get("/health") def health(): return {"status": "ok", "events_loaded": len(store.all()), "data_file": str(store.path)} @app.post("/reload") def reload(): """Re-read the JSON file from disk (e.g. after a fresh scrape).""" try: count = store.reload() except FileNotFoundError as exc: raise HTTPException(status_code=500, detail=str(exc)) return {"reloaded": True, "events_loaded": count} @app.get("/events", response_model=EventList) def list_events( free: Optional[bool] = Query(None, description="Filter by free admission"), name: Optional[str] = Query(None, description="Case-insensitive substring match on event name"), artist: Optional[str] = Query(None, description="Case-insensitive substring match on any artist name"), date: Optional[str] = Query(None, description="Exact match on the raw event date (dd.mm.yy)"), upcoming: Optional[bool] = Query(None, description="true = today or later, false = past events"), ): today = datetime.date.today() events = [ e for e in store.all() if _matches(e, free, name, artist, date, upcoming, today) ] return EventList(count=len(events), events=events) @app.get("/artists", response_model=List[str]) def list_artists(): """Deduplicated, sorted list of all artist names across all events.""" names = set() for event in store.all(): names.update(iter_artist_names(event)) return sorted(names, key=str.lower) @app.get("/events/{index}", response_model=Event) def get_event(index: int): event = store.get(index) if event is None: raise HTTPException(status_code=404, detail=f"No event at index {index}") return event