chore: initial import
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
# -*- 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
|
||||
]
|
||||
Reference in New Issue
Block a user