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,
|
error_message TEXT,
|
||||||
tenant_id INTEGER REFERENCES organizations(id)
|
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()
|
await db.commit()
|
||||||
logger.info("Migration: token_usage_monthly Tabelle erstellt")
|
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)
|
# Verwaiste running-Eintraege beim Start als error markieren (aelter als 15 Min)
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""UPDATE refresh_log SET status = 'error', error_message = 'Verwaist beim Neustart',
|
"""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