Initial commit: Waste Collection API
This commit is contained in:
0
app/core/__init__.py
Normal file
0
app/core/__init__.py
Normal file
42
app/core/notifier.py
Normal file
42
app/core/notifier.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
Send notification via Telegram.
|
||||
Requires bot token and chat_id configured in environment or .env file.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "8638072533:AAG3fcm0Sq_JBSSA1UXLspgRzn-tdIJ9N_8")
|
||||
TELEGRAM_CHAT_ID = int(os.getenv("TELEGRAM_CHAT_ID", "1320170074"))
|
||||
|
||||
|
||||
def send_telegram(text: str, chat_id: int = TELEGRAM_CHAT_ID) -> bool:
|
||||
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
|
||||
try:
|
||||
resp = httpx.post(url, json={"chat_id": chat_id, "text": text}, timeout=10)
|
||||
resp.raise_for_status()
|
||||
log.info(f"Telegram sent to {chat_id}: {text[:50]}")
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error(f"Telegram failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def format_collection_reminder(collection: dict, source_id: str) -> str:
|
||||
date = collection.get("date", "?")
|
||||
wtype = collection.get("type", "?")
|
||||
days = collection.get("daysUntil", "?")
|
||||
icon = collection.get("icon", "🗑️")
|
||||
return (
|
||||
f"{icon} *Waste Collection Reminder*\n"
|
||||
f"📅 {date} (in {days} day{'s' if days != 1 else ''})\n"
|
||||
f"🗑️ {wtype}\n"
|
||||
f"🔔 from: {source_id}"
|
||||
)
|
||||
118
app/core/scheduler.py
Normal file
118
app/core/scheduler.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
import json
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
NOTIFS_FILE = Path(__file__).resolve().parent.parent.parent / "data" / "notifications.json"
|
||||
|
||||
# ─── Source registry ───────────────────────────────────────────
|
||||
SOURCE_DEFS = {
|
||||
"ics": {
|
||||
"name": "Generic ICS / iCal URL",
|
||||
"params": [
|
||||
{"name": "url", "type": "string", "required": True, "example": "https://example.com/kalender.ics"},
|
||||
],
|
||||
},
|
||||
"abfall_io": {
|
||||
"name": "Abfall.IO (Germany)",
|
||||
"params": [
|
||||
{"name": "key", "type": "string", "required": True, "example": "8215c62763967916979e0e8566b6172e"},
|
||||
{"name": "f_id_kommune", "type": "integer", "required": True, "example": 2999},
|
||||
{"name": "f_id_strasse", "type": "integer", "required": True, "example": 1087},
|
||||
],
|
||||
},
|
||||
"awm_muenchen_de": {
|
||||
"name": "AWM München (Germany)",
|
||||
"params": [
|
||||
{"name": "street", "type": "string", "required": True, "example": "Bahnstr."},
|
||||
{"name": "house_number", "type": "string", "required": True, "example": "11"},
|
||||
],
|
||||
},
|
||||
"stuttgart_de": {
|
||||
"name": "Abfall Stuttgart (Germany)",
|
||||
"params": [
|
||||
{"name": "street", "type": "string", "required": True, "example": "Im Steinengarten"},
|
||||
{"name": "streetnr", "type": "integer", "required": True, "example": 7},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class NotificationScheduler:
|
||||
def __init__(self):
|
||||
self._notifications: dict[str, dict] = {}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
if NOTIFS_FILE.exists():
|
||||
try:
|
||||
data = json.loads(NOTIFS_FILE.read_text())
|
||||
self._notifications = {n["id"]: n for n in data.get("notifications", [])}
|
||||
log.info(f"Loaded {len(self._notifications)} notifications from {NOTIFS_FILE}")
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to load notifications: {e}")
|
||||
|
||||
def _save(self):
|
||||
NOTIFS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
NOTIFS_FILE.write_text(
|
||||
json.dumps({"notifications": list(self._notifications.values())}, indent=2)
|
||||
)
|
||||
|
||||
def add(
|
||||
self,
|
||||
source_id: str,
|
||||
collection: dict,
|
||||
notify_at_days_before: int,
|
||||
channels: list[str],
|
||||
chat_id: int,
|
||||
) -> dict:
|
||||
nid = f"notif_{uuid.uuid4().hex[:8]}"
|
||||
entry = {
|
||||
"id": nid,
|
||||
"source_id": source_id,
|
||||
"collection_type": collection.get("type", "Unknown"),
|
||||
"collection_date": collection.get("date", ""),
|
||||
"notify_at_days_before": notify_at_days_before,
|
||||
"channels": channels,
|
||||
"chat_id": chat_id,
|
||||
"fired": False,
|
||||
}
|
||||
self._notifications[nid] = entry
|
||||
self._save()
|
||||
return entry
|
||||
|
||||
def list_all(self) -> list[dict]:
|
||||
return list(self._notifications.values())
|
||||
|
||||
def delete(self, nid: str) -> bool:
|
||||
if nid in self._notifications:
|
||||
del self._notifications[nid]
|
||||
self._save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def due_today(self) -> list[dict]:
|
||||
today = datetime.now().date().isoformat()
|
||||
due = []
|
||||
for n in self._notifications.values():
|
||||
if n.get("fired"):
|
||||
continue
|
||||
coll_date = n.get("collection_date", "")
|
||||
days_before = n.get("notify_at_days_before", 1)
|
||||
# check if today is notify_at_days_before days before collection
|
||||
try:
|
||||
coll = datetime.fromisoformat(coll_date).date()
|
||||
notify_date = coll - timedelta(days=days_before)
|
||||
if notify_date.isoformat() == today:
|
||||
due.append(n)
|
||||
except Exception:
|
||||
pass
|
||||
return due
|
||||
|
||||
def mark_fired(self, nid: str):
|
||||
if nid in self._notifications:
|
||||
self._notifications[nid]["fired"] = True
|
||||
self._save()
|
||||
Reference in New Issue
Block a user