Sicherheit + Tests + A11y, SW by-v1118

PYDANTIC max_length (38 Routen, ~400 Field-Constraints):
Schützt vor DoS durch Riesen-Payloads (10MB Thread-Titel etc.).
Pragmatische Limits:
- Titel/Name: 200 · Beschreibung/Body: 10000 · Notiz: 5000
- Email: 254 (RFC 5321) · URL: 500 · Slug/Kategorie: 100
- Hund-Name/Rasse: 80 · Hund-Bio: 2000

Top-betroffen: forum.py, diary.py, health.py, dogs.py, expenses.py,
notes.py, auth.py, profile.py. Manuelle len()-Checks in profile,
chat, ki entfernt (jetzt durch Field abgedeckt).

PYTEST COVERAGE (+19 Tests, 37 grün + 1 xfail):
- test_security.py: require_owner (Places GET/PATCH/DELETE mit
  Fremduser → 403), JWT-Blacklist (Logout invalidiert Token),
  Login-Lockout (5 Fehlversuche → 429 + Retry-After Header)
- test_race.py: Invoice-Counter (20 parallele Threads, alle unique),
  Founder-Number (atomare Vergabe, voll bei 100)
- test_validation.py: Forum-Titel 30k Zeichen → 422, Diary-Text
  50k → 422 (verifiziert Pydantic max_length-Sweep)

A11Y (Tap-Targets ≥44×44 + Dark-Mode-Kontrast):
- #header-user-btn 36→44px, .header-back 40→44, .header-menu-btn 40→44
- dog-profile Wrapped-Slider Prev/Next 40→44
- forum-Lightbox Close 40→44
- --c-text-muted Light: #B0A090 (2.37:1 FAIL) → #7F6B58 (4.74:1 PASS)
- --c-text-muted Dark:  #806A58 (3.58:1 FAIL) → #A08878 (5.46:1 PASS)
- Branding-Farben unangetastet
This commit is contained in:
rene 2026-05-27 13:40:30 +02:00
parent 7751d303bb
commit 1ff66a7083
57 changed files with 1253 additions and 612 deletions

View file

