import uuid import json from django.core.cache import cache from .defaults import DEFAULTS, DEFAULTS_HASH from .types import SettingType, coerce_from_storage, coerce_from_spec_default from labs.utils.database_fields import get_default_fernet from labs.models import SiteSetting as DBSiteSetting NAMESPACE_KEY = "site_settings:NS" ITEM_KEY_TMPL = f"site_settings:{DEFAULTS_HASH}:{{ns}}:{{key}}" ITEM_KEY_TTL = 60 * 60 * 24 * 10 # 10 days _MISS = object() def _enc_set(key, value): raw = json.dumps(value).encode("utf-8") value = get_default_fernet().encrypt(raw) cache.set(key, value, ITEM_KEY_TTL) def _enc_get(key): payload = cache.get(key) if payload is None: return _MISS try: raw = get_default_fernet().decrypt(payload) return json.loads(raw.decode("utf-8")) except (ValueError, json.JSONDecodeError): return _MISS def get_namespace(): ns = cache.get(NAMESPACE_KEY) if not ns: ns = uuid.uuid4().hex cache.set(NAMESPACE_KEY, ns, None) return ns def bump_namespace(): cache.set(NAMESPACE_KEY, uuid.uuid4().hex, None) def _fetch_from_db(key): try: obj = DBSiteSetting.objects.only("key_type", "value").get(key=key) t = SettingType(obj.key_type) return coerce_from_storage(t, obj.value) except DBSiteSetting.DoesNotExist: return None def _default_for(key): spec = DEFAULTS.get(key) if not spec: raise AttributeError(f"Unknown setting '{key}' (not in defaults.yaml)") t = SettingType(spec["type"]) return coerce_from_spec_default(t, spec["default"]) def get(key): if key not in DEFAULTS: raise AttributeError(f"Unknown setting '{key}' (not in defaults.yaml)") ns = get_namespace() cache_key = ITEM_KEY_TMPL.format(ns=ns, key=key) hit = _enc_get(cache_key) if hit is not _MISS: return hit value = _fetch_from_db(key) if value is None: value = _default_for(key) _enc_set(cache_key, value) return value class _LazySettings: def __getattr__(self, name: str): return get(name) def __getitem__(self, name: str): return get(name) def refresh(self): bump_namespace() SiteSettings = _LazySettings()