From 521633bde9f96b0f645424cf68882b86f5f73709 Mon Sep 17 00:00:00 2001 From: claude-dev Date: Wed, 13 May 2026 20:46:10 +0000 Subject: [PATCH] feat(shared): org_settings Helper (Kopie aus Monitor) Helper aus AegisSight-Monitor/src/services/org_settings.py uebernommen. Wird in Phase 7 vom Verwaltungs-Org-Router verwendet, um output_language beim Org-Anlegen/Bearbeiten zu setzen. Phase 1 von 8 (eng_demo / Org-Sprache). --- src/shared/services/org_settings.py | 104 ++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/shared/services/org_settings.py diff --git a/src/shared/services/org_settings.py b/src/shared/services/org_settings.py new file mode 100644 index 0000000..d152b5d --- /dev/null +++ b/src/shared/services/org_settings.py @@ -0,0 +1,104 @@ +"""Organization-Settings-Helper. + +KV-Store pro Organisation. Aktuell genutzt fuer output_language ('de'|'en'). +Spaeter erweiterbar (Default-Modell, Telegram-Toggle, Theme, ...). + +Cache: TTL 60s in-memory pro (tenant_id, key). Wird bei set_org_setting() +invalidiert. +""" +import logging +import time +from typing import Optional + +import aiosqlite + +logger = logging.getLogger("osint.org_settings") + +_CACHE: dict[tuple[int, str], tuple[float, Optional[str]]] = {} +_TTL_SECONDS = 60.0 + + +def _cache_get(tenant_id: int, key: str) -> tuple[bool, Optional[str]]: + """(hit, value). hit=True heisst Cache traf; value kann auch None sein.""" + entry = _CACHE.get((tenant_id, key)) + if entry is None: + return (False, None) + expires_at, value = entry + if time.monotonic() > expires_at: + _CACHE.pop((tenant_id, key), None) + return (False, None) + return (True, value) + + +def _cache_put(tenant_id: int, key: str, value: Optional[str]) -> None: + _CACHE[(tenant_id, key)] = (time.monotonic() + _TTL_SECONDS, value) + + +def _cache_invalidate(tenant_id: int, key: str) -> None: + _CACHE.pop((tenant_id, key), None) + + +async def get_org_setting( + db: aiosqlite.Connection, + tenant_id: int, + key: str, + default: Optional[str] = None, +) -> Optional[str]: + """Liest ein Org-Setting. Fallback auf default.""" + if tenant_id is None: + return default + hit, cached = _cache_get(tenant_id, key) + if hit: + return cached if cached is not None else default + cursor = await db.execute( + "SELECT value FROM organization_settings WHERE organization_id = ? AND key = ?", + (tenant_id, key), + ) + row = await cursor.fetchone() + value = row["value"] if row else None + _cache_put(tenant_id, key, value) + return value if value is not None else default + + +async def set_org_setting( + db: aiosqlite.Connection, + tenant_id: int, + key: str, + value: str, +) -> None: + """Setzt ein Org-Setting (upsert).""" + await db.execute( + """INSERT INTO organization_settings (organization_id, key, value, updated_at) + VALUES (?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(organization_id, key) DO UPDATE SET + value = excluded.value, + updated_at = CURRENT_TIMESTAMP""", + (tenant_id, key, value), + ) + await db.commit() + _cache_invalidate(tenant_id, key) + logger.info("Org %s Setting %s='%s' gespeichert", tenant_id, key, value) + + +# Bekannte Sprachen + Anzeigenamen fuer Prompts +LANGUAGE_DISPLAY_NAMES = { + "de": "Deutsch", + "en": "English", +} + + +async def get_org_language( + db: aiosqlite.Connection, + tenant_id: int, +) -> str: + """Liefert ISO-2-Sprachcode der Org (default 'de').""" + value = await get_org_setting(db, tenant_id, "output_language", default="de") + if value not in LANGUAGE_DISPLAY_NAMES: + logger.warning("Unbekannte output_language '%s' fuer Org %s -- fallback 'de'", value, tenant_id) + return "de" + return value + + +def language_display(lang_iso: str) -> str: + """ISO-Code -> Anzeigename fuer Prompts ('de' -> 'Deutsch').""" + return LANGUAGE_DISPLAY_NAMES.get(lang_iso, lang_iso)