Files
2026-07-24 21:37:03 +02:00

174 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Zentrale Einstellungen der Suite.
Alle Tools brauchen dieselben Zugangsdaten (Tandoor, OpenAI). Statt sie in
jedem Plugin einzeln zu pflegen, liegen sie einmal in data/settings.json und
werden beim Laden in os.environ gespiegelt. Damit funktionieren die
Original-Tools unverändert weiter: sie lesen wie bisher ihre Umgebungsvariablen.
Vorrang:
1. data/settings.json (über die Oberfläche gepflegt)
2. Umgebung / .env (Fallback, wenn in settings.json leer)
"""
from __future__ import annotations
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
MASK = "••••••••"
@dataclass(frozen=True)
class Field:
key: str
label: str
group: str
kind: str = "text" # text | secret | bool | number | select
default: str = ""
help: str = ""
options: tuple[str, ...] = ()
placeholder: str = ""
@property
def secret(self) -> bool:
return self.kind == "secret"
FIELDS: tuple[Field, ...] = (
Field("TANDOOR_URL", "Tandoor-URL", "Tandoor", "text",
placeholder="https://kitchen.example.de",
help="Basis-URL deiner Tandoor-Instanz, ohne /api."),
Field("TANDOOR_TOKEN", "Tandoor-Token", "Tandoor", "secret",
help="Token mit Schreibrechten. Verlässt das lokale Backend nicht."),
Field("TANDOOR_AUTH_SCHEME", "Auth-Schema", "Tandoor", "select",
default="Bearer", options=("Bearer", "Token"),
help="Bei älteren DRF-Tokens „Token“ wählen."),
Field("TANDOOR_VERIFY_TLS", "TLS prüfen", "Tandoor", "bool", default="true",
help="Nur für interne Instanzen mit selbstsigniertem Zertifikat abschalten."),
Field("TANDOOR_TIMEOUT", "Timeout (s)", "Tandoor", "number", default="45"),
Field("OPENAI_API_KEY", "OpenAI API-Key", "OpenAI", "secret",
help="Wird nur für den AI-Webimport verwendet."),
Field("OPENAI_MODEL", "Modell", "OpenAI", "text", default="gpt-5.5",
placeholder="gpt-5.5"),
Field("SOURCE_VERIFY_TLS", "TLS der Quelle prüfen", "Rezeptquellen", "bool", default="true"),
Field("SOURCE_TIMEOUT", "Timeout (s)", "Rezeptquellen", "number", default="20"),
Field("FETCH_MAX_BYTES", "Max. Downloadgröße (Byte)", "Rezeptquellen", "number",
default="3000000"),
Field("ALLOW_PRIVATE_SOURCE_URLS", "Private Quell-IPs erlauben", "Rezeptquellen", "bool",
default="false",
help="Standardmäßig aus: schützt vor SSRF über localhost und private Netze."),
)
BY_KEY = {f.key: f for f in FIELDS}
GROUPS = tuple(dict.fromkeys(f.group for f in FIELDS))
@dataclass
class Settings:
path: Path
values: dict[str, str] = field(default_factory=dict)
# ------------------------------------------------------------- Laden
@classmethod
def load(cls, path: Path) -> "Settings":
values: dict[str, str] = {}
if path.exists():
try:
raw = json.loads(path.read_text(encoding="utf-8"))
if isinstance(raw, dict):
values = {k: str(v) for k, v in raw.items() if k in BY_KEY}
except Exception:
values = {}
instance = cls(path=path, values=values)
instance.apply_env()
return instance
def save(self) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
tmp = self.path.with_suffix(".json.tmp")
tmp.write_text(json.dumps(self.values, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8")
tmp.replace(self.path)
try:
self.path.chmod(0o600)
except Exception:
pass
self.apply_env()
# -------------------------------------------------------------- Werte
def get(self, key: str) -> str:
value = self.values.get(key, "").strip()
if value:
return value
return os.environ.get(key, "").strip() or BY_KEY[key].default
def apply_env(self) -> None:
"""Spiegelt die Einstellungen in die Prozess-Umgebung."""
for f in FIELDS:
value = self.values.get(f.key, "").strip()
if value:
os.environ[f.key] = value
elif f.key not in os.environ and f.default:
os.environ[f.key] = f.default
def tool_env(self) -> dict[str, str]:
"""Umgebung für Subprozess-Tools (CLI-Plugins)."""
env = dict(os.environ)
for f in FIELDS:
value = self.get(f.key)
if value:
env[f.key] = value
return env
# ----------------------------------------------------------- Web-API
def public(self) -> dict[str, Any]:
"""Für die Oberfläche Geheimnisse werden maskiert."""
out: dict[str, Any] = {}
for f in FIELDS:
value = self.get(f.key)
out[f.key] = MASK if (f.secret and value) else ("" if f.secret else value)
return out
def status(self) -> dict[str, bool]:
return {
"tandoor": bool(self.get("TANDOOR_URL") and self.get("TANDOOR_TOKEN")),
"openai": bool(self.get("OPENAI_API_KEY") and self.get("OPENAI_MODEL")),
}
def update(self, payload: dict[str, Any]) -> None:
for key, value in payload.items():
f = BY_KEY.get(key)
if not f:
continue
text = "" if value is None else str(value).strip()
if f.secret and text == MASK:
continue # unverändert lassen
if f.kind == "bool":
text = "true" if str(value).lower() in {"1", "true", "on", "yes"} else "false"
self.values[key] = text
self.save()
def schema() -> list[dict[str, Any]]:
"""Beschreibung der Felder für die Oberfläche."""
return [
{
"group": group,
"fields": [
{
"key": f.key, "label": f.label, "kind": f.kind,
"help": f.help, "options": list(f.options),
"placeholder": f.placeholder, "default": f.default,
}
for f in FIELDS if f.group == group
],
}
for group in GROUPS
]