Initial commit: Waste Collection API

This commit is contained in:
2026-04-05 07:29:36 +00:00
commit 109b682a88
17 changed files with 1016 additions and 0 deletions

0
app/routes/__init__.py Normal file
View File

56
app/routes/collections.py Normal file
View File

@@ -0,0 +1,56 @@
from fastapi import APIRouter, HTTPException
from ..schemas import (
ICSRequest,
AbfallIORequest,
AWMRequest,
StuttgartRequest,
CollectionsResponse,
)
from ..collector import fetch_ics, fetch_abfall_io, fetch_awm_muenchen, fetch_stuttgart
router = APIRouter(prefix="/collections", tags=["collections"])
@router.post("/ics", response_model=CollectionsResponse)
def collections_ics(body: ICSRequest):
try:
collections = fetch_ics(body.url, body.count)
except Exception as e:
raise HTTPException(status_code=422, detail=str(e))
if not collections:
raise HTTPException(status_code=422, detail="No collections returned from this ICS URL")
return CollectionsResponse(source="ics", collections=collections)
@router.post("/abfall_io", response_model=CollectionsResponse)
def collections_abfall_io(body: AbfallIORequest):
try:
collections = fetch_abfall_io(body.key, body.f_id_kommune, body.f_id_strasse, body.count)
except Exception as e:
raise HTTPException(status_code=422, detail=str(e))
if not collections:
raise HTTPException(status_code=422, detail="No collections returned. Check your IDs.")
return CollectionsResponse(source="abfall_io", collections=collections)
@router.post("/awm_muenchen_de", response_model=CollectionsResponse)
def collections_awm_muenchen(body: AWMRequest):
try:
collections = fetch_awm_muenchen(body.street, body.house_number, body.count)
except Exception as e:
raise HTTPException(status_code=422, detail=str(e))
if not collections:
raise HTTPException(status_code=422, detail="No collections returned. Check street name.")
return CollectionsResponse(source="awm_muenchen_de", collections=collections)
@router.post("/stuttgart_de", response_model=CollectionsResponse)
def collections_stuttgart(body: StuttgartRequest):
try:
collections = fetch_stuttgart(body.street, body.streetnr, body.count)
except Exception as e:
raise HTTPException(status_code=422, detail=str(e))
if not collections:
raise HTTPException(status_code=422, detail="No collections returned. Check street name and number.")
return CollectionsResponse(source="stuttgart_de", collections=collections)

View File

@@ -0,0 +1,44 @@
from fastapi import APIRouter, HTTPException
from ..schemas import (
NotifyRequest,
NotifyResponse,
NotificationsResponse,
DeleteResponse,
)
from ..core.scheduler import NotificationScheduler
router = APIRouter(prefix="/notifications", tags=["notifications"])
# singleton scheduler
_scheduler = NotificationScheduler()
@router.post("", response_model=NotifyResponse)
def create_notification(body: NotifyRequest):
entry = _scheduler.add(
source_id=body.source_id,
collection=body.collection.model_dump(),
notify_at_days_before=body.notify_at_days_before,
channels=body.channels,
chat_id=body.chat_id,
)
return NotifyResponse(
ok=True,
notification_id=entry["id"],
message=f"Reminder set for {entry['collection_date']}: {entry['collection_type']}",
)
@router.get("", response_model=NotificationsResponse)
def list_notifications():
notes = _scheduler.list_all()
return NotificationsResponse(count=len(notes), notifications=notes)
@router.delete("/{notification_id}", response_model=DeleteResponse)
def delete_notification(notification_id: str):
ok = _scheduler.delete(notification_id)
if not ok:
raise HTTPException(status_code=404, detail=f"Notification '{notification_id}' not found")
return DeleteResponse(ok=True, deleted=notification_id)

65
app/routes/sources.py Normal file
View File

@@ -0,0 +1,65 @@
import json
from pathlib import Path
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse
from ..schemas import (
SourceListResponse,
SourceDetailResponse,
SourceParam,
)
from ..core.scheduler import SOURCE_DEFS
router = APIRouter(prefix="/sources", tags=["sources"])
SOURCES_JSON = (
Path(__file__).resolve().parent.parent.parent.parent
/ "waste_lib"
/ "custom_components"
/ "waste_collection_schedule"
/ "sources.json"
)
@router.get("", response_model=SourceListResponse)
def list_sources():
return SourceListResponse(
count=len(SOURCE_DEFS),
sources=[{"id": k, "name": v["name"]} for k, v in SOURCE_DEFS.items()],
)
@router.get("/germany")
def list_germany_sources():
"""
Return all 727 German municipalities from sources.json.
Includes title, module type, and default_params (if any).
"""
with open(SOURCES_JSON) as f:
data = json.load(f)
return {"country": "Germany", "count": len(data["Germany"]), "sources": data["Germany"]}
@router.get("/germany/search")
def search_germany_sources(q: str):
"""
Search Germany municipalities by name (case-insensitive).
"""
with open(SOURCES_JSON) as f:
data = json.load(f)
q = q.lower()
results = [s for s in data["Germany"] if q in s["title"].lower()]
return {"query": q, "count": len(results), "sources": results[:20]}
@router.get("/{source_id}", response_model=SourceDetailResponse)
def get_source(source_id: str):
if source_id not in SOURCE_DEFS:
raise HTTPException(status_code=404, detail=f"Source '{source_id}' not found")
s = SOURCE_DEFS[source_id]
return SourceDetailResponse(
id=source_id,
name=s["name"],
params=[SourceParam(**p) for p in s["params"]],
)