@ -12,7 +12,7 @@ from zoneinfo import ZoneInfo
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional, List
from database import db, DB_PATH
from auth import get_current_user
@ -92,15 +92,15 @@ _VALID_TIERS = {"standard", "pro", "breeder", "standard_test", "pro_test", "bree
class QuarterlyReportBody(BaseModel):
year: int
quarter: int
email: str
email: str = Field(..., max_length=254)
class UserPatch(BaseModel):
rolle: Optional[str] = None # user | moderator | admin
rolle: Optional[str] = Field(None, max_length=30) # user | moderator | admin
is_moderator: Optional[int] = None
is_banned: Optional[int] = None
ban_reason: Optional[str] = None
ban_reason: Optional[str] = Field(None, max_length=1000)
is_social_media: Optional[int] = None
subscription_tier: Optional[str] = None
subscription_tier: Optional[str] = Field(None, max_length=50)
class WikiEnrichBody(BaseModel):
limit: int = 10

View file

@ -16,7 +16,7 @@ import uuid
import httpx
from datetime import datetime, timedelta
from fastapi import APIRouter, Query, BackgroundTasks, Depends, Form, UploadFile, File, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -292,7 +292,7 @@ async def adoption_geocode(plz: str = Query(..., min_length=4, max_length=10)):
# ==================================================================
class InterestBody(BaseModel):
nachricht: Optional[str] = None
nachricht: Optional[str] = Field(None, max_length=5000)
# ------------------------------------------------------------------
@ -422,7 +422,7 @@ async def community_create(
# PATCH /api/adoption/community/{id} — Status ändern (nur Besitzer)
# ------------------------------------------------------------------
class _StatusBody(BaseModel):
status: str
status: str = Field(..., max_length=50)
@router.patch("/community/{listing_id}")
def community_update_status(

View file

@ -10,7 +10,7 @@ from typing import Optional
import jwt as _pyjwt
from fastapi import APIRouter, HTTPException, Request, Response, Depends
from fastapi.responses import RedirectResponse
from pydantic import BaseModel, EmailStr
from pydantic import BaseModel, EmailStr, Field
from database import db
from auth import (
hash_password, verify_password, create_token,
@ -146,13 +146,13 @@ def _send_verification_email(email: str, name: str, token: str):
class LoginRequest(BaseModel):
email: EmailStr
password: str
password: str = Field(..., min_length=1, max_length=200)
class RegisterRequest(BaseModel):
email: EmailStr
password: str
name: str
ref_code: Optional[str] = None
password: str = Field(..., min_length=8, max_length=200)
name: str = Field(..., min_length=2, max_length=40)
ref_code: Optional[str] = Field(None, max_length=50)
def _gen_referral_code() -> str:
@ -426,8 +426,8 @@ class ForgotPasswordRequest(BaseModel):
email: EmailStr
class ResetPasswordRequest(BaseModel):
token: str
password: str
token: str = Field(..., min_length=10, max_length=200)
password: str = Field(..., min_length=8, max_length=200)
@router.post("/forgot-password")
async def forgot_password(data: ForgotPasswordRequest, request: Request):
@ -471,8 +471,8 @@ async def forgot_password(data: ForgotPasswordRequest, request: Request):
class UpgradeRequestBody(BaseModel):
tier: str
message: Optional[str] = None
tier: str = Field(..., max_length=50)
message: Optional[str] = Field(None, max_length=2000)
@router.post("/upgrade-request")
async def create_upgrade_request(data: UpgradeRequestBody, user=Depends(get_current_user)):

View file

@ -6,7 +6,7 @@ from zoneinfo import ZoneInfo
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
from fastapi.responses import FileResponse
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
@ -237,7 +237,7 @@ async def admin_download_document(user_id: int, doc_id: int, admin=Depends(requi
class RejectBody(BaseModel):
grund: str
grund: str = Field(..., min_length=3, max_length=2000)
# ------------------------------------------------------------------
@ -483,13 +483,13 @@ async def admin_create_profile(admin=Depends(require_admin)):
# PUT /api/breeder/profile — eigenes Profil bearbeiten
# ------------------------------------------------------------------
class BreederProfileUpdate(BaseModel):
zwingername: Optional[str] = None
rasse_text: Optional[str] = None
verein: Optional[str] = None
zwingername: Optional[str] = Field(None, max_length=200)
rasse_text: Optional[str] = Field(None, max_length=200)
verein: Optional[str] = Field(None, max_length=200)
vdh_mitglied: Optional[int] = None
stadt: Optional[str] = None
website: Optional[str] = None
beschreibung: Optional[str] = None
stadt: Optional[str] = Field(None, max_length=200)
website: Optional[str] = Field(None, max_length=500)
beschreibung: Optional[str] = Field(None, max_length=10000)
@router.put("/breeder/profile")
async def update_breeder_profile(body: BreederProfileUpdate, user=Depends(require_breeder)):

View file

@ -1,7 +1,7 @@
"""BAN YARO — Züchter-Fotos (Upload, Verwaltung, öffentliche Ansicht)"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
from fastapi.responses import FileResponse
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
import os, logging, asyncio
from database import db
@ -30,10 +30,10 @@ def _require_breeder(user=Depends(get_current_user)):
# Modelle
# ------------------------------------------------------------------
class VisibilityBody(BaseModel):
visibility: str
visibility: str = Field(..., max_length=30)
class CaptionBody(BaseModel):
caption: Optional[str] = None
caption: Optional[str] = Field(None, max_length=500)
# ------------------------------------------------------------------

View file

@ -4,7 +4,7 @@ import os
import uuid
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from database import db
from auth import get_current_user
@ -142,7 +142,7 @@ async def get_messages(conv_id: int, offset: int = 0, limit: int = 50,
class SendMsgModel(BaseModel):
text: str
text: str = Field(..., min_length=1, max_length=2000)
@router.post("/conversations/{conv_id}/messages", status_code=201)
@ -151,8 +151,6 @@ async def send_message(conv_id: int, data: SendMsgModel, user=Depends(get_curren
text = data.text.strip()
if not text:
raise HTTPException(400, "Nachricht darf nicht leer sein.")
if len(text) > 2000:
raise HTTPException(400, "Nachricht zu lang (max. 2000 Zeichen).")
with db() as conn:
conv = conn.execute(

View file

@ -2,7 +2,7 @@
import os, uuid, json, logging, asyncio
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user, require_admin
@ -20,27 +20,27 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
class DiaryCreate(BaseModel):
datum: Optional[str] = None # ISO date, default heute
client_time: Optional[str] = None # lokale Uhrzeit des Geräts (YYYY-MM-DDTHH:MM:SS)
typ: str = "eintrag"
titel: Optional[str] = None
text: Optional[str] = None
datum: Optional[str] = Field(None, max_length=32) # ISO date, default heute
client_time: Optional[str] = Field(None, max_length=64) # lokale Uhrzeit des Geräts (YYYY-MM-DDTHH:MM:SS)
typ: str = Field("eintrag", max_length=50)
titel: Optional[str] = Field(None, max_length=200)
text: Optional[str] = Field(None, max_length=10000)
tags: Optional[list] = None
gps_lat: Optional[float] = None
gps_lon: Optional[float] = None
location_name: Optional[str] = None
location_name: Optional[str] = Field(None, max_length=300)
is_milestone: bool = False
dog_ids: Optional[list[int]] = None # alle Hunde inkl. primär; None = nur primary
weather_json: Optional[str] = None # Client-seitig vorab geholtes Wetter (Fallback wenn kein GPS)
weather_json: Optional[str] = Field(None, max_length=5000) # Client-seitig vorab geholtes Wetter (Fallback wenn kein GPS)
class DiaryUpdate(BaseModel):
titel: Optional[str] = None
text: Optional[str] = None
titel: Optional[str] = Field(None, max_length=200)
text: Optional[str] = Field(None, max_length=10000)
tags: Optional[list] = None
gps_lat: Optional[float] = None
gps_lon: Optional[float] = None
location_name: Optional[str] = None
location_name: Optional[str] = Field(None, max_length=300)
is_milestone: Optional[bool] = None
dog_ids: Optional[list[int]] = None # wenn gesetzt: Hunde-Zuweisung ersetzen

View file

@ -3,7 +3,7 @@
import os
import uuid
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user, has_pro_access
@ -29,28 +29,28 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
class DogCreate(BaseModel):
name: str
rasse: Optional[str] = None
geburtstag: Optional[str] = None
geschlecht: Optional[str] = None
name: str = Field(..., min_length=1, max_length=80)
rasse: Optional[str] = Field(None, max_length=80)
geburtstag: Optional[str] = Field(None, max_length=32)
geschlecht: Optional[str] = Field(None, max_length=20)
gewicht_kg: Optional[float] = None
widerrist_cm: Optional[float] = None
chip_nr: Optional[str] = None
bio: Optional[str] = None
is_public: bool = False
chip_nr: Optional[str] = Field(None, max_length=50)
bio: Optional[str] = Field(None, max_length=2000)
is_public: bool = False
class DogUpdate(BaseModel):
name: Optional[str] = None
rasse: Optional[str] = None
rasse_id: Optional[int] = None
geburtstag: Optional[str] = None
geschlecht: Optional[str] = None
name: Optional[str] = Field(None, max_length=80)
rasse: Optional[str] = Field(None, max_length=80)
rasse_id: Optional[int] = None
geburtstag: Optional[str] = Field(None, max_length=32)
geschlecht: Optional[str] = Field(None, max_length=20)
gewicht_kg: Optional[float] = None
widerrist_cm: Optional[float] = None
chip_nr: Optional[str] = None
bio: Optional[str] = None
is_public: Optional[bool] = None
chip_nr: Optional[str] = Field(None, max_length=50)
bio: Optional[str] = Field(None, max_length=2000)
is_public: Optional[bool] = None
@router.get("")
@ -1033,8 +1033,8 @@ async def public_dog_profile(dog_id: int):
class FoundReport(BaseModel):
message: Optional[str] = None
kontakt: Optional[str] = None
message: Optional[str] = Field(None, max_length=1000)
kontakt: Optional[str] = Field(None, max_length=300)
# Gefunden-Meldung (kein Login nötig)
@ -1319,7 +1319,7 @@ async def get_dog_timeline(dog_id: int, user=Depends(get_current_user)):
# POST /api/dogs/{id}/gedenken — Hund als verstorben markieren
# ------------------------------------------------------------------
class GedenkenData(BaseModel):
verstorben_am: str # YYYY-MM-DD
verstorben_am: str = Field(..., max_length=32) # YYYY-MM-DD
@router.post("/{dog_id}/gedenken")
async def mark_verstorben(dog_id: int, data: GedenkenData, user=Depends(get_current_user)):

View file

@ -2,7 +2,7 @@
import logging
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -16,18 +16,18 @@ logger = logging.getLogger(__name__)
# Schemas
# ------------------------------------------------------------------
class FutterProfilUpdate(BaseModel):
futter_typ: Optional[str] = None # trocken|nass|barf|mix
marke: Optional[str] = None
futter_typ: Optional[str] = Field(None, max_length=50) # trocken|nass|barf|mix
marke: Optional[str] = Field(None, max_length=200)
kcal_tag: Optional[int] = None
portionen: Optional[int] = None
notizen: Optional[str] = None
notizen: Optional[str] = Field(None, max_length=5000)
class KiBeratungRequest(BaseModel):
frage: str
dog_name: Optional[str] = None
rasse: Optional[str] = None
alter: Optional[str] = None
frage: str = Field(..., min_length=3, max_length=2000)
dog_name: Optional[str] = Field(None, max_length=80)
rasse: Optional[str] = Field(None, max_length=80)
alter: Optional[str] = Field(None, max_length=50)
gewicht: Optional[float] = None
aktiv: Optional[bool] = None
@ -183,20 +183,20 @@ _GASTRO_HINWEIS = "Magen-Darm-Symptome wie {label} treten meist innerhalb wenige
class FutterEintragCreate(BaseModel):
datum: str
uhrzeit: str
futter_name: str
futter_typ: Optional[str] = "trockenfutter"
datum: str = Field(..., max_length=32)
uhrzeit: str = Field(..., max_length=20)
futter_name: str = Field(..., max_length=200)
futter_typ: Optional[str] = Field("trockenfutter", max_length=50)
menge_g: Optional[int] = None
notiz: Optional[str] = None
notiz: Optional[str] = Field(None, max_length=2000)
class ReaktionCreate(BaseModel):
datum: str
uhrzeit: str
reaktion_typ: str
datum: str = Field(..., max_length=32)
uhrzeit: str = Field(..., max_length=20)
reaktion_typ: str = Field(..., max_length=100)
intensitaet: Optional[int] = 3
notiz: Optional[str] = None
notiz: Optional[str] = Field(None, max_length=2000)
# ------------------------------------------------------------------

View file

@ -2,7 +2,7 @@
from datetime import date
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -17,29 +17,29 @@ TYPEN = {'ausstellung', 'training', 'treffen', 'markt', 'wettkampf', 'sonstiges'
# Schemas
# ------------------------------------------------------------------
class RsvpCreate(BaseModel):
status: str = 'going' # 'going' | 'maybe'
status: str = Field('going', max_length=20) # 'going' | 'maybe'
class EventCreate(BaseModel):
titel: str
datum: str # YYYY-MM-DD
uhrzeit: Optional[str] = None
titel: str = Field(..., min_length=3, max_length=200)
datum: str = Field(..., max_length=32) # YYYY-MM-DD
uhrzeit: Optional[str] = Field(None, max_length=20)
lat: Optional[float] = None
lon: Optional[float] = None
ort_name: Optional[str] = None
typ: str = 'sonstiges'
beschreibung: Optional[str] = None
link: Optional[str] = None
ort_name: Optional[str] = Field(None, max_length=300)
typ: str = Field('sonstiges', max_length=50)
beschreibung: Optional[str] = Field(None, max_length=10000)
link: Optional[str] = Field(None, max_length=500)
class EventUpdate(BaseModel):
titel: Optional[str] = None
datum: Optional[str] = None
uhrzeit: Optional[str] = None
titel: Optional[str] = Field(None, max_length=200)
datum: Optional[str] = Field(None, max_length=32)
uhrzeit: Optional[str] = Field(None, max_length=20)
lat: Optional[float] = None
lon: Optional[float] = None
ort_name: Optional[str] = None
typ: Optional[str] = None
beschreibung: Optional[str] = None
link: Optional[str] = None
ort_name: Optional[str] = Field(None, max_length=300)
typ: Optional[str] = Field(None, max_length=50)
beschreibung: Optional[str] = Field(None, max_length=10000)
link: Optional[str] = Field(None, max_length=500)
# ------------------------------------------------------------------

View file

@ -4,7 +4,7 @@ import logging
from datetime import date, timedelta
from dateutil.relativedelta import relativedelta
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -20,35 +20,35 @@ KATEGORIEN = {"tierarzt", "futter", "zubehoer", "versicherung", "sitter", "sonst
# ------------------------------------------------------------------
class ExpenseCreate(BaseModel):
dog_id: Optional[int] = None
kategorie: str
kategorie: str = Field(..., max_length=50)
betrag: float
datum: str
notiz: Optional[str] = None
datum: str = Field(..., max_length=32)
notiz: Optional[str] = Field(None, max_length=1000)
class ExpenseUpdate(BaseModel):
dog_id: Optional[int] = None
kategorie: Optional[str] = None
kategorie: Optional[str] = Field(None, max_length=50)
betrag: Optional[float] = None
datum: Optional[str] = None
notiz: Optional[str] = None
datum: Optional[str] = Field(None, max_length=32)
notiz: Optional[str] = Field(None, max_length=1000)
class RecurringCreate(BaseModel):
dog_id: Optional[int] = None
kategorie: str
kategorie: str = Field(..., max_length=50)
betrag: float
haeufigkeit: str # monatlich | quartalsweise | jaehrlich
startdatum: str # ISO date
notiz: Optional[str] = None
haeufigkeit: str = Field(..., max_length=30) # monatlich | quartalsweise | jaehrlich
startdatum: str = Field(..., max_length=32) # ISO date
notiz: Optional[str] = Field(None, max_length=1000)
class RecurringUpdate(BaseModel):
dog_id: Optional[int] = None
kategorie: Optional[str] = None
kategorie: Optional[str] = Field(None, max_length=50)
betrag: Optional[float] = None
haeufigkeit: Optional[str] = None
startdatum: Optional[str] = None
notiz: Optional[str] = None
haeufigkeit: Optional[str] = Field(None, max_length=30)
startdatum: Optional[str] = Field(None, max_length=32)
notiz: Optional[str] = Field(None, max_length=1000)
aktiv: Optional[bool] = None

View file

@ -2,7 +2,7 @@
import os, uuid, json, logging
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user, get_current_user_optional
@ -27,40 +27,40 @@ KATEGORIEN = ['allgemein', 'rasse', 'region', 'gesundheit', 'erziehung',
# Schemas
# ------------------------------------------------------------------
class ThreadCreate(BaseModel):
kategorie: str = 'allgemein'
titel: str
text: str
kategorie: str = Field('allgemein', max_length=100)
titel: str = Field(..., min_length=3, max_length=200)
text: str = Field(..., min_length=1, max_length=10000)
thread_lat: Optional[float] = None
thread_lon: Optional[float] = None
thread_ort: Optional[str] = None
client_time: Optional[str] = None
thread_ort: Optional[str] = Field(None, max_length=300)
client_time: Optional[str] = Field(None, max_length=64)
class PostCreate(BaseModel):
text: str
client_time: Optional[str] = None
text: str = Field(..., min_length=1, max_length=10000)
client_time: Optional[str] = Field(None, max_length=64)
class ThreadPatch(BaseModel):
is_pinned: Optional[int] = None
is_locked: Optional[int] = None
class ThreadUpdate(BaseModel):
titel: Optional[str] = None
text: Optional[str] = None
titel: Optional[str] = Field(None, max_length=200)
text: Optional[str] = Field(None, max_length=10000)
thread_lat: Optional[float] = None
thread_lon: Optional[float] = None
thread_ort: Optional[str] = None
thread_ort: Optional[str] = Field(None, max_length=300)
class PostUpdate(BaseModel):
text: str
text: str = Field(..., min_length=1, max_length=10000)
class LikeBody(BaseModel):
target_type: str # 'thread' | 'post'
target_type: str = Field(..., max_length=20) # 'thread' | 'post'
target_id: int
class ReportBody(BaseModel):
target_type: str
target_type: str = Field(..., max_length=20)
target_id: int
grund: str
grund: str = Field(..., min_length=3, max_length=1000)
class LocationBody(BaseModel):
lat: Optional[float] = None

View file

@ -3,7 +3,7 @@
import json
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional, List
from database import db
from auth import get_current_user
@ -15,14 +15,14 @@ router = APIRouter()
class GassiZeitCreate(BaseModel):
dog_id: Optional[int] = None
wochentage: List[str] # ["mo", "mi", "fr"]
uhrzeit: str # "17:00"
ort_name: Optional[str] = None
dog_id: Optional[int] = None
wochentage: List[str] # ["mo", "mi", "fr"]
uhrzeit: str = Field(..., max_length=20) # "17:00"
ort_name: Optional[str] = Field(None, max_length=300)
lat: Optional[float] = None
lon: Optional[float] = None
radius_m: int = 500
notiz: Optional[str] = None
notiz: Optional[str] = Field(None, max_length=2000)
class GassiZeitUpdate(BaseModel):

View file

@ -3,7 +3,7 @@
import os, uuid
from datetime import date, datetime
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -22,59 +22,59 @@ TYPEN = {"impfung", "entwurmung", "tierarzt", "medikament", "gewicht", "allergie
# Schemas
# ------------------------------------------------------------------
class HealthCreate(BaseModel):
typ: str
bezeichnung: Optional[str] = None
datum: str
naechstes: Optional[str] = None
notiz: Optional[str] = None
typ: str = Field(..., max_length=50)
bezeichnung: Optional[str] = Field(None, max_length=200)
datum: str = Field(..., max_length=32)
naechstes: Optional[str] = Field(None, max_length=32)
notiz: Optional[str] = Field(None, max_length=5000)
# Gewicht
wert: Optional[float] = None
einheit: Optional[str] = "kg"
einheit: Optional[str] = Field("kg", max_length=20)
# Impfung
charge_nr: Optional[str] = None
tierarzt_name: Optional[str] = None
charge_nr: Optional[str] = Field(None, max_length=100)
tierarzt_name: Optional[str] = Field(None, max_length=200)
# Tierarztbesuch
kosten: Optional[float] = None
diagnose: Optional[str] = None
diagnose: Optional[str] = Field(None, max_length=2000)
# Medikament
dosierung: Optional[str] = None
haeufigkeit: Optional[str] = None
dosierung: Optional[str] = Field(None, max_length=200)
haeufigkeit: Optional[str] = Field(None, max_length=200)
aktiv: Optional[int] = 1
bis_datum: Optional[str] = None
bis_datum: Optional[str] = Field(None, max_length=32)
# Allergie
schweregrad: Optional[str] = None # leicht | mittel | schwer
reaktion: Optional[str] = None
schweregrad: Optional[str] = Field(None, max_length=50) # leicht | mittel | schwer
reaktion: Optional[str] = Field(None, max_length=1000)
erinnerung: Optional[int] = 1
intervall_tage: Optional[int] = None # Wiederkehrend alle X Tage
# Tierarzt-Verknüpfung
tierarzt_id: Optional[int] = None
# Züchter
deckdatum: Optional[str] = None
wurftermin: Optional[str] = None
deckdatum: Optional[str] = Field(None, max_length=32)
wurftermin: Optional[str] = Field(None, max_length=32)
class HealthUpdate(BaseModel):
bezeichnung: Optional[str] = None
datum: Optional[str] = None
naechstes: Optional[str] = None
notiz: Optional[str] = None
bezeichnung: Optional[str] = Field(None, max_length=200)
datum: Optional[str] = Field(None, max_length=32)
naechstes: Optional[str] = Field(None, max_length=32)
notiz: Optional[str] = Field(None, max_length=5000)
wert: Optional[float] = None
einheit: Optional[str] = None
charge_nr: Optional[str] = None
tierarzt_name: Optional[str] = None
einheit: Optional[str] = Field(None, max_length=20)
charge_nr: Optional[str] = Field(None, max_length=100)
tierarzt_name: Optional[str] = Field(None, max_length=200)
kosten: Optional[float] = None
diagnose: Optional[str] = None
dosierung: Optional[str] = None
haeufigkeit: Optional[str] = None
diagnose: Optional[str] = Field(None, max_length=2000)
dosierung: Optional[str] = Field(None, max_length=200)
haeufigkeit: Optional[str] = Field(None, max_length=200)
aktiv: Optional[int] = None
bis_datum: Optional[str] = None
schweregrad: Optional[str] = None
reaktion: Optional[str] = None
bis_datum: Optional[str] = Field(None, max_length=32)
schweregrad: Optional[str] = Field(None, max_length=50)
reaktion: Optional[str] = Field(None, max_length=1000)
erinnerung: Optional[int] = None
intervall_tage: Optional[int] = None
tierarzt_id: Optional[int] = None
deckdatum: Optional[str] = None
wurftermin: Optional[str] = None
deckdatum: Optional[str] = Field(None, max_length=32)
wurftermin: Optional[str] = Field(None, max_length=32)
# ------------------------------------------------------------------
@ -390,7 +390,7 @@ async def list_gewicht(dog_id: int, user=Depends(get_current_user)):
# POST /api/dogs/{dog_id}/health/symptom-check — KI-Symptomprüfung
# ------------------------------------------------------------------
class SymptomCheckRequest(BaseModel):
symptoms: str
symptoms: str = Field(..., min_length=3, max_length=5000)
@router.post("/{dog_id}/health/symptom-check")
@ -576,20 +576,20 @@ async def terminvorschlaege(dog_id: int, user=Depends(get_current_user)):
# ==================================================================
class InsuranceCreate(BaseModel):
anbieter: str
police_nr: Optional[str] = None
anbieter: str = Field(..., min_length=1, max_length=200)
police_nr: Optional[str] = Field(None, max_length=100)
jahresbeitrag: Optional[float] = None
kontakt: Optional[str] = None
ablaufdatum: Optional[str] = None
notizen: Optional[str] = None
kontakt: Optional[str] = Field(None, max_length=500)
ablaufdatum: Optional[str] = Field(None, max_length=32)
notizen: Optional[str] = Field(None, max_length=5000)
class InsuranceUpdate(BaseModel):
anbieter: Optional[str] = None
police_nr: Optional[str] = None
anbieter: Optional[str] = Field(None, max_length=200)
police_nr: Optional[str] = Field(None, max_length=100)
jahresbeitrag: Optional[float] = None
kontakt: Optional[str] = None
ablaufdatum: Optional[str] = None
notizen: Optional[str] = None
kontakt: Optional[str] = Field(None, max_length=500)
ablaufdatum: Optional[str] = Field(None, max_length=32)
notizen: Optional[str] = Field(None, max_length=5000)
@router.get("/{dog_id}/insurance")
@ -674,12 +674,12 @@ TRIGGER_LABELS = {
class BehaviorCreate(BaseModel):
datum: str
uhrzeit: Optional[str] = None
kategorie: str
intensitaet: int = 3
trigger: Optional[str] = None
notiz: Optional[str] = None
datum: str = Field(..., max_length=32)
uhrzeit: Optional[str] = Field(None, max_length=20)
kategorie: str = Field(..., max_length=50)
intensitaet: int = 3
trigger: Optional[str] = Field(None, max_length=200)
notiz: Optional[str] = Field(None, max_length=5000)
@router.get("/{dog_id}/behavior")

View file

@ -1,7 +1,7 @@
"""BAN YARO — Hilfe / FAQ Routes"""
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user_optional, require_admin
@ -31,17 +31,17 @@ def _load_active_help_articles() -> list[dict]:
# Schemas
# ------------------------------------------------------------------
class ArticleCreate(BaseModel):
kategorie: str
frage: str
antwort: str
sort_order: int = 0
aktiv: int = 1
kategorie: str = Field(..., max_length=100)
frage: str = Field(..., min_length=3, max_length=500)
antwort: str = Field(..., min_length=3, max_length=10000)
sort_order: int = 0
aktiv: int = 1
class ArticleUpdate(BaseModel):
kategorie: Optional[str] = None
frage: Optional[str] = None
antwort: Optional[str] = None
kategorie: Optional[str] = Field(None, max_length=100)
frage: Optional[str] = Field(None, max_length=500)
antwort: Optional[str] = Field(None, max_length=10000)
sort_order: Optional[int] = None
aktiv: Optional[int] = None

View file

@ -6,7 +6,7 @@ from datetime import datetime
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response
from pydantic import BaseModel
from pydantic import BaseModel, Field
from database import db
from auth import require_admin
import mailer
@ -19,30 +19,30 @@ logger = logging.getLogger(__name__)
# Schemas
# ------------------------------------------------------------------
class InvoiceItem(BaseModel):
description: str
quantity: float = 1.0
description: str = Field(..., max_length=500)
quantity: float = 1.0
unit_price: float
class InvoiceCreate(BaseModel):
user_id: Optional[int] = None
recipient_name: str
recipient_email: str
recipient_address: Optional[str] = None
recipient_name: str = Field(..., max_length=200)
recipient_email: str = Field(..., max_length=254)
recipient_address: Optional[str] = Field(None, max_length=500)
items: List[InvoiceItem]
discount_pct: Optional[float] = 0.0
service_period: Optional[str] = None
notes: Optional[str] = None
service_period: Optional[str] = Field(None, max_length=200)
notes: Optional[str] = Field(None, max_length=5000)
class PayBody(BaseModel):
paid_at: str
paid_at: str = Field(..., max_length=32)
paid_amount: float
notes: Optional[str] = None
notes: Optional[str] = Field(None, max_length=2000)
class CancelBody(BaseModel):
reason: str
reason: str = Field(..., min_length=3, max_length=1000)
# ------------------------------------------------------------------

View file

@ -1,6 +1,6 @@
"""BAN YARO — KI Routes"""
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
import ki as ki_module
from auth import get_current_user
@ -11,9 +11,9 @@ router = APIRouter()
class TrainingRequest(BaseModel):
problem: str
rasse: Optional[str] = None
alter: Optional[str] = None
problem: str = Field(..., min_length=10, max_length=1000)
rasse: Optional[str] = Field(None, max_length=80)
alter: Optional[str] = Field(None, max_length=50)
@router.post("/training")
@ -23,8 +23,6 @@ async def ki_training(req: TrainingRequest, request: Request,
rl_check(request, max_requests=10, window_seconds=3600, key="ki_training")
if not req.problem or len(req.problem.strip()) < 10:
raise HTTPException(400, "Bitte beschreibe das Problem genauer.")
if len(req.problem) > 1000:
raise HTTPException(400, "Beschreibung zu lang (max. 1000 Zeichen).")
rasse = req.rasse or "unbekannt"
alter = req.alter or "unbekannt"
@ -69,10 +67,10 @@ Schreibe klar und strukturiert, ohne unnötigen Fachjargon."""
# POST /ki/tierarzt — KI-Tierarztfragen
# ------------------------------------------------------------------
class TierarztRequest(BaseModel):
symptom: str
symptom: str = Field(..., min_length=5, max_length=1000)
dog_id: Optional[int] = None
dog_name: Optional[str] = None
rasse: Optional[str] = None
dog_name: Optional[str] = Field(None, max_length=80)
rasse: Optional[str] = Field(None, max_length=80)
@router.post("/tierarzt")
@ -81,8 +79,6 @@ async def ki_tierarzt(req: TierarztRequest, request: Request,
"""KI-Tierarztfragen: Symptombeschreibung → erste Einschätzung."""
if not req.symptom or len(req.symptom.strip()) < 5:
raise HTTPException(400, "Bitte beschreibe das Symptom genauer.")
if len(req.symptom) > 1000:
raise HTTPException(400, "Beschreibung zu lang (max. 1000 Zeichen).")
# Rate-Limit: max 5 Anfragen pro User pro Tag
with db() as conn:
@ -173,10 +169,10 @@ def _log_rasse_request(user_id: int):
class BirthdayRequest(BaseModel):
dog_id: int
name: str
rasse: Optional[str] = None
name: str = Field(..., max_length=80)
rasse: Optional[str] = Field(None, max_length=80)
alter: Optional[int] = None
mode: str = "tomorrow" # "tomorrow" | "today"
mode: str = Field("tomorrow", max_length=20) # "tomorrow" | "today"
@router.post("/geburtstag")
async def ki_geburtstag(req: BirthdayRequest, request: Request,
@ -368,12 +364,12 @@ Falls kein Hund erkennbar: ist_hund=false und leeres rassen-Array."""
# ------------------------------------------------------------------
class AbschiedRequest(BaseModel):
dog_id: int
name: str
rasse: Optional[str] = None
name: str = Field(..., max_length=80)
rasse: Optional[str] = Field(None, max_length=80)
km_total: Optional[float] = None
diary_count: Optional[int] = None
gemeinsam_tage: Optional[int] = None
last_entry_titel: Optional[str] = None
last_entry_titel: Optional[str] = Field(None, max_length=200)
@router.post("/abschied")
async def ki_abschied(req: AbschiedRequest, request: Request,

View file

@ -1,7 +1,7 @@
"""BAN YARO — Hunde-Knigge Routes"""
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user, get_current_user_optional
@ -13,12 +13,12 @@ router = APIRouter()
# Schemas
# ------------------------------------------------------------------
class VoteRequest(BaseModel):
szenario_id: str
answer: str
szenario_id: str = Field(..., max_length=100)
answer: str = Field(..., max_length=100)
class KiRatRequest(BaseModel):
situation: str
situation: str = Field(..., min_length=3, max_length=2000)
# ------------------------------------------------------------------

View file

@ -1,7 +1,7 @@
"""BAN YARO — Läufigkeit, Progesterontests & Trächtigkeit (Züchter)"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from datetime import date, timedelta
@ -78,47 +78,47 @@ def _calc_meilensteine(deckdatum_str: str) -> list:
# Schemas
# ------------------------------------------------------------------
class LaeufiCreate(BaseModel):
beginn: str
ende: Optional[str] = None
notiz: Optional[str] = None
beginn: str = Field(..., max_length=32)
ende: Optional[str] = Field(None, max_length=32)
notiz: Optional[str] = Field(None, max_length=2000)
class LaeufiUpdate(BaseModel):
beginn: Optional[str] = None
ende: Optional[str] = None
notiz: Optional[str] = None
beginn: Optional[str] = Field(None, max_length=32)
ende: Optional[str] = Field(None, max_length=32)
notiz: Optional[str] = Field(None, max_length=2000)
class ProgestCreate(BaseModel):
datum: str
datum: str = Field(..., max_length=32)
wert: Optional[float] = None
einheit: str = "ng/ml"
labor: Optional[str] = None
notiz: Optional[str] = None
einheit: str = Field("ng/ml", max_length=20)
labor: Optional[str] = Field(None, max_length=200)
notiz: Optional[str] = Field(None, max_length=2000)
class ProgestUpdate(BaseModel):
datum: Optional[str] = None
datum: Optional[str] = Field(None, max_length=32)
wert: Optional[float] = None
einheit: Optional[str] = None
labor: Optional[str] = None
notiz: Optional[str] = None
einheit: Optional[str] = Field(None, max_length=20)
labor: Optional[str] = Field(None, max_length=200)
notiz: Optional[str] = Field(None, max_length=2000)
class DeckCreate(BaseModel):
deckdatum: str
deckdatum: str = Field(..., max_length=32)
laeufi_id: Optional[int] = None
ruede_id: Optional[int] = None
ruede_name: Optional[str] = None
deckart: str = "natuerlich"
ruede_name: Optional[str] = Field(None, max_length=200)
deckart: str = Field("natuerlich", max_length=50)
traechtig: int = 0
ultraschall_datum: Optional[str] = None
notiz: Optional[str] = None
ultraschall_datum: Optional[str] = Field(None, max_length=32)
notiz: Optional[str] = Field(None, max_length=2000)
class DeckUpdate(BaseModel):
deckdatum: Optional[str] = None
deckdatum: Optional[str] = Field(None, max_length=32)
ruede_id: Optional[int] = None
ruede_name: Optional[str] = None
deckart: Optional[str] = None
ruede_name: Optional[str] = Field(None, max_length=200)
deckart: Optional[str] = Field(None, max_length=50)
traechtig: Optional[int] = None
ultraschall_datum: Optional[str] = None
notiz: Optional[str] = None
ultraschall_datum: Optional[str] = Field(None, max_length=32)
notiz: Optional[str] = Field(None, max_length=2000)
# ------------------------------------------------------------------

View file

@ -4,7 +4,7 @@ import logging
from datetime import date
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
@ -27,68 +27,68 @@ def _require_breeder(user=Depends(get_current_user)):
# Schemas
# ------------------------------------------------------------------
class LitterCreate(BaseModel):
wurf_rang: Optional[str] = None # A, B, C …
wurf_name: Optional[str] = None # z.B. "Vatertags-Wurf"
vater_name: Optional[str] = None
mutter_name: Optional[str] = None
wurf_rang: Optional[str] = Field(None, max_length=10) # A, B, C …
wurf_name: Optional[str] = Field(None, max_length=200) # z.B. "Vatertags-Wurf"
vater_name: Optional[str] = Field(None, max_length=200)
mutter_name: Optional[str] = Field(None, max_length=200)
vater_id: Optional[int] = None
mutter_id: Optional[int] = None
geburt_datum: Optional[str] = None
erwartetes_datum: Optional[str] = None
geburt_datum: Optional[str] = Field(None, max_length=32)
erwartetes_datum: Optional[str] = Field(None, max_length=32)
welpen_gesamt: Optional[int] = None
welpen_verfuegbar: Optional[int] = None
beschreibung: Optional[str] = None
gesundheitstests: Optional[str] = None
preis_spanne: Optional[str] = None
status: str = "geplant"
beschreibung: Optional[str] = Field(None, max_length=10000)
gesundheitstests: Optional[str] = Field(None, max_length=5000)
preis_spanne: Optional[str] = Field(None, max_length=100)
status: str = Field("geplant", max_length=30)
sichtbar: int = 0
sichtbar_bis: Optional[str] = None
sichtbar_bis: Optional[str] = Field(None, max_length=32)
class LitterUpdate(BaseModel):
wurf_rang: Optional[str] = None
wurf_name: Optional[str] = None
vater_name: Optional[str] = None
mutter_name: Optional[str] = None
wurf_rang: Optional[str] = Field(None, max_length=10)
wurf_name: Optional[str] = Field(None, max_length=200)
vater_name: Optional[str] = Field(None, max_length=200)
mutter_name: Optional[str] = Field(None, max_length=200)
vater_id: Optional[int] = None
mutter_id: Optional[int] = None
geburt_datum: Optional[str] = None
erwartetes_datum: Optional[str] = None
geburt_datum: Optional[str] = Field(None, max_length=32)
erwartetes_datum: Optional[str] = Field(None, max_length=32)
welpen_gesamt: Optional[int] = None
welpen_verfuegbar: Optional[int] = None
beschreibung: Optional[str] = None
gesundheitstests: Optional[str] = None
preis_spanne: Optional[str] = None
status: Optional[str] = None
beschreibung: Optional[str] = Field(None, max_length=10000)
gesundheitstests: Optional[str] = Field(None, max_length=5000)
preis_spanne: Optional[str] = Field(None, max_length=100)
status: Optional[str] = Field(None, max_length=30)
sichtbar: Optional[int] = None
sichtbar_bis: Optional[str] = None
sichtbar_bis: Optional[str] = Field(None, max_length=32)
class PuppyCreate(BaseModel):
name: Optional[str] = None
geschlecht: Optional[str] = None # maennlich|weiblich
farbe: Optional[str] = None
chip_nr: Optional[str] = None
name: Optional[str] = Field(None, max_length=80)
geschlecht: Optional[str] = Field(None, max_length=20) # maennlich|weiblich
farbe: Optional[str] = Field(None, max_length=100)
chip_nr: Optional[str] = Field(None, max_length=50)
geburtsgewicht: Optional[float] = None # Gramm
status: str = "verfuegbar" # verfuegbar|reserviert|abgegeben
status: str = Field("verfuegbar", max_length=30) # verfuegbar|reserviert|abgegeben
status_sichtbar: int = 1
notiz: Optional[str] = None
notiz: Optional[str] = Field(None, max_length=2000)
class PuppyUpdate(BaseModel):
name: Optional[str] = None
geschlecht: Optional[str] = None
farbe: Optional[str] = None
chip_nr: Optional[str] = None
name: Optional[str] = Field(None, max_length=80)
geschlecht: Optional[str] = Field(None, max_length=20)
farbe: Optional[str] = Field(None, max_length=100)
chip_nr: Optional[str] = Field(None, max_length=50)
geburtsgewicht: Optional[float] = None
status: Optional[str] = None
status: Optional[str] = Field(None, max_length=30)
status_sichtbar: Optional[int] = None
notiz: Optional[str] = None
notiz: Optional[str] = Field(None, max_length=2000)
class WeightEntry(BaseModel):
gewicht_g: float
gemessen_am: str # YYYY-MM-DD
gemessen_am: str = Field(..., max_length=32) # YYYY-MM-DD
# ------------------------------------------------------------------
@ -663,15 +663,15 @@ async def generate_contract(
# Warteliste
# ------------------------------------------------------------------
class WaitlistEntry(BaseModel):
name: str
email: Optional[str] = None
telefon: Optional[str] = None
nachricht: Optional[str] = None
wunsch_geschlecht: str = "egal"
wunsch_farbe: Optional[str] = None
name: str = Field(..., min_length=1, max_length=200)
email: Optional[str] = Field(None, max_length=254)
telefon: Optional[str] = Field(None, max_length=30)
nachricht: Optional[str] = Field(None, max_length=5000)
wunsch_geschlecht: str = Field("egal", max_length=20)
wunsch_farbe: Optional[str] = Field(None, max_length=100)
prioritaet: int = 0
status: str = "anfrage"
notiz: Optional[str] = None
status: str = Field("anfrage", max_length=30)
notiz: Optional[str] = Field(None, max_length=2000)
class WaitlistUpdate(BaseModel):

View file

@ -3,7 +3,7 @@
import os, uuid
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -20,13 +20,13 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
# Schemas
# ------------------------------------------------------------------
class LostDogCreate(BaseModel):
name: str
rasse: Optional[str] = None
beschreibung: str
name: str = Field(..., min_length=1, max_length=80)
rasse: Optional[str] = Field(None, max_length=80)
beschreibung: str = Field(..., min_length=3, max_length=5000)
lat: float
lon: float
dog_id: Optional[int] = None
client_time: Optional[str] = None
client_time: Optional[str] = Field(None, max_length=64)
# ------------------------------------------------------------------

View file

@ -1,7 +1,7 @@
"""BAN YARO — Hunde-Filme Routes"""
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from database import db
@ -207,31 +207,31 @@ class HundDesMonatsVoteRequest(BaseModel):
dog_id: int
class MovieCreate(BaseModel):
id: str
titel: str
originaltitel: Optional[str] = None
id: str = Field(..., max_length=100)
titel: str = Field(..., min_length=1, max_length=200)
originaltitel: Optional[str] = Field(None, max_length=200)
jahr: Optional[int] = None
genre: Optional[str] = None
typ: str = "film"
hund_rasse: Optional[str] = None
stirbt_der_hund: bool = False
beschreibung: Optional[str] = None
bild_emoji: str = "🐾"
genre: Optional[str] = Field(None, max_length=100)
typ: str = Field("film", max_length=30)
hund_rasse: Optional[str] = Field(None, max_length=200)
stirbt_der_hund: bool = False
beschreibung: Optional[str] = Field(None, max_length=5000)
bild_emoji: str = Field("🐾", max_length=10)
imdb_rating: Optional[float] = None
streaming: Optional[str] = None
streaming: Optional[str] = Field(None, max_length=500)
class MovieUpdate(BaseModel):
titel: Optional[str] = None
originaltitel: Optional[str] = None
titel: Optional[str] = Field(None, max_length=200)
originaltitel: Optional[str] = Field(None, max_length=200)
jahr: Optional[int] = None
genre: Optional[str] = None
typ: Optional[str] = None
hund_rasse: Optional[str] = None
genre: Optional[str] = Field(None, max_length=100)
typ: Optional[str] = Field(None, max_length=30)
hund_rasse: Optional[str] = Field(None, max_length=200)
stirbt_der_hund: Optional[bool] = None
beschreibung: Optional[str] = None
bild_emoji: Optional[str] = None
beschreibung: Optional[str] = Field(None, max_length=5000)
bild_emoji: Optional[str] = Field(None, max_length=10)
imdb_rating: Optional[float] = None
streaming: Optional[str] = None
streaming: Optional[str] = Field(None, max_length=500)
# ------------------------------------------------------------------

View file

@ -4,7 +4,7 @@ import json
import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional, Any, List
from database import db
from auth import get_current_user
@ -18,18 +18,18 @@ logger = logging.getLogger(__name__)
# Schemas
# ------------------------------------------------------------------
class NoteCreate(BaseModel):
text: str
meta_json: Optional[Any] = None
location_name: Optional[str] = None
parent_label: Optional[str] = None
client_time: Optional[str] = None
text: str = Field(..., min_length=1, max_length=5000)
meta_json: Optional[Any] = None
location_name: Optional[str] = Field(None, max_length=300)
parent_label: Optional[str] = Field(None, max_length=200)
client_time: Optional[str] = Field(None, max_length=64)
class NoteUpdate(BaseModel):
text: Optional[str] = None
text: Optional[str] = Field(None, max_length=5000)
meta_json: Optional[Any] = None
location_name: Optional[str] = None
parent_label: Optional[str] = None
location_name: Optional[str] = Field(None, max_length=300)
parent_label: Optional[str] = Field(None, max_length=200)
# ------------------------------------------------------------------

View file

@ -9,7 +9,7 @@ import httpx
import logging
from typing import Optional
from fastapi import APIRouter, Query, BackgroundTasks, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from database import db
from auth import get_current_user, get_current_user_optional as get_optional_user
@ -273,11 +273,11 @@ async def get_pois(
# POST /user-poi — Community-Marker setzen
# ------------------------------------------------------------------
class UserPoiIn(BaseModel):
type: str
type: str = Field(..., max_length=200)
lat: float
lon: float
name: Optional[str] = None
notiz: Optional[str] = None
name: Optional[str] = Field(None, max_length=300)
notiz: Optional[str] = Field(None, max_length=2000)
ALLOWED_TYPES = {
'waste_basket', 'drinking_water', 'dog_park',
@ -331,8 +331,8 @@ async def delete_user_poi(poi_id: int, user = Depends(get_current_user)):
# POST /report — Marker als ungültig melden
# ------------------------------------------------------------------
class ReportIn(BaseModel):
type: str
grund: str
type: str = Field(..., max_length=100)
grund: str = Field(..., max_length=200)
osm_id: Optional[int] = None
user_poi_id: Optional[int] = None
@ -388,9 +388,9 @@ async def analyze_region(
# POST /pois/{osm_id}/edit — Nutzer schlägt Korrektur vor
# ------------------------------------------------------------------
class PoiEditCreate(BaseModel):
poi_name: str
field: str = 'opening_hours'
new_value: str
poi_name: str = Field(..., max_length=300)
field: str = Field('opening_hours', max_length=50)
new_value: str = Field(..., max_length=1000)
@router.post('/pois/{osm_id}/edit', status_code=201)

View file

@ -13,7 +13,7 @@ from typing import List, Optional
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from auth import require_admin
from database import db
@ -135,26 +135,26 @@ def _send_smtp(to: str, subject: str, body: str, account: str = "partner", html:
# ------------------------------------------------------------------
class TemplateIn(BaseModel):
key: str
label: str
subject: str
body: str
from_account: str = "partner"
key: str = Field(..., max_length=100)
label: str = Field(..., max_length=200)
subject: str = Field(..., max_length=500)
body: str = Field(..., max_length=50000)
from_account: str = Field("partner", max_length=50)
class TemplateUpdate(BaseModel):
label: str
subject: str
body: str
from_account: str = "partner"
label: str = Field(..., max_length=200)
subject: str = Field(..., max_length=500)
body: str = Field(..., max_length=50000)
from_account: str = Field("partner", max_length=50)
class SendRequest(BaseModel):
to: List[str]
subject: str
body: str
from_account: str = "partner"
template_id: Optional[int] = None
subject: str = Field(..., max_length=500)
body: str = Field(..., max_length=50000)
from_account: str = Field("partner", max_length=50)
template_id: Optional[int] = None
# ------------------------------------------------------------------

View file

@ -2,7 +2,7 @@
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from pydantic import BaseModel, Field
from database import db
from auth import require_admin, get_current_user
@ -10,8 +10,8 @@ router = APIRouter()
class PartnerCodeCreate(BaseModel):
code: str
label: str
code: str = Field(..., min_length=1, max_length=50)
label: str = Field(..., min_length=1, max_length=200)
grants_founder: int = 1
max_uses: Optional[int] = None

View file

@ -5,7 +5,7 @@ import secrets
from datetime import date, datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -17,25 +17,25 @@ router = APIRouter()
# Schemas
# ------------------------------------------------------------------
class PassportMeta(BaseModel):
blutgruppe: Optional[str] = None
allergien: Optional[str] = None
besonderheiten: Optional[str] = None
blutgruppe: Optional[str] = Field(None, max_length=50)
allergien: Optional[str] = Field(None, max_length=2000)
besonderheiten: Optional[str] = Field(None, max_length=2000)
class VaccinationCreate(BaseModel):
krankheit: str
datum: str
naechste: Optional[str] = None
tierarzt: Optional[str] = None
charge_nr: Optional[str] = None
krankheit: str = Field(..., max_length=200)
datum: str = Field(..., max_length=32)
naechste: Optional[str] = Field(None, max_length=32)
tierarzt: Optional[str] = Field(None, max_length=200)
charge_nr: Optional[str] = Field(None, max_length=100)
class MedicationCreate(BaseModel):
name: str
dosierung: Optional[str] = None
von: Optional[str] = None
bis: Optional[str] = None
notiz: Optional[str] = None
name: str = Field(..., max_length=200)
dosierung: Optional[str] = Field(None, max_length=200)
von: Optional[str] = Field(None, max_length=32)
bis: Optional[str] = Field(None, max_length=32)
notiz: Optional[str] = Field(None, max_length=2000)
# ------------------------------------------------------------------

View file

@ -1,7 +1,7 @@
"""BAN YARO — Hundefreundliche Orte"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user, require_owner
@ -16,25 +16,25 @@ TYPEN = {'restaurant', 'shop', 'freilauf', 'kotbeutel', 'tierarzt', 'hundesalon'
# Schemas
# ------------------------------------------------------------------
class PlaceCreate(BaseModel):
name: str
typ: str
name: str = Field(..., min_length=1, max_length=200)
typ: str = Field(..., max_length=50)
lat: float
lon: float
adresse: Optional[str] = None
website: Optional[str] = None
telefon: Optional[str] = None
adresse: Optional[str] = Field(None, max_length=300)
website: Optional[str] = Field(None, max_length=500)
telefon: Optional[str] = Field(None, max_length=30)
hund_rein: Optional[bool] = None
leine_pflicht: Optional[bool] = None
wasser_fuer_hunde: Optional[bool] = None
class PlaceUpdate(BaseModel):
name: Optional[str] = None
typ: Optional[str] = None
name: Optional[str] = Field(None, max_length=200)
typ: Optional[str] = Field(None, max_length=50)
lat: Optional[float]= None
lon: Optional[float]= None
adresse: Optional[str] = None
website: Optional[str] = None
telefon: Optional[str] = None
adresse: Optional[str] = Field(None, max_length=300)
website: Optional[str] = Field(None, max_length=500)
telefon: Optional[str] = Field(None, max_length=30)
hund_rein: Optional[bool] = None
leine_pflicht: Optional[bool] = None
wasser_fuer_hunde: Optional[bool] = None

View file

@ -2,7 +2,7 @@
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -40,18 +40,18 @@ class ListingUpsert(BaseModel):
dog_id: int
lat: float
lon: float
ort_name: Optional[str] = None
ort_name: Optional[str] = Field(None, max_length=300)
radius_km: int = 10
beschreibung: Optional[str] = None
beschreibung: Optional[str] = Field(None, max_length=2000)
class RequestCreate(BaseModel):
to_dog_id: int
nachricht: Optional[str] = None
nachricht: Optional[str] = Field(None, max_length=2000)
class RequestPatch(BaseModel):
status: str # accepted | declined
status: str = Field(..., max_length=30) # accepted | declined
# ------------------------------------------------------------------

View file

@ -3,7 +3,7 @@
import os, uuid
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -22,12 +22,12 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
class PoisonCreate(BaseModel):
lat: float
lon: float
beschreibung: Optional[str] = None
typ: str = "unbekannt"
beschreibung: Optional[str] = Field(None, max_length=2000)
typ: str = Field("unbekannt", max_length=50)
class PoisonResolve(BaseModel):
grund: str = "beseitigt" # beseitigt | fehlerhaft | anderes
grund: str = Field("beseitigt", max_length=50) # beseitigt | fehlerhaft | anderes
# ------------------------------------------------------------------

View file

@ -7,7 +7,7 @@ import uuid
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from auth import get_current_user
from database import db
@ -20,17 +20,17 @@ VALID_SICHTBARKEIT = {"public", "friends", "private"}
class ProfileUpdate(BaseModel):
real_name: Optional[str] = None
bio: Optional[str] = None
wohnort: Optional[str] = None
erfahrung: Optional[str] = None
social_link: Optional[str] = None
profil_sichtbarkeit: Optional[str] = None
real_name: Optional[str] = Field(None, max_length=100)
bio: Optional[str] = Field(None, max_length=300)
wohnort: Optional[str] = Field(None, max_length=60)
erfahrung: Optional[str] = Field(None, max_length=30)
social_link: Optional[str] = Field(None, max_length=120)
profil_sichtbarkeit: Optional[str] = Field(None, max_length=30)
notes_ki_enabled: Optional[int] = None
gassi_stunde_push: Optional[int] = None
preferred_theme: Optional[str] = None
billing_address: Optional[str] = None
geburtstag: Optional[str] = None
preferred_theme: Optional[str] = Field(None, max_length=20)
billing_address: Optional[str] = Field(None, max_length=500)
geburtstag: Optional[str] = Field(None, max_length=10)
def _load_user(user_id: int) -> dict:
@ -61,12 +61,7 @@ async def update_profile(data: ProfileUpdate, user=Depends(get_current_user)):
raise HTTPException(400, f"profil_sichtbarkeit muss eines von {sorted(VALID_SICHTBARKEIT)} sein.")
if "preferred_theme" in fields and fields["preferred_theme"] not in ("system", "light", "dark"):
raise HTTPException(400, "preferred_theme muss 'system', 'light' oder 'dark' sein.")
if "bio" in fields and len(fields["bio"]) > 300:
raise HTTPException(400, "bio darf maximal 300 Zeichen lang sein.")
if "wohnort" in fields and len(fields["wohnort"]) > 60:
raise HTTPException(400, "wohnort darf maximal 60 Zeichen lang sein.")
if "social_link" in fields and len(fields["social_link"]) > 120:
raise HTTPException(400, "social_link darf maximal 120 Zeichen lang sein.")
# Längen-Begrenzungen sind jetzt via Field max_length im Schema abgedeckt.
if "geburtstag" in fields and fields["geburtstag"]:
if not re.fullmatch(r"\d{2}\.\d{2}", fields["geburtstag"]):
raise HTTPException(400, "geburtstag muss im Format TT.MM sein (z.B. 16.05).")

View file

@ -4,7 +4,7 @@ import os
import json
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from pywebpush import webpush, WebPushException
@ -33,7 +33,7 @@ async def get_vapid_key():
# POST /api/push/subscribe — Subscription speichern
# ------------------------------------------------------------------
class PushSubscription(BaseModel):
endpoint: str
endpoint: str = Field(..., max_length=2000)
keys: dict # { p256dh, auth }
expirationTime: Optional[int] = None

View file

@ -1,7 +1,7 @@
"""BAN YARO — Bewertungssystem (Ratings)"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -23,10 +23,10 @@ TABLE_MAP = {
# Schemas
# ------------------------------------------------------------------
class RatingCreate(BaseModel):
target_type: str
target_type: str = Field(..., max_length=50)
target_id: int
stars: int
kommentar: Optional[str] = None
kommentar: Optional[str] = Field(None, max_length=5000)
# ------------------------------------------------------------------

View file

@ -5,7 +5,7 @@ import json, os, uuid
import httpx
import polyline as _polyline
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional, List
from database import db
from auth import get_current_user, get_current_user_optional
@ -37,29 +37,29 @@ class GPSPoint(BaseModel):
alt: Optional[float] = None
class RouteCreate(BaseModel):
name: str
beschreibung: Optional[str] = None
name: str = Field(..., min_length=1, max_length=200)
beschreibung: Optional[str] = Field(None, max_length=5000)
gps_track: List[GPSPoint]
distanz_km: Optional[float] = None
dauer_min: Optional[int] = None
schwierigkeit: Optional[str] = "leicht" # leicht | mittel | anspruchsvoll
untergrund: Optional[str] = None # wald | asphalt | wiese | mix
schwierigkeit: Optional[str] = Field("leicht", max_length=30) # leicht | mittel | anspruchsvoll
untergrund: Optional[str] = Field(None, max_length=50) # wald | asphalt | wiese | mix
schatten: Optional[bool] = None
leine_empfohlen: Optional[bool] = None
is_public: Optional[bool] = False
hunde_tauglichkeit: Optional[str] = None # eingeschränkt | gut | sehr_gut | premium
client_time: Optional[str] = None
hunde_tauglichkeit: Optional[str] = Field(None, max_length=50) # eingeschränkt | gut | sehr_gut | premium
client_time: Optional[str] = Field(None, max_length=64)
dog_ids: Optional[List[int]] = None # Welche Hunde mitgegangen sind
class RouteUpdate(BaseModel):
name: Optional[str] = None
beschreibung: Optional[str] = None
schwierigkeit: Optional[str] = None
untergrund: Optional[str] = None
name: Optional[str] = Field(None, max_length=200)
beschreibung: Optional[str] = Field(None, max_length=5000)
schwierigkeit: Optional[str] = Field(None, max_length=30)
untergrund: Optional[str] = Field(None, max_length=50)
schatten: Optional[bool] = None
leine_empfohlen: Optional[bool] = None
is_public: Optional[bool] = None
hunde_tauglichkeit: Optional[str] = None
hunde_tauglichkeit: Optional[str] = Field(None, max_length=50)
class RouteDogs(BaseModel):
dog_ids: List[int]
@ -553,7 +553,7 @@ async def add_route_photo(
# POST /api/routes/{id}/feedback — Feedback an Route-Ersteller
# ------------------------------------------------------------------
class RouteFeedback(BaseModel):
text: str
text: str = Field(..., min_length=5, max_length=2000)
@router.post("/{route_id}/feedback", status_code=201)
async def route_feedback(route_id: int, data: RouteFeedback, user=Depends(get_current_user)):

View file

@ -1,7 +1,7 @@
"""BAN YARO — Service-Angebote (Sitting & Walks Matching)"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -16,8 +16,8 @@ ALLOWED_TYPES = {'sitting', 'walks'}
# Schemas
# ------------------------------------------------------------------
class ServiceCreate(BaseModel):
type: str
beschreibung: Optional[str] = None
type: str = Field(..., max_length=30)
beschreibung: Optional[str] = Field(None, max_length=5000)
preis_pro_tag: Optional[float] = None
lat: Optional[float] = None
lon: Optional[float] = None

View file

@ -2,7 +2,7 @@
import secrets
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from database import db
from auth import get_current_user
@ -14,7 +14,7 @@ share_router = APIRouter()
class ShareInvite(BaseModel):
role: str = "editor" # viewer | editor
role: str = Field("editor", max_length=20) # viewer | editor
# ------------------------------------------------------------------

View file

@ -2,7 +2,7 @@
import json
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional, List
from database import db
from auth import get_current_user
@ -17,7 +17,7 @@ SERVICES = {'tagesbetreuung', 'uebernachtung', 'gassi', 'hausbesuch'}
# Schemas
# ------------------------------------------------------------------
class SitterCreate(BaseModel):
beschreibung: Optional[str] = None
beschreibung: Optional[str] = Field(None, max_length=5000)
preis_pro_tag: float = 0
max_hunde: int = 1
lat: Optional[float] = None
@ -26,7 +26,7 @@ class SitterCreate(BaseModel):
services: List[str] = []
class SitterUpdate(BaseModel):
beschreibung: Optional[str] = None
beschreibung: Optional[str] = Field(None, max_length=5000)
preis_pro_tag: Optional[float] = None
max_hunde: Optional[int] = None
lat: Optional[float] = None
@ -38,12 +38,12 @@ class SitterUpdate(BaseModel):
class RequestCreate(BaseModel):
sitter_id: int
dog_ids: List[int] = []
von: str # YYYY-MM-DD
bis: str
nachricht: Optional[str] = None
von: str = Field(..., max_length=32) # YYYY-MM-DD
bis: str = Field(..., max_length=32)
nachricht: Optional[str] = Field(None, max_length=2000)
class RequestUpdate(BaseModel):
status: str # angenommen | abgelehnt | abgebrochen
status: str = Field(..., max_length=30) # angenommen | abgelehnt | abgebrochen
# ------------------------------------------------------------------

View file

@ -1,7 +1,7 @@
"""BAN YARO — Gasthund-Zugang (Sitter-Subscriptions)"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from database import db
from auth import get_current_user
@ -11,7 +11,7 @@ router = APIRouter()
class AccessCreate(BaseModel):
dog_id: int
sitter_id: int
valid_until: str # 'YYYY-MM-DD'
valid_until: str = Field(..., max_length=32) # 'YYYY-MM-DD'
@router.post("", status_code=201)

View file

@ -9,7 +9,7 @@ import random
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from auth import get_current_user, require_social_media
from database import db
@ -849,24 +849,24 @@ Antworte NUR mit einem JSON-Objekt:
class GenerateRequest(BaseModel):
platform: str = "both"
format: str = "post"
topic: str
platform: str = Field("both", max_length=30)
format: str = Field("post", max_length=30)
topic: str = Field(..., min_length=2, max_length=500)
breed_id: Optional[int] = None
class EvaluateRequest(BaseModel):
platform: str = "instagram"
format: str = "post"
draft: str
platform: str = Field("instagram", max_length=30)
format: str = Field("post", max_length=30)
draft: str = Field(..., min_length=1, max_length=10000)
class StatusUpdate(BaseModel):
status: Optional[str] = None
scheduled_at: Optional[str] = None
published_at: Optional[str] = None
notes: Optional[str] = None
post_url: Optional[str] = None
status: Optional[str] = Field(None, max_length=50)
scheduled_at: Optional[str] = Field(None, max_length=64)
published_at: Optional[str] = Field(None, max_length=64)
notes: Optional[str] = Field(None, max_length=5000)
post_url: Optional[str] = Field(None, max_length=500)
def _used_topics(limit: int = 30) -> str:

View file

@ -2,7 +2,7 @@
import math
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user
@ -11,20 +11,20 @@ router = APIRouter()
class TierarztCreate(BaseModel):
name: str
strasse: Optional[str] = None
plz: Optional[str] = None
ort: Optional[str] = None
telefon: Optional[str] = None
notfall_telefon: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
notizen: Optional[str] = None
name: str = Field(..., min_length=1, max_length=200)
strasse: Optional[str] = Field(None, max_length=300)
plz: Optional[str] = Field(None, max_length=20)
ort: Optional[str] = Field(None, max_length=200)
telefon: Optional[str] = Field(None, max_length=30)
notfall_telefon: Optional[str] = Field(None, max_length=30)
email: Optional[str] = Field(None, max_length=254)
website: Optional[str] = Field(None, max_length=500)
notizen: Optional[str] = Field(None, max_length=5000)
ist_notfallpraxis: bool = False
opening_hours: Optional[str] = None
opening_hours: Optional[str] = Field(None, max_length=500)
lat: Optional[float] = None
lon: Optional[float] = None
osm_id: Optional[str] = None
osm_id: Optional[str] = Field(None, max_length=100)
class BewertungCreate(BaseModel):
@ -32,25 +32,25 @@ class BewertungCreate(BaseModel):
wartezeit: Optional[int] = None
freundlichkeit: Optional[int] = None
kompetenz: Optional[int] = None
text: Optional[str] = None
text: Optional[str] = Field(None, max_length=5000)
class TierarztUpdate(BaseModel):
name: Optional[str] = None
strasse: Optional[str] = None
plz: Optional[str] = None
ort: Optional[str] = None
telefon: Optional[str] = None
notfall_telefon: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
notizen: Optional[str] = None
name: Optional[str] = Field(None, max_length=200)
strasse: Optional[str] = Field(None, max_length=300)
plz: Optional[str] = Field(None, max_length=20)
ort: Optional[str] = Field(None, max_length=200)
telefon: Optional[str] = Field(None, max_length=30)
notfall_telefon: Optional[str] = Field(None, max_length=30)
email: Optional[str] = Field(None, max_length=254)
website: Optional[str] = Field(None, max_length=500)
notizen: Optional[str] = Field(None, max_length=5000)
ist_notfallpraxis: Optional[bool] = None
aktiv: Optional[bool] = None
opening_hours: Optional[str] = None
opening_hours: Optional[str] = Field(None, max_length=500)
lat: Optional[float] = None
lon: Optional[float] = None
osm_id: Optional[str] = None
osm_id: Optional[str] = Field(None, max_length=100)
def _fmt_opening_hours(raw: str | None) -> str | None:

View file

@ -1,7 +1,7 @@
"""BAN YARO — Übungs- & Trainingsfortschritt"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
import datetime
import ki
@ -61,9 +61,9 @@ async def get_exercises():
# Admin: Übung bearbeiten (beschreibung / schritte / tipp)
# ------------------------------------------------------------------
class ExerciseUpdate(BaseModel):
beschreibung: Optional[str] = None
schritte: Optional[str] = None # JSON-String: '["Schritt 1", ...]'
tipp: Optional[str] = None
beschreibung: Optional[str] = Field(None, max_length=10000)
schritte: Optional[str] = Field(None, max_length=10000) # JSON-String: '["Schritt 1", ...]'
tipp: Optional[str] = Field(None, max_length=5000)
@router.put("/exercises/{exercise_id}")
async def update_exercise(exercise_id: int, body: ExerciseUpdate, _=Depends(require_admin)):
@ -93,9 +93,9 @@ async def update_exercise(exercise_id: int, body: ExerciseUpdate, _=Depends(requ
# Übungs-Status
# ------------------------------------------------------------------
class ProgressUpdate(BaseModel):
exercise_id: str
status: Optional[str] = None
dog_id: Optional[int] = None
exercise_id: str = Field(..., max_length=200)
status: Optional[str] = Field(None, max_length=50)
dog_id: Optional[int] = None
@router.get("/progress")
async def get_progress(dog_id: Optional[int] = None, user=Depends(get_current_user)):
@ -137,9 +137,9 @@ async def upsert_progress(body: ProgressUpdate, user=Depends(get_current_user)):
# Trainingsplan-Checkboxen
# ------------------------------------------------------------------
class PlanProgress(BaseModel):
item_key: str
checked: bool
dog_id: Optional[int] = None
item_key: str = Field(..., max_length=200)
checked: bool
dog_id: Optional[int] = None
@router.get("/plan-progress")
async def get_plan_progress(dog_id: Optional[int] = None, user=Depends(get_current_user)):
@ -327,15 +327,15 @@ def _check_badges(conn, user_id: int, dog_name: str) -> list:
class SessionCreate(BaseModel):
dog_id: int
exercise_id: str
exercise_name: str
datum: Optional[str] = None
wiederholungen: int = 1
erfolgsquote: int = 50
hund_stimmung: Optional[str] = "aufmerksam"
exercise_id: str = Field(..., max_length=200)
exercise_name: str = Field(..., max_length=200)
datum: Optional[str] = Field(None, max_length=32)
wiederholungen: int = 1
erfolgsquote: int = 50
hund_stimmung: Optional[str] = Field("aufmerksam", max_length=50)
zufriedenheit: Optional[int] = 3
notiz: Optional[str] = None
tagebuch_eintrag: bool = False # ignoriert — Training hat eigenes Protokoll
notiz: Optional[str] = Field(None, max_length=2000)
tagebuch_eintrag: bool = False # ignoriert — Training hat eigenes Protokoll
@router.post("/sessions")

View file

@ -4,7 +4,7 @@ import os, uuid
import httpx
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional, List
from database import db
from auth import get_current_user
@ -20,24 +20,24 @@ router = APIRouter()
# Schemas
# ------------------------------------------------------------------
class WalkCreate(BaseModel):
titel: str
datum: str # YYYY-MM-DD
uhrzeit: str # HH:MM
titel: str = Field(..., min_length=1, max_length=200)
datum: str = Field(..., max_length=32) # YYYY-MM-DD
uhrzeit: str = Field(..., max_length=20) # HH:MM
lat: float
lon: float
ort_name: Optional[str] = None
ort_name: Optional[str] = Field(None, max_length=300)
max_teilnehmer: int = 10
beschreibung: Optional[str] = None
beschreibung: Optional[str] = Field(None, max_length=5000)
class WalkUpdate(BaseModel):
titel: Optional[str] = None
datum: Optional[str] = None
uhrzeit: Optional[str] = None
titel: Optional[str] = Field(None, max_length=200)
datum: Optional[str] = Field(None, max_length=32)
uhrzeit: Optional[str] = Field(None, max_length=20)
lat: Optional[float] = None
lon: Optional[float] = None
ort_name: Optional[str] = None
ort_name: Optional[str] = Field(None, max_length=300)
max_teilnehmer: Optional[int] = None
beschreibung: Optional[str] = None
beschreibung: Optional[str] = Field(None, max_length=5000)
class JoinRequest(BaseModel):
dog_ids: List[int] = [] # leere Liste = ohne Hund (selten)
@ -46,7 +46,7 @@ class InviteRequest(BaseModel):
friend_id: int
class RsvpRequest(BaseModel):
status: str # 'yes' | 'maybe' | 'no'
status: str = Field(..., max_length=20) # 'yes' | 'maybe' | 'no'
# ------------------------------------------------------------------

View file

@ -6,7 +6,7 @@ import time
import logging
from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request, UploadFile, File
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from pydantic import BaseModel, Field
from database import db
from auth import get_current_user, get_current_user_optional
from ratelimit import check as rl_check, block_ip
@ -36,9 +36,9 @@ async def honeypot(request: Request):
# Schemas
# ------------------------------------------------------------------
class BerichtCreate(BaseModel):
rasse: str
titel: str
text: str
rasse: str = Field(..., max_length=100)
titel: str = Field(..., min_length=3, max_length=200)
text: str = Field(..., min_length=10, max_length=10000)
# ------------------------------------------------------------------
@ -411,8 +411,8 @@ async def list_submissions(user=Depends(get_current_user)):
# PATCH /api/wiki/foto-submissions/{id} — genehmigen oder ablehnen
# ------------------------------------------------------------------
class ReviewModel(BaseModel):
action: str # "approve" | "reject"
reject_reason: str = ""
action: str = Field(..., max_length=30) # "approve" | "reject"
reject_reason: str = Field("", max_length=2000)
@router.patch("/foto-submissions/{sub_id}")
@ -575,19 +575,19 @@ async def get_rasse_stats(slug: str, user=Depends(get_current_user_optional)):
# Schemas für Interesse und Züchter
# ------------------------------------------------------------------
class InteresseCreate(BaseModel):
typ: str # "hat" oder "will"
typ: str = Field(..., max_length=30) # "hat" oder "will"
class ZuchterCreate(BaseModel):
rasse_slug: str
name: str
zwingername: str = ""
ort: str = ""
plz: str = ""
bundesland: str = ""
rasse_slug: str = Field(..., max_length=100)
name: str = Field(..., min_length=1, max_length=200)
zwingername: str = Field("", max_length=200)
ort: str = Field("", max_length=200)
plz: str = Field("", max_length=20)
bundesland: str = Field("", max_length=100)
vdh_mitglied: int = 0
website: str = ""
telefon: str = ""
beschreibung: str = ""
website: str = Field("", max_length=500)
telefon: str = Field("", max_length=30)
beschreibung: str = Field("", max_length=10000)
# ------------------------------------------------------------------

View file

@ -2,7 +2,7 @@
import logging
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
@ -134,108 +134,108 @@ def _ik_rating(ik: float) -> str:
# Pydantic-Schemas
# ------------------------------------------------------------------
class HundCreate(BaseModel):
name: str
rufname: Optional[str] = None
geschlecht: str # maennlich|weiblich
geburtsdatum: Optional[str] = None
sterbedatum: Optional[str] = None
chip_nr: Optional[str] = None
taetowiernummer: Optional[str] = None
zuchtbuchnummer: Optional[str] = None
farbe: Optional[str] = None
name: str = Field(..., min_length=1, max_length=200)
rufname: Optional[str] = Field(None, max_length=80)
geschlecht: str = Field(..., max_length=20) # maennlich|weiblich
geburtsdatum: Optional[str] = Field(None, max_length=32)
sterbedatum: Optional[str] = Field(None, max_length=32)
chip_nr: Optional[str] = Field(None, max_length=50)
taetowiernummer: Optional[str] = Field(None, max_length=50)
zuchtbuchnummer: Optional[str] = Field(None, max_length=100)
farbe: Optional[str] = Field(None, max_length=100)
vater_id: Optional[int] = None
mutter_id: Optional[int] = None
zuechter_name: Optional[str] = None
eigentuemer_name: Optional[str] = None
zuechter_name: Optional[str] = Field(None, max_length=200)
eigentuemer_name: Optional[str] = Field(None, max_length=200)
is_public: int = 1
notiz: Optional[str] = None
foto_url: Optional[str] = None
notiz: Optional[str] = Field(None, max_length=5000)
foto_url: Optional[str] = Field(None, max_length=500)
class HundUpdate(BaseModel):
name: Optional[str] = None
rufname: Optional[str] = None
geschlecht: Optional[str] = None
geburtsdatum: Optional[str] = None
sterbedatum: Optional[str] = None
chip_nr: Optional[str] = None
taetowiernummer: Optional[str] = None
zuchtbuchnummer: Optional[str] = None
farbe: Optional[str] = None
name: Optional[str] = Field(None, max_length=200)
rufname: Optional[str] = Field(None, max_length=80)
geschlecht: Optional[str] = Field(None, max_length=20)
geburtsdatum: Optional[str] = Field(None, max_length=32)
sterbedatum: Optional[str] = Field(None, max_length=32)
chip_nr: Optional[str] = Field(None, max_length=50)
taetowiernummer: Optional[str] = Field(None, max_length=50)
zuchtbuchnummer: Optional[str] = Field(None, max_length=100)
farbe: Optional[str] = Field(None, max_length=100)
vater_id: Optional[int] = None
mutter_id: Optional[int] = None
zuechter_name: Optional[str] = None
eigentuemer_name: Optional[str] = None
zuechter_name: Optional[str] = Field(None, max_length=200)
eigentuemer_name: Optional[str] = Field(None, max_length=200)
is_public: Optional[int] = None
notiz: Optional[str] = None
foto_url: Optional[str] = None
notiz: Optional[str] = Field(None, max_length=5000)
foto_url: Optional[str] = Field(None, max_length=500)
class HealthTestCreate(BaseModel):
test_typ: str # HD|ED|OCD|augen|herz|patella|ZTP|custom
test_name: Optional[str] = None
ergebnis: Optional[str] = None
untersuch_am: Optional[str] = None
gueltig_bis: Optional[str] = None
untersucher: Optional[str] = None
labor: Optional[str] = None
zertifikat_nr: Optional[str] = None
test_typ: str = Field(..., max_length=50) # HD|ED|OCD|augen|herz|patella|ZTP|custom
test_name: Optional[str] = Field(None, max_length=200)
ergebnis: Optional[str] = Field(None, max_length=500)
untersuch_am: Optional[str] = Field(None, max_length=32)
gueltig_bis: Optional[str] = Field(None, max_length=32)
untersucher: Optional[str] = Field(None, max_length=200)
labor: Optional[str] = Field(None, max_length=200)
zertifikat_nr: Optional[str] = Field(None, max_length=100)
is_public: int = 1
class HealthTestUpdate(BaseModel):
test_typ: Optional[str] = None
test_name: Optional[str] = None
ergebnis: Optional[str] = None
untersuch_am: Optional[str] = None
gueltig_bis: Optional[str] = None
untersucher: Optional[str] = None
labor: Optional[str] = None
zertifikat_nr: Optional[str] = None
test_typ: Optional[str] = Field(None, max_length=50)
test_name: Optional[str] = Field(None, max_length=200)
ergebnis: Optional[str] = Field(None, max_length=500)
untersuch_am: Optional[str] = Field(None, max_length=32)
gueltig_bis: Optional[str] = Field(None, max_length=32)
untersucher: Optional[str] = Field(None, max_length=200)
labor: Optional[str] = Field(None, max_length=200)
zertifikat_nr: Optional[str] = Field(None, max_length=100)
is_public: Optional[int] = None
class GeneticTestCreate(BaseModel):
marker_name: str # MDR1|PRA-prcd|DM|vWD|HUU etc.
marker_kategorie: Optional[str] = None # krankheit|farbe|eigenschaft
genotyp: Optional[str] = None # +/+|+/-|-/-
ergebnis_klasse: Optional[str] = None # clear|carrier|affected
getestet_am: Optional[str] = None
labor: Optional[str] = None
zertifikat_nr: Optional[str] = None
marker_name: str = Field(..., max_length=100) # MDR1|PRA-prcd|DM|vWD|HUU etc.
marker_kategorie: Optional[str] = Field(None, max_length=50) # krankheit|farbe|eigenschaft
genotyp: Optional[str] = Field(None, max_length=20) # +/+|+/-|-/-
ergebnis_klasse: Optional[str] = Field(None, max_length=50) # clear|carrier|affected
getestet_am: Optional[str] = Field(None, max_length=32)
labor: Optional[str] = Field(None, max_length=200)
zertifikat_nr: Optional[str] = Field(None, max_length=100)
is_public: int = 1
class GeneticTestUpdate(BaseModel):
marker_name: Optional[str] = None
marker_kategorie: Optional[str] = None
genotyp: Optional[str] = None
ergebnis_klasse: Optional[str] = None
getestet_am: Optional[str] = None
labor: Optional[str] = None
zertifikat_nr: Optional[str] = None
marker_name: Optional[str] = Field(None, max_length=100)
marker_kategorie: Optional[str] = Field(None, max_length=50)
genotyp: Optional[str] = Field(None, max_length=20)
ergebnis_klasse: Optional[str] = Field(None, max_length=50)
getestet_am: Optional[str] = Field(None, max_length=32)
labor: Optional[str] = Field(None, max_length=200)
zertifikat_nr: Optional[str] = Field(None, max_length=100)
is_public: Optional[int] = None
class TitelCreate(BaseModel):
titel_typ: str # ausstellung|arbeit|sport|zucht|champion|custom
titel_name: str
verliehen_am: Optional[str] = None
ort: Optional[str] = None
richter: Optional[str] = None
ausstellung: Optional[str] = None
formwert: Optional[str] = None
titel_typ: str = Field(..., max_length=50) # ausstellung|arbeit|sport|zucht|champion|custom
titel_name: str = Field(..., min_length=1, max_length=200)
verliehen_am: Optional[str] = Field(None, max_length=32)
ort: Optional[str] = Field(None, max_length=200)
richter: Optional[str] = Field(None, max_length=200)
ausstellung: Optional[str] = Field(None, max_length=200)
formwert: Optional[str] = Field(None, max_length=100)
is_public: int = 1
class TitelUpdate(BaseModel):
titel_typ: Optional[str] = None
titel_name: Optional[str] = None
verliehen_am: Optional[str] = None
ort: Optional[str] = None
richter: Optional[str] = None
ausstellung: Optional[str] = None
formwert: Optional[str] = None
titel_typ: Optional[str] = Field(None, max_length=50)
titel_name: Optional[str] = Field(None, max_length=200)
verliehen_am: Optional[str] = Field(None, max_length=32)
ort: Optional[str] = Field(None, max_length=200)
richter: Optional[str] = Field(None, max_length=200)
ausstellung: Optional[str] = Field(None, max_length=200)
formwert: Optional[str] = Field(None, max_length=100)
is_public: Optional[int] = None

View file

@ -3,7 +3,7 @@
import logging
from datetime import date, timedelta
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional, Literal
from database import db
@ -41,7 +41,7 @@ class PaarungAnalyseBody(BaseModel):
vater_id: int
mutter_id: int
ik_prozent: Optional[float] = None
welfare_level: Optional[str] = None
welfare_level: Optional[str] = Field(None, max_length=50)
class HundBeschreibungBody(BaseModel):