"""Site-wide switches an administrator sets once. They live in Redis because they are configuration rather than records — but a site must keep working when Redis does not, so every read falls back to the default rather than raising. The defaults are deliberately the permissive ones for features that already existed and the restrictive one for the gate that protects sign-up: losing Redis should not silently open registration. """ import logging logger = logging.getLogger(__name__) #: name -> default. Anything not listed here cannot be set. FLAGS: dict[str, bool] = { #: Whether anybody new may join at all. Stored under the same key the #: registration route and the admin settings page have always used; it was #: simply not declared here, so the one helper that reads these flags #: refused it and every caller wrote its own Redis lookup instead. "registration_enabled": True, #: Whether a learner may create a public share link for a session. "sharing_enabled": True, #: Whether the only way in is the identity provider. Read here as well as #: in the SSO routes because it governs every email-based way of signing #: in, link included, and each of those had been reading the key itself. "sso_only": False, #: Whether the AI tutor may be opened while a session is being sat. It is #: only ever offered in study mode — the tutor is given the correct answer #: and told it may reveal it, so during an exam it would simply hand it #: over. This switch decides whether even study mode gets it. "tutor_in_quiz": True, #: Whether an AI draft may be grounded in the indexed clinical library. #: Off until somebody points the site at a library and turns it on. "clinical_library_enabled": False, #: Whether an AI draft may search PubMed for published literature to cite. "pubmed_enabled": False, } #: Settings that are text rather than a switch: an address, a key, an email. #: name -> default. Anything not listed here cannot be set, for the same reason #: the flags cannot: a typo should fail loudly rather than write a key nothing #: will ever read. VALUES: dict[str, str] = { #: Where the clinical library answers. Blank means there is not one. "clinical_mcp_url": "", #: NCBI raises the rate limit for a caller who identifies themselves. Both #: optional: E-utilities works without either, more slowly. "pubmed_api_key": "", "pubmed_contact_email": "", } def _client(): import redis as redis_lib from app.config import settings return redis_lib.from_url(settings.REDIS_URL, decode_responses=True) def get_flag(name: str, default: bool | None = None) -> bool: if name not in FLAGS: raise KeyError(name) default = FLAGS[name] if default is None else default try: value = _client().get(f"settings:{name}") except Exception: logger.warning("Redis unavailable reading %s; using the default", name, exc_info=True) return default if value is None: return default return value == "true" def set_flag(name: str, value: bool) -> None: if name not in FLAGS: raise KeyError(name) _client().set(f"settings:{name}", "true" if value else "false") def all_flags() -> dict[str, bool]: return {name: get_flag(name) for name in FLAGS} def get_value(name: str) -> str: if name not in VALUES: raise KeyError(name) default = VALUES[name] try: value = _client().get(f"settings:{name}") except Exception: logger.warning("Redis unavailable reading %s; using the default", name, exc_info=True) return default return default if value is None else value def set_value(name: str, value: str) -> None: if name not in VALUES: raise KeyError(name) _client().set(f"settings:{name}", (value or "").strip()) def all_values() -> dict[str, str]: return {name: get_value(name) for name in VALUES}