feat(settings): organization_settings KV-Tabelle + org_settings Helper
Neue Tabelle organization_settings (organization_id, key, value) als KV-Store fuer Org-spezifische Konfiguration. Erster Use-Case: output_language (de|en). Bestandsorgs werden per Migration auf de gesetzt. Helper services/org_settings.py mit get_org_setting / set_org_setting / get_org_language / language_display. In-Memory-Cache TTL 60s. Phase 1 von 8 (eng_demo / Org-Sprache).
Dieser Commit ist enthalten in:
@@ -345,6 +345,15 @@ CREATE TABLE IF NOT EXISTS network_generation_log (
|
||||
error_message TEXT,
|
||||
tenant_id INTEGER REFERENCES organizations(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS organization_settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(organization_id, key)
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
@@ -782,6 +791,32 @@ async def init_db():
|
||||
await db.commit()
|
||||
logger.info("Migration: token_usage_monthly Tabelle erstellt")
|
||||
|
||||
# Migration: organization_settings KV-Tabelle (pro Org Sprache, ggf. spaeter weitere Settings)
|
||||
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='organization_settings'")
|
||||
if not await cursor.fetchone():
|
||||
await db.execute("""
|
||||
CREATE TABLE organization_settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(organization_id, key)
|
||||
)
|
||||
""")
|
||||
await db.commit()
|
||||
logger.info("Migration: organization_settings Tabelle erstellt")
|
||||
|
||||
# Default-Setting output_language='de' fuer Orgs ohne Eintrag
|
||||
await db.execute("""
|
||||
INSERT OR IGNORE INTO organization_settings (organization_id, key, value)
|
||||
SELECT id, 'output_language', 'de' FROM organizations
|
||||
WHERE id NOT IN (
|
||||
SELECT organization_id FROM organization_settings WHERE key='output_language'
|
||||
)
|
||||
""")
|
||||
await db.commit()
|
||||
|
||||
# Verwaiste running-Eintraege beim Start als error markieren (aelter als 15 Min)
|
||||
await db.execute(
|
||||
"""UPDATE refresh_log SET status = 'error', error_message = 'Verwaist beim Neustart',
|
||||
|
||||
104
src/services/org_settings.py
Normale Datei
104
src/services/org_settings.py
Normale Datei
@@ -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)
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren