112 lines
3.2 KiB
Python
112 lines
3.2 KiB
Python
"""Persistent overrides for application settings.
|
|
|
|
Overrides take priority over `.env` values for keys exposed via the admin
|
|
manifest. Values are stored as JSON-encoded text to preserve typing across
|
|
strings, booleans, integers and floats.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from sqlalchemy import delete, select
|
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from db.models import AppSettingOverride
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _encode(value: Any) -> str:
|
|
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
|
|
|
|
def _decode(raw: Optional[str]) -> Any:
|
|
if raw is None:
|
|
return None
|
|
try:
|
|
return json.loads(raw)
|
|
except (TypeError, ValueError):
|
|
return raw
|
|
|
|
|
|
async def get_all_overrides(session: AsyncSession) -> Dict[str, Any]:
|
|
rows = (await session.execute(select(AppSettingOverride))).scalars().all()
|
|
return {row.key: _decode(row.value) for row in rows}
|
|
|
|
|
|
async def get_override_value(session: AsyncSession, key: str) -> Tuple[bool, Any]:
|
|
row = (
|
|
await session.execute(
|
|
select(AppSettingOverride).where(AppSettingOverride.key == key).limit(1)
|
|
)
|
|
).scalar_one_or_none()
|
|
if row is None:
|
|
return False, None
|
|
return True, _decode(row.value)
|
|
|
|
|
|
async def get_overrides_with_meta(session: AsyncSession) -> List[Dict[str, Any]]:
|
|
rows = (await session.execute(select(AppSettingOverride))).scalars().all()
|
|
items: List[Dict[str, Any]] = []
|
|
for row in rows:
|
|
items.append(
|
|
{
|
|
"key": row.key,
|
|
"value": _decode(row.value),
|
|
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
|
|
"updated_by": row.updated_by,
|
|
}
|
|
)
|
|
return items
|
|
|
|
|
|
async def upsert_override(
|
|
session: AsyncSession,
|
|
*,
|
|
key: str,
|
|
value: Any,
|
|
updated_by: Optional[int],
|
|
) -> None:
|
|
encoded = _encode(value)
|
|
now = datetime.now(timezone.utc)
|
|
stmt = (
|
|
pg_insert(AppSettingOverride)
|
|
.values(key=key, value=encoded, updated_at=now, updated_by=updated_by)
|
|
.on_conflict_do_update(
|
|
index_elements=[AppSettingOverride.key],
|
|
set_={
|
|
"value": encoded,
|
|
"updated_at": now,
|
|
"updated_by": updated_by,
|
|
},
|
|
)
|
|
)
|
|
await session.execute(stmt)
|
|
|
|
|
|
async def delete_override(session: AsyncSession, key: str) -> bool:
|
|
stmt = delete(AppSettingOverride).where(AppSettingOverride.key == key)
|
|
result = await session.execute(stmt)
|
|
return bool(result.rowcount or 0)
|
|
|
|
|
|
async def bulk_apply(
|
|
session: AsyncSession,
|
|
*,
|
|
updates: Dict[str, Tuple[bool, Any]],
|
|
updated_by: Optional[int],
|
|
) -> None:
|
|
"""Apply a batch of changes. Each entry maps key -> (set_flag, value).
|
|
|
|
When set_flag is False the override is deleted (revert to env). Otherwise
|
|
the value is upserted.
|
|
"""
|
|
for key, (set_flag, value) in updates.items():
|
|
if set_flag:
|
|
await upsert_override(session, key=key, value=value, updated_by=updated_by)
|
|
else:
|
|
await delete_override(session, key)
|