JacobLinCool's picture
deploy: sync GitHub main de5dbf9
13fe947 verified
"""Central configuration accessors.
Every accessor reads ``os.environ`` (or an explicit mapping) **live** on each call,
so lazily-built runtimes and tests that monkeypatch the environment always observe
the current value — there is no import-time snapshot. Stdlib-only, so this module is
safe to import from any runtime, embedding subprocess, or Modal container.
"""
from __future__ import annotations
import os
from collections.abc import Mapping
TRUE_VALUES = {"1", "true", "yes", "on"}
FALSE_VALUES = {"0", "false", "no", "off"}
class ConfigError(RuntimeError, ValueError):
"""Invalid configuration value.
Subclasses both ``RuntimeError`` and ``ValueError`` so existing
``except RuntimeError`` handlers and ``pytest.raises`` checks keep working
regardless of which base they expect.
"""
def _source(env: Mapping[str, str] | None) -> Mapping[str, str]:
return os.environ if env is None else env
def str_env(name: str, default: str = "", *, env: Mapping[str, str] | None = None) -> str:
"""Raw environment string, or ``default`` when unset."""
return _source(env).get(name, default)
def bool_env(name: str, default: bool = False, *, env: Mapping[str, str] | None = None) -> bool:
"""Boolean flag. Empty or unrecognised values fall back to ``default``."""
raw = _source(env).get(name, "").strip().lower()
if not raw:
return default
if raw in TRUE_VALUES:
return True
if raw in FALSE_VALUES:
return False
return default
def tri_state_env(name: str, *, env: Mapping[str, str] | None = None) -> bool | None:
"""``True``/``False`` for recognised boolean strings, ``None`` when unset/unrecognised."""
raw = _source(env).get(name, "").strip().lower()
if raw in TRUE_VALUES:
return True
if raw in FALSE_VALUES:
return False
return None
def int_env(
name: str,
default: int,
*,
minimum: int | None = None,
maximum: int | None = None,
env: Mapping[str, str] | None = None,
) -> int:
"""Integer with optional bounds. Empty falls back to ``default``; out-of-range raises ConfigError."""
raw = _source(env).get(name, "").strip()
if not raw:
return default
value = int(raw)
if minimum is not None and value < minimum:
raise ConfigError(f"{name} {_below_message(minimum)}")
if maximum is not None and value > maximum:
raise ConfigError(f"{name} must be at most {maximum}.")
return value
def optional_int_env(
name: str,
*,
minimum: int = 1,
env: Mapping[str, str] | None = None,
) -> int | None:
"""Integer or ``None`` when unset. Values below ``minimum`` raise ConfigError."""
raw = _source(env).get(name, "").strip()
if not raw:
return None
value = int(raw)
if value < minimum:
raise ConfigError(f"{name} {_below_message(minimum)}")
return value
def first_nonempty_env(*names: str, default: str = "", env: Mapping[str, str] | None = None) -> str:
"""First non-empty (stripped) value among ``names``, else ``default``."""
source = _source(env)
for name in names:
value = source.get(name, "").strip()
if value:
return value
return default
def _below_message(minimum: int) -> str:
if minimum == 1:
return "must be a positive integer."
if minimum == 0:
return "must be a non-negative integer."
return f"must be at least {minimum}."