added sorting and artist dates
This commit is contained in:
+18
-1
@@ -75,12 +75,14 @@ RELOAD_MIN_INTERVAL_SECONDS=30 uvicorn main:app
|
||||
| GET | `/events` | List events, with optional filters (see below) |
|
||||
| GET | `/events/archive` | Past events only — shorthand for `/events?upcoming=false` |
|
||||
| GET | `/events/{index}` | Single event by its position in the merged list (0-based) |
|
||||
| GET | `/artists` | Deduplicated, sorted list of all artist names |
|
||||
| GET | `/artists` | Deduplicated, sorted list of all artists with every date they appear on |
|
||||
| GET | `/calendar.ics` | iCalendar/webcal feed of all events — subscribe with `webcal://<host>/calendar.ics` |
|
||||
| POST | `/reload` | Re-read the data file(s) from disk (after a fresh scrape) - rate-limited, see below |
|
||||
|
||||
### `/events` query parameters
|
||||
|
||||
`/events/archive` accepts the same parameters except `upcoming` (always `false`).
|
||||
|
||||
| Param | Type | Meaning |
|
||||
|------------|--------|---------|
|
||||
| `free` | bool | Only free / only paid events |
|
||||
@@ -88,11 +90,26 @@ RELOAD_MIN_INTERVAL_SECONDS=30 uvicorn main:app
|
||||
| `artist` | string | Case-insensitive substring match on any artist name |
|
||||
| `date` | string | Exact match on the raw date string (`dd.mm.yy`) |
|
||||
| `upcoming` | bool | `true` = today or later, `false` = past events |
|
||||
| `sort` | string | `asc`/`desc` = chronological by event date (unparseable dates sort last either way); `az`/`za` = alphabetical by event name (case-insensitive) |
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
curl 'http://127.0.0.1:8000/events?free=true'
|
||||
curl 'http://127.0.0.1:8000/events?artist=randali&upcoming=true'
|
||||
curl 'http://127.0.0.1:8000/events?sort=asc'
|
||||
curl 'http://127.0.0.1:8000/events?sort=za'
|
||||
curl 'http://127.0.0.1:8000/events/0'
|
||||
```
|
||||
|
||||
### `/artists` response shape
|
||||
|
||||
Each artist appears once, with every date (past or upcoming) they're on the
|
||||
line-up for, sorted chronologically:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "artist_name": "Bizarre", "dates": ["05.09.26", "12.10.26"] },
|
||||
{ "artist_name": "Skkin Velvet", "dates": ["04.09.26"] }
|
||||
]
|
||||
```
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -109,3 +109,38 @@ def iter_artist_names(event: Event):
|
||||
for artist in group:
|
||||
if artist.artist_name:
|
||||
yield artist.artist_name
|
||||
|
||||
|
||||
def sort_events(events: List[Event], sort: Optional[str]) -> List[Event]:
|
||||
"""Sort events per the ``?sort=`` query param.
|
||||
|
||||
"asc"/"desc" order chronologically by ``event_date``; events with an
|
||||
unparseable date always sort last, regardless of direction. "az"/"za"
|
||||
order alphabetically (case-insensitive) by ``event_name``. Any other
|
||||
value (including None) leaves the events in their existing order.
|
||||
"""
|
||||
if sort in ("asc", "desc"):
|
||||
with_parsed = [(e, parse_event_date(e.event_date)) for e in events]
|
||||
dated = sorted(
|
||||
(item for item in with_parsed if item[1] is not None),
|
||||
key=lambda item: item[1],
|
||||
reverse=(sort == "desc"),
|
||||
)
|
||||
undated = [e for e, parsed in with_parsed if parsed is None]
|
||||
return [e for e, _ in dated] + undated
|
||||
if sort in ("az", "za"):
|
||||
return sorted(events, key=lambda e: e.event_name.lower(), reverse=(sort == "za"))
|
||||
return events
|
||||
|
||||
|
||||
def sort_date_strings(dates) -> List[str]:
|
||||
"""Sort raw ``dd.mm.yy`` date strings chronologically, de-duplicated.
|
||||
|
||||
Unparseable strings sort last (alphabetically among themselves).
|
||||
"""
|
||||
unique = set(dates)
|
||||
dated = sorted(
|
||||
(d for d in unique if parse_event_date(d) is not None), key=parse_event_date
|
||||
)
|
||||
undated = sorted(d for d in unique if parse_event_date(d) is None)
|
||||
return dated + undated
|
||||
|
||||
+24
-8
@@ -9,17 +9,23 @@ Run with:
|
||||
|
||||
import datetime
|
||||
import os
|
||||
from typing import List, Optional
|
||||
from collections import defaultdict
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.responses import PlainTextResponse
|
||||
|
||||
from calendar_feed import build_ics
|
||||
from datasource import EventStore, iter_artist_names, parse_event_date
|
||||
from models import Event, EventList
|
||||
from datasource import EventStore, iter_artist_names, parse_event_date, sort_date_strings, sort_events
|
||||
from models import ArtistSchedule, Event, EventList
|
||||
from rate_limit import Cooldown
|
||||
from scrape_schedule import discover_cron_expr, get_schedule_info
|
||||
|
||||
SortOrder = Literal["asc", "desc", "az", "za"]
|
||||
_SORT_DESCRIPTION = (
|
||||
"Sort order: asc/desc by event date, az/za alphabetically by event name"
|
||||
)
|
||||
|
||||
app = FastAPI(
|
||||
title="Fundi Scraper API",
|
||||
description="REST API over the scraped upcoming/past events at the Fundbureau Hamburg.",
|
||||
@@ -118,6 +124,7 @@ def list_events(
|
||||
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"),
|
||||
sort: Optional[SortOrder] = Query(None, description=_SORT_DESCRIPTION),
|
||||
):
|
||||
today = datetime.date.today()
|
||||
events = [
|
||||
@@ -125,16 +132,23 @@ def list_events(
|
||||
for e in store.all()
|
||||
if _matches(e, free, name, artist, date, upcoming, today)
|
||||
]
|
||||
events = sort_events(events, sort)
|
||||
return EventList(count=len(events), events=events)
|
||||
|
||||
|
||||
@app.get("/artists", response_model=List[str])
|
||||
@app.get("/artists", response_model=List[ArtistSchedule])
|
||||
def list_artists():
|
||||
"""Deduplicated, sorted list of all artist names across all events."""
|
||||
names = set()
|
||||
"""Deduplicated, sorted list of all artists, each with every date
|
||||
(past or upcoming) they appear in the line-up.
|
||||
"""
|
||||
appearances: dict = defaultdict(set)
|
||||
for event in store.all():
|
||||
names.update(iter_artist_names(event))
|
||||
return sorted(names, key=str.lower)
|
||||
for artist_name in iter_artist_names(event):
|
||||
appearances[artist_name].add(event.event_date)
|
||||
return [
|
||||
ArtistSchedule(artist_name=artist_name, dates=sort_date_strings(dates))
|
||||
for artist_name, dates in sorted(appearances.items(), key=lambda kv: kv[0].lower())
|
||||
]
|
||||
|
||||
|
||||
@app.get("/events/archive", response_model=EventList)
|
||||
@@ -143,6 +157,7 @@ def list_archive_events(
|
||||
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)"),
|
||||
sort: Optional[SortOrder] = Query(None, description=_SORT_DESCRIPTION),
|
||||
):
|
||||
"""Past events only - shorthand for ``/events?upcoming=false``."""
|
||||
today = datetime.date.today()
|
||||
@@ -151,6 +166,7 @@ def list_archive_events(
|
||||
for e in store.all()
|
||||
if _matches(e, free, name, artist, date, upcoming=False, today=today)
|
||||
]
|
||||
events = sort_events(events, sort)
|
||||
return EventList(count=len(events), events=events)
|
||||
|
||||
|
||||
|
||||
@@ -21,3 +21,8 @@ class Event(BaseModel):
|
||||
class EventList(BaseModel):
|
||||
count: int
|
||||
events: List[Event]
|
||||
|
||||
|
||||
class ArtistSchedule(BaseModel):
|
||||
artist_name: str
|
||||
dates: List[str]
|
||||
|
||||
Reference in New Issue
Block a user