Ctrl K
ComunidadGuías…

backend/config_singleton.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Process-wide singleton accessor for Settings.

Other modules call ``get_settings()`` to retrieve the active Settings
instance and ``reload_settings(overrides=...)`` to swap it (e.g. after
the user saves config_entries via the UI).

DB ``config_entries`` overrides are applied **only** to ``UISettings``
fields. Boot fields (``BootSettings``) are read once from the environment
at process start and stay fixed for the lifetime of the process —
restart Sublarr to pick up new boot env values.

Importing rule: this module imports ``Settings``/``BootSettings``/
``UISettings`` from ``config_settings`` at top level — never the other
way round.
"""

import threading

from config_settings import BootSettings, Settings, UISettings

_settings: Settings | None = None
_settings_lock = threading.Lock()

# Boot fields that, although env-loadable and read at process start, are safe
# to reconfigure at runtime and are therefore ALSO overridable from the DB
# ``config_entries`` (set via the UI). Logging is fully re-applied live by
# ``_setup_logging`` on save, so a UI change to these takes effect without a
# restart and survives one (DB override wins over ENV once the user sets it).
# Everything else in ``_ALLOWED_BOOT_FIELDS`` (port, paths, database_url, …)
# stays ENV-only.
_RUNTIME_OVERRIDABLE_BOOT_FIELDS: frozenset[str] = frozenset(
    {"log_level", "log_file", "log_format"}
)


def get_settings() -> Settings:
    """Get or create the singleton Settings instance (thread-safe)."""
    global _settings
    if _settings is not None:
        return _settings
    with _settings_lock:
        if _settings is not None:
            return _settings
        _settings = Settings()
        return _settings


def peek_settings() -> Settings | None:
    """The active settings if they exist — never builds them.

    ``get_settings()`` builds the singleton on first use by merging the
    database overrides onto the defaults, which needs a Flask application
    context. A caller that runs outside one — WSGI middleware, in particular —
    would otherwise cache a Settings object with every stored setting missing,
    and every later reader would get that object.

    Callers in that position ask here instead and treat ``None`` as "too early,
    use the default". The app builds the singleton during startup, so in
    practice this returns it from the first request onwards.
    """
    return _settings


def reload_settings(overrides: dict | None = None) -> Settings:
    """Force reload settings from environment/file, with optional DB overrides.

    Boot fields are re-read from ENV/.env. UI fields start from defaults and
    then have ``overrides`` (from DB ``config_entries``) overlaid on top.

    Args:
        overrides: Dict of UI field name → value (string-form coming from
                   ``config_entries.value``). Boot-field keys in the dict
                   are ignored — boot is ENV-only by design.
    """
    global _settings
    boot = BootSettings()
    ui = UISettings()

    if overrides:
        # DB overrides for the UI fields and the runtime-reconfigurable boot
        # fields (logging), decoded by type and validated (#61 A04: model_copy
        # skipped validators, and lists arrived as their string form).
        from config_values import apply_overrides

        boot_override = {
            key: value
            for key, value in overrides.items()
            if key in _RUNTIME_OVERRIDABLE_BOOT_FIELDS and value not in (None, "")
        }
        if boot_override:
            boot = apply_overrides(boot, boot_override)
        ui = apply_overrides(ui, overrides)

    new_settings = Settings(boot=boot, ui=ui)

    with _settings_lock:
        _settings = new_settings
        return _settings


__all__ = ["get_settings", "reload_settings"]