""" Validierung & Konstanten für persönliche Referenzwerte. """ from __future__ import annotations from typing import Any, Optional from fastapi import HTTPException REF_VALUE_SOURCES = frozenset( { "manual_user", "manual_admin", "import_device", "import_app", "derived_system", "estimated_system", "test_entry", } ) REF_VALUE_METHODS = frozenset( { "direct_measurement", "lab_test", "field_test", "questionnaire", "formula_estimation", "trend_analysis", "device_algorithm", "manual_assessment", "imported_external", "unknown", } ) REF_VALUE_CONFIDENCE = frozenset({"high", "medium", "low", "unknown"}) # Anzeigereihenfolge (nicht alphabetisch) REF_VALUE_CONFIDENCE_ORDER = ("high", "medium", "low", "unknown") VALUE_DATA_TYPES = frozenset({"integer", "decimal", "percentage", "text", "enum"}) def _rules_dict(raw: Any) -> dict: if not raw: return {} if isinstance(raw, dict): return raw return {} def validate_meta_source(source: Optional[str]) -> str: if not source or not str(source).strip(): raise HTTPException(400, "Quelle (source) ist erforderlich.") s = str(source).strip() if s not in REF_VALUE_SOURCES: raise HTTPException(400, f"Ungültige Quelle: {s}") return s def validate_meta_method(method: Optional[str]) -> str: if not method or not str(method).strip(): raise HTTPException(400, "Methode (method) ist erforderlich.") m = str(method).strip() if m not in REF_VALUE_METHODS: raise HTTPException(400, f"Ungültige Methode: {m}") return m def validate_meta_confidence(confidence: Optional[str]) -> str: if not confidence or not str(confidence).strip(): raise HTTPException(400, "Vertrauensgrad (confidence) ist erforderlich.") c = str(confidence).strip().lower() if c not in REF_VALUE_CONFIDENCE: raise HTTPException(400, f"Ungültiger Vertrauensgrad: {c}") return c def resolve_unit_from_type(default_unit: Optional[str]) -> str: u = (default_unit or "").strip() if not u: raise HTTPException( 400, "Für diesen Kennwert-Typ ist keine Einheit hinterlegt. Bitte im Admin einen Standard unter „Standard-Einheit“ setzen.", ) return u def validate_value_for_data_type( value_data_type: str, validation_rules_raw: Any, value_numeric: Optional[float], value_text: Optional[str], ) -> tuple[Optional[float], Optional[str]]: """ Je nach value_data_type den Wert prüfen und (value_numeric, value_text) für die DB liefern. """ vdt = (value_data_type or "decimal").strip().lower() if vdt not in VALUE_DATA_TYPES: raise HTTPException(400, f"Ungültiger interner Datentyp: {vdt}") rules = _rules_dict(validation_rules_raw) if vdt in ("integer", "decimal", "percentage"): if value_numeric is None: raise HTTPException(400, "Bitte einen numerischen Wert eingeben.") v = float(value_numeric) if vdt == "integer": if abs(v - round(v)) > 1e-9: raise HTTPException(400, "Der Wert muss eine ganze Zahl sein.") v = float(int(round(v))) pos = bool(rules.get("positive_only")) mn = rules.get("min") mx = rules.get("max") if mn is not None: mn = float(mn) if mx is not None: mx = float(mx) if vdt == "percentage": gmn = float(mn) if mn is not None else 0.0 gmx = float(mx) if mx is not None else 100.0 gmn = max(gmn, 0.0) gmx = min(gmx, 100.0) if gmn > gmx: raise HTTPException(500, "Ungültige Plausibilisierung: min > max (Prozent).") if v < gmn or v > gmx: raise HTTPException( 400, f"Prozentwert muss zwischen {gmn} und {gmx} liegen.", ) if pos and v <= 0: raise HTTPException(400, "Prozentwert muss positiv sein (laut Konfiguration).") else: if pos and v <= 0: raise HTTPException(400, "Der Wert muss positiv sein (laut Konfiguration).") if mn is not None and v < mn: raise HTTPException(400, f"Der Wert muss mindestens {mn} sein.") if mx is not None and v > mx: raise HTTPException(400, f"Der Wert darf höchstens {mx} sein.") return v, None # text / enum s = (value_text or "").strip() if value_text is not None else "" if vdt == "text" and not s and not rules.get("not_empty"): return None, "" if rules.get("not_empty") and not s: raise HTTPException(400, "Der Text darf nicht leer sein.") ml = rules.get("max_length") if ml is not None: try: ml_int = int(ml) except (TypeError, ValueError): ml_int = None if ml_int is not None and len(s) > ml_int: raise HTTPException(400, f"Text zu lang (max. {ml_int} Zeichen).") if vdt == "enum": allowed = rules.get("allowed_values") or [] if not isinstance(allowed, list): allowed = [] allowed_str = [str(x).strip() for x in allowed if str(x).strip()] if not allowed_str: raise HTTPException(500, "ENUM-Typ ohne erlaubte Werte (Admin-Konfiguration).") if s not in allowed_str: raise HTTPException( 400, f"Ungültiger Wert. Erlaubt: {', '.join(allowed_str)}", ) return None, s