17 KiB
BSN-Livesystem — Aufgaben für Alicia (Nacht 1)
Jede Aufgabe = neuer Context. Nach Abschluss:
hermes newoder neuen Chat starten. Qualität vor Geschwindigkeit. Lieber 5 saubere Aufgaben als 10 schlampige. Coding-Standards:skill_view('bsn-coding-standards')vor jeder Aufgabe laden.
Aufgabe 1: pyproject.toml — Projekt-Konfiguration
Erstelle die zentrale Projekt-Konfiguration für das BSN-Livesystem.
Was zu tun ist
Erstelle die Datei pyproject.toml im Projekt-Root mit folgenden Abschnitten:
[project]
- name: "bsn-livesystem"
- version: "0.1.0"
- python: ">=3.13"
- dependencies: flask, pydantic, redis, structlog, pytest, pytest-cov, mypy
[tool.ruff]
- line-length: 100
- target-version: "py313"
- select: ["E", "F", "I", "N", "W", "UP", "PL", "C90", "S"]
- ignore: []
[tool.ruff.format]
- quote-style: "double"
- indent-style: "space"
- docstring-code-format: true
[tool.mypy]
- strict: true
- python_version: "3.13"
[tool.pytest.ini_options]
- testpaths: ["tests"]
- addopts: "-v --tb=short"
Abgabe
- 1 Datei:
pyproject.toml - Kein Test nötig (Konfigurationsdatei)
- Commit:
chore: pyproject.toml mit Ruff, mypy, pytest-Konfiguration - Push auf
origin/main
⚠️ NACH DIESER AUFGABE
Starte einen neuen Context. NICHT im gleichen Context weiterarbeiten.
Aufgabe 2: .pre-commit-config.yaml — Pre-Commit Hooks
Erstelle die Pre-Commit-Konfiguration, die vor JEDEM Commit automatisch Qualitätschecks ausführt.
Was zu tun ist
Erstelle .pre-commit-config.yaml im Projekt-Root:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
Wichtig
- Die Datei MUSS exakt
.pre-commit-config.yamlheißen (Punkt am Anfang!) - Nach Installation mit
pre-commit installwerden die Hooks automatisch vor jedem Commit ausgeführt
Abgabe
- 1 Datei:
.pre-commit-config.yaml - Kein Test nötig
- Commit:
chore: pre-commit hooks mit Ruff Lint + Format - Push auf
origin/main
⚠️ NACH DIESER AUFGABE
Starte einen neuen Context. NICHT im gleichen Context weiterarbeiten.
Aufgabe 3: Submission-Modell — Pydantic DB-Modell
Erstelle das zentrale Datenmodell für eingehende Community-Submissions.
Was zu tun ist
Erstelle zwei Dateien:
src/models/__init__.py — leer
src/models/submission.py — das Submission-Modell:
from pydantic import BaseModel, Field
from datetime import datetime
class Submission(BaseModel):
"""Eine Community-Einreichung (WhatsApp/Telegram)."""
message_id: str = Field(..., description="Eindeutige Nachrichten-ID")
plattform: str = Field(..., description="whatsapp oder telegram")
absender: str = Field(..., description="Absender-ID oder Telefonnummer")
text: str = Field(..., min_length=1, max_length=5000, description="Nachrichtentext")
zeitstempel: datetime = Field(default_factory=datetime.now, description="Eingangszeitpunkt")
status: str = Field(default="neu", description="neu, in_bearbeitung, erledigt, fehler")
Besonderheiten
message_idist der Primärschlüssel für Deduplizierungstatusdurchläuft: neu → in_bearbeitung → erledigt (oder fehler)textmax 5000 Zeichen (Telegram-Limit)
Coding-Standards
- Pydantic v2 mit
Field()-Validatoren - Deutsche Docstrings (Google-Style)
- Type Hints auf allen Funktionen
ruff check+ruff format+mypymüssen grün sein
Test-Datei: tests/test_submission.py
Schreibe 4 Tests:
- Gültige Submission erstellen — alle Felder korrekt
- Leerer Text wirft ValidationError
- Text > 5000 Zeichen wirft ValidationError
- zeitstempel wird automatisch gesetzt (nicht None)
Abgabe
- 3 Dateien:
src/models/__init__.py,src/models/submission.py,tests/test_submission.py - Commit:
feat: Submission-Pydantic-Modell mit Validierung - Push auf
origin/main
⚠️ NACH DIESER AUFGABE
Starte einen neuen Context. NICHT im gleichen Context weiterarbeiten.
Aufgabe 4: Input-Validatoren — Validierungs-Helfer
Erstelle Hilfsfunktionen zur Validierung von Benutzereingaben.
Was zu tun ist
Erstelle zwei Dateien:
src/utils/__init__.py — leer
src/utils/validators.py mit drei Funktionen:
def validiere_telefonnummer(telefon: str) -> str:
"""Validiert und normalisiert eine Telefonnummer."""
# Entferne alles außer + und Ziffern
# Muss mit + beginnen, min 10 Ziffern
# Gibt bereinigte Nummer zurück oder wirft ValueError
...
def validiere_spiel_name(name: str) -> str:
"""Validiert einen Spielnamen."""
# 2-200 Zeichen
# Keine HTML-Tags (< >)
# Strip Whitespace
...
def validiere_spieleranzahl(anzahl: int) -> int:
"""Validiert eine Spieleranzahl."""
# 1-20 Spieler
...
Verhalten im Detail
validiere_telefonnummer:
- Akzeptiert: "+491****7890", "+49 123 4567890", "00491234567890"
- Entfernt Leerzeichen, Bindestriche, Klammern
- Konvertiert "00" am Anfang zu "+"
- Wirft ValueError bei: weniger als 10 Ziffern, kein + nach Bereinigung
validiere_spiel_name:
- Entfernt HTML-Tags mit Regex
<[^>]*> - Strip Whitespace an beiden Enden
- Wirft ValueError bei: < 2 Zeichen, > 200 Zeichen
validiere_spieleranzahl:
- Wirft ValueError bei: < 1, > 20, kein Integer
Coding-Standards
- Type Hints auf ALLEN Funktionen
- Deutsche Docstrings mit Args, Returns, Raises
ruff check+ruff format+mypymüssen grün sein
Test-Datei: tests/test_validators.py
Schreibe 6 Tests (2 pro Funktion):
- Gültige Telefonnummer → bereinigt
- Ungültige Telefonnummer → ValueError
- Gültiger Spielname → bereinigt
- HTML-Tags im Spielnamen → entfernt
- Gültige Spieleranzahl → unverändert
- Ungültige Spieleranzahl → ValueError
Abgabe
- 3 Dateien:
src/utils/__init__.py,src/utils/validators.py,tests/test_validators.py - Commit:
feat: Input-Validatoren für Telefon, Spielname, Spieleranzahl - Push auf
origin/main
⚠️ NACH DIESER AUFGABE
Starte einen neuen Context. NICHT im gleichen Context weiterarbeiten.
Aufgabe 5: Queue-Handler — Redis-basierte Message-Queue
Erstelle den zentralen Queue-Handler für asynchrone Nachrichtenverarbeitung.
Was zu tun ist
Erstelle zwei Dateien:
src/queue/__init__.py — leer
src/queue/handler.py — QueueHandler-Klasse:
import redis
from src.models.submission import Submission
class QueueHandler:
"""Redis-basierte Message-Queue mit Idempotenz-Garantie."""
def __init__(self, redis_host: str = "localhost", redis_port: int = 6379):
self.redis = redis.Redis(host=redis_host, port=redis_port, decode_responses=True)
self.stream_key = "bsn:submissions"
self.processed_key = "bsn:processed"
def enqueue(self, submission: Submission) -> str | None:
"""Legt eine Submission in die Queue. Überspringt Duplikate."""
# Prüfe ob message_id bereits verarbeitet wurde
# Wenn ja → return None (Idempotenz)
# Wenn nein → XADD in Stream, merke ID in processed_set (TTL 24h)
...
def dequeue(self, count: int = 5) -> list[Submission]:
"""Holt bis zu `count` Submissions aus der Queue (FIFO)."""
...
def mark_done(self, submission: Submission) -> None:
"""Markiert eine Submission als erfolgreich verarbeitet."""
...
def queue_size(self) -> int:
"""Gibt die aktuelle Queue-Größe zurück."""
...
Anforderungen
- Redis-Stream
bsn:submissionsfür persistente Pufferung - Redis-Set
bsn:processedfür Deduplizierung (24h TTL) - Consumer-Group
bsn-workersfür zuverlässige Verarbeitung - Alle Operationen idempotent
Coding-Standards
- Type Hints auf ALLEN Methoden
- Deutsche Docstrings (Google-Style) pro Methode
ruff check+ruff format+mypymüssen grün sein
Test-Datei: tests/test_queue_handler.py
Schreibe 5 Tests (mit fakeredis):
- enqueue einer neuen Submission → gibt stream_id zurück
- enqueue eines Duplikats → gibt None zurück
- dequeue holt eingereihte Submission zurück
- mark_done entfernt aus Queue
- queue_size gibt korrekte Anzahl
Abgabe
- 3 Dateien:
src/queue/__init__.py,src/queue/handler.py,tests/test_queue_handler.py - Commit:
feat: QueueHandler mit Redis-Streams, Idempotenz, Dedup - Push auf
origin/main
⚠️ NACH DIESER AUFGABE
Starte einen neuen Context. NICHT im gleichen Context weiterarbeiten.
Aufgabe 6: Webhook-Signatur-Prüfung
Erstelle die Signatur-Prüfung für eingehende Webhooks.
Was zu tun ist
Erstelle src/queue/webhook.py:
import hashlib
import hmac
import os
from src.queue.handler import QueueHandler
from src.models.submission import Submission
def verify_whatsapp_signature(payload: bytes, signature: str, app_secret: str | None = None) -> bool:
"""Verifiziert die WhatsApp X-Hub-Signature-256."""
secret = app_secret or os.environ.get("WHATSAPP_APP_SECRET", "")
if not secret:
return False
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
def verify_telegram_signature(token: str | None = None) -> bool:
"""Verifiziert den Telegram Bot-Token."""
expected = token or os.environ.get("TELEGRAM_BOT_TOKEN", "")
return len(expected) > 20 if expected else False
def validate_and_enqueue(payload: bytes, signature: str, plattform: str, qh: QueueHandler) -> bool:
"""Validiert Signatur und legt in Queue ab."""
...
Anforderungen
hmac.compare_digest()für Timing-sicheren Vergleich- Secrets NUR aus Environment (
os.environ) - Plattform-Erkennung über
plattform-Parameter
Coding-Standards
- Type Hints auf ALLEN Funktionen
- Deutsche Docstrings
- Keine Secrets im Code
ruff check+ruff format+mypymüssen grün sein
Test-Datei: tests/test_webhook.py
Schreibe 5 Tests:
- Korrekte WhatsApp-Signatur → True
- Falsche WhatsApp-Signatur → False
- Fehlender App-Secret → False (sicherer Fail)
- Korrekter Telegram-Token → True
- Ungültiger Telegram-Token (zu kurz) → False
Abgabe
- 2 Dateien:
src/queue/webhook.py,tests/test_webhook.py - Commit:
feat: Webhook-Signatur-Prüfung für WhatsApp + Telegram - Push auf
origin/main
⚠️ NACH DIESER AUFGABE
Starte einen neuen Context. NICHT im gleichen Context weiterarbeiten.
Aufgabe 7: Queue-Worker — Async Verarbeitung
Erstelle den asynchronen Worker für Queue-Verarbeitung.
Was zu tun ist
Erstelle src/queue/worker.py:
import time
from src.queue.handler import QueueHandler
from src.models.submission import Submission
def verarbeite_submission(submission: Submission) -> bool:
"""Verarbeitet eine einzelne Submission (Platzhalter für KI-Pipeline)."""
# Platzhalter: Status auf erledigt setzen
...
def worker_loop(qh: QueueHandler, poll_interval: int = 2, batch_size: int = 5) -> None:
"""Endlos-Schleife: Pollt Queue, verarbeitet Batches."""
# 1. dequeue batch
# 2. Für jede Submission: verarbeite_submission()
# 3. Bei Erfolg: mark_done()
# 4. Bei Fehler: loggen, NICHT crashen
# 5. poll_interval Sekunden warten
...
Anforderungen
- Endlos-Schleife mit
while True - Fehler in EINER Submission dürfen Worker NICHT crashen
- Poll-Intervall konfigurierbar (Default: 2 Sekunden)
Coding-Standards
- Type Hints auf allen Funktionen
- Deutsche Docstrings
ruff check+ruff format+mypymüssen grün sein
Test-Datei: tests/test_worker.py
Schreibe 3 Tests (mit fakeredis, mocke time.sleep):
- verarbeite_submission setzt Status auf "erledigt"
- worker_loop verarbeitet alle Submissions in der Queue
- worker_loop stoppt bei leerer Queue (ohne Fehler)
Abgabe
- 2 Dateien:
src/queue/worker.py,tests/test_worker.py - Commit:
feat: Queue-Worker mit Batch-Verarbeitung und Fehlertoleranz - Push auf
origin/main
⚠️ NACH DIESER AUFGABE
Starte einen neuen Context. NICHT im gleichen Context weiterarbeiten.
Aufgabe 8: Flask-App-Factory — App-Grundgerüst
Erstelle die Flask-App-Factory — der Einstiegspunkt der Anwendung.
Was zu tun ist
Erstelle src/app.py:
from flask import Flask
from src.queue.handler import QueueHandler
def create_app() -> Flask:
"""Erstellt und konfiguriert die Flask-App."""
app = Flask(__name__)
app.config.from_mapping(
REDIS_HOST="localhost",
REDIS_PORT=6379,
)
app.queue_handler = QueueHandler(
redis_host=app.config["REDIS_HOST"],
redis_port=app.config["REDIS_PORT"],
)
@app.route("/health")
def health() -> dict:
return {"status": "ok", "queue_size": app.queue_handler.queue_size()}
return app
Anforderungen
- App-Factory-Pattern (
create_app()) — KEIN globalesapp = Flask() - QueueHandler als App-Extension (
app.queue_handler) /healthEndpoint für Monitoring
Test-Datei: tests/test_app.py
Schreibe 3 Tests mit Flask-Test-Client:
- create_app() gibt Flask-App zurück
- GET /health → 200, JSON mit "status": "ok"
- GET /health → queue_size ist Integer
Abgabe
- 2 Dateien:
src/app.py,tests/test_app.py - Commit:
feat: Flask-App-Factory mit Health-Check und QueueHandler - Push auf
origin/main
⚠️ NACH DIESER AUFGABE
Starte einen neuen Context. NICHT im gleichen Context weiterarbeiten.
Aufgabe 9: WhatsApp-Webhook — Route + Signatur + Queue
Erstelle den WhatsApp-Webhook-Endpunkt als Flask-Blueprint.
Was zu tun ist
Erstelle zwei Dateien:
src/routes/__init__.py — leer
src/routes/whatsapp.py als Flask-Blueprint:
from flask import Blueprint, request, jsonify, current_app
from src.queue.webhook import verify_whatsapp_signature
from src.models.submission import Submission
whatsapp_bp = Blueprint("whatsapp", __name__)
@whatsapp_bp.route("/webhook/whatsapp", methods=["GET", "POST"])
def whatsapp_webhook():
"""WhatsApp Webhook-Endpunkt."""
# GET = Verifikation (Hub Challenge)
if request.method == "GET":
mode = request.args.get("hub.mode")
token = request.args.get("hub.verify_token")
challenge = request.args.get("hub.challenge")
if mode == "subscribe" and token == "bsn_verify_token":
return challenge, 200
return "Verification failed", 403
# POST = Eingehende Nachricht
signature = request.headers.get("X-Hub-Signature-256", "")
payload = request.get_data()
if not verify_whatsapp_signature(payload, signature):
return jsonify({"error": "Invalid signature"}), 401
qh = current_app.queue_handler
# Submission bauen und enqueuen
...
return jsonify({"status": "accepted"}), 200
Anforderungen
- Blueprint, keine direkte @app.route
- GET = Hub-Challenge, POST = Signatur → Queue → 200
- Sofort 200 OK, KEINE synchrone Verarbeitung
Test-Datei: tests/test_whatsapp.py
Schreibe 4 Tests:
- GET mit korrektem verify_token → 200 + Challenge
- GET mit falschem verify_token → 403
- POST mit korrekter Signatur → 200
- POST mit falscher Signatur → 401
Abgabe
- 3 Dateien:
src/routes/__init__.py,src/routes/whatsapp.py,tests/test_whatsapp.py - Commit:
feat: WhatsApp-Webhook mit Signatur-Prüfung und Queue-Enqueue - Push auf
origin/main
⚠️ NACH DIESER AUFGABE
Starte einen neuen Context. NICHT im gleichen Context weiterarbeiten.
Aufgabe 10: Telegram-Webhook — Route + Queue
Erstelle den Telegram-Webhook-Endpunkt als Flask-Blueprint.
Was zu tun ist
Erstelle src/routes/telegram.py:
from flask import Blueprint, request, jsonify, current_app
from src.models.submission import Submission
telegram_bp = Blueprint("telegram", __name__)
@telegram_bp.route("/webhook/telegram", methods=["POST"])
def telegram_webhook():
"""Telegram Webhook-Endpunkt."""
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "Invalid JSON"}), 400
qh = current_app.queue_handler
message = data.get("message", {})
message_id = str(message.get("message_id", ""))
chat_id = str(message.get("chat", {}).get("id", ""))
text = message.get("text", "")
if not message_id or not text:
return jsonify({"error": "Missing message_id or text"}), 400
submission = Submission(
message_id=message_id,
plattform="telegram",
absender=chat_id,
text=text,
)
qh.enqueue(submission)
return jsonify({"status": "accepted"}), 200
Anforderungen
- Nur POST (Telegram nutzt setWebhook einmalig)
request.get_json(silent=True)mit None-Check- Sofort 200 OK nach Enqueue
Test-Datei: tests/test_telegram.py
Schreibe 4 Tests:
- POST mit gültigem Payload → 200
- POST ohne JSON → 400
- POST ohne message_id → 400
- POST mit leerem text → 400
Abgabe
- 2 Dateien:
src/routes/telegram.py,tests/test_telegram.py - Commit:
feat: Telegram-Webhook mit JSON-Validierung und Queue-Enqueue - Push auf
origin/main
⚠️ NACH DIESER AUFGABE — FERTIG FÜR HEUTE
Alle 10 Aufgaben abgeschlossen. Gute Nacht! 🌙