Source code for sqlspec.utils.env

"""Environment variable parsing utilities."""

import json
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any, Final, get_args, get_origin, overload

if TYPE_CHECKING:
    from collections.abc import Callable, Sequence

__all__ = ("get_config_val", "get_config_val_with_aliases", "get_env", "get_env_with_aliases", "is_env_set")

TRUE_VALUES: Final[frozenset[str]] = frozenset({"1", "true", "yes", "y", "on", "t"})
FALSE_VALUES: Final[frozenset[str]] = frozenset({"0", "false", "no", "n", "off", "f"})

ParseType = bool | int | float | str | Path | list[Any] | dict[str, Any] | None


class _UnsetType:
    """Sentinel for an omitted type hint."""

    __slots__ = ()


_UNSET = _UnsetType()


class _EnvFactory:
    """Callable factory for delayed environment parsing."""

    __slots__ = ("_aliases", "_default", "_key", "_type_hint")

    def __init__(self, key: str, aliases: "Sequence[str]", default: ParseType, type_hint: object) -> None:
        self._aliases = aliases
        self._default = default
        self._key = key
        self._type_hint = type_hint

    def __call__(self) -> Any:
        if self._aliases:
            return get_config_val_with_aliases(self._key, self._aliases, self._default, self._type_hint)
        return get_config_val(self._key, self._default, self._type_hint)


@overload
def get_env(key: str, default: bool, type_hint: "_UnsetType" = _UNSET) -> "Callable[[], bool]": ...


@overload
def get_env(key: str, default: int, type_hint: "_UnsetType" = _UNSET) -> "Callable[[], int]": ...


@overload
def get_env(key: str, default: float, type_hint: "_UnsetType" = _UNSET) -> "Callable[[], float]": ...


@overload
def get_env(key: str, default: str, type_hint: "_UnsetType" = _UNSET) -> "Callable[[], str]": ...


@overload
def get_env(key: str, default: Path, type_hint: "_UnsetType" = _UNSET) -> "Callable[[], Path]": ...


@overload
def get_env(key: str, default: list[Any], type_hint: "_UnsetType" = _UNSET) -> "Callable[[], list[Any]]": ...


@overload
def get_env(key: str, default: dict[str, Any], type_hint: "_UnsetType" = _UNSET) -> "Callable[[], dict[str, Any]]": ...


@overload
def get_env(key: str, default: None, type_hint: "_UnsetType" = _UNSET) -> "Callable[[], str | None]": ...


@overload
def get_env(key: str, default: ParseType, type_hint: object) -> "Callable[[], Any]": ...


[docs] def get_env(key: str, default: ParseType, type_hint: object = _UNSET) -> "Callable[[], Any]": """Return a callable that parses an environment variable on demand. Args: key: Environment variable name. default: Value returned when the variable is unset. type_hint: Optional parse target, including generic aliases such as ``list[int]``. Returns: Callable that returns the parsed value. """ return _EnvFactory(key, (), default, type_hint)
@overload def get_env_with_aliases( key: str, aliases: "Sequence[str]", default: bool, type_hint: "_UnsetType" = _UNSET ) -> "Callable[[], bool]": ... @overload def get_env_with_aliases( key: str, aliases: "Sequence[str]", default: int, type_hint: "_UnsetType" = _UNSET ) -> "Callable[[], int]": ... @overload def get_env_with_aliases( key: str, aliases: "Sequence[str]", default: float, type_hint: "_UnsetType" = _UNSET ) -> "Callable[[], float]": ... @overload def get_env_with_aliases( key: str, aliases: "Sequence[str]", default: str, type_hint: "_UnsetType" = _UNSET ) -> "Callable[[], str]": ... @overload def get_env_with_aliases( key: str, aliases: "Sequence[str]", default: Path, type_hint: "_UnsetType" = _UNSET ) -> "Callable[[], Path]": ... @overload def get_env_with_aliases( key: str, aliases: "Sequence[str]", default: list[Any], type_hint: "_UnsetType" = _UNSET ) -> "Callable[[], list[Any]]": ... @overload def get_env_with_aliases( key: str, aliases: "Sequence[str]", default: dict[str, Any], type_hint: "_UnsetType" = _UNSET ) -> "Callable[[], dict[str, Any]]": ... @overload def get_env_with_aliases( key: str, aliases: "Sequence[str]", default: None, type_hint: "_UnsetType" = _UNSET ) -> "Callable[[], str | None]": ... @overload def get_env_with_aliases( key: str, aliases: "Sequence[str]", default: ParseType, type_hint: object ) -> "Callable[[], Any]": ...
[docs] def get_env_with_aliases( key: str, aliases: "Sequence[str]", default: ParseType, type_hint: object = _UNSET ) -> "Callable[[], Any]": """Return a callable that parses a canonical environment key plus aliases. Args: key: Canonical environment variable name. aliases: Fallback variable names checked in order. default: Value returned when none of the variables are set. type_hint: Optional parse target, including generic aliases such as ``list[int]``. Returns: Callable that returns the parsed value. """ return _EnvFactory(key, aliases, default, type_hint)
@overload def get_config_val(key: str, default: bool, type_hint: "_UnsetType" = _UNSET) -> bool: ... @overload def get_config_val(key: str, default: int, type_hint: "_UnsetType" = _UNSET) -> int: ... @overload def get_config_val(key: str, default: float, type_hint: "_UnsetType" = _UNSET) -> float: ... @overload def get_config_val(key: str, default: str, type_hint: "_UnsetType" = _UNSET) -> str: ... @overload def get_config_val(key: str, default: Path, type_hint: "_UnsetType" = _UNSET) -> Path: ... @overload def get_config_val(key: str, default: list[Any], type_hint: "_UnsetType" = _UNSET) -> list[Any]: ... @overload def get_config_val(key: str, default: dict[str, Any], type_hint: "_UnsetType" = _UNSET) -> dict[str, Any]: ... @overload def get_config_val(key: str, default: None, type_hint: "_UnsetType" = _UNSET) -> "str | None": ... @overload def get_config_val(key: str, default: ParseType, type_hint: object) -> Any: ...
[docs] def get_config_val(key: str, default: ParseType, type_hint: object = _UNSET) -> Any: """Parse an environment variable value. Args: key: Environment variable name. default: Value returned when the variable is unset. type_hint: Optional parse target, including generic aliases such as ``list[int]``. Returns: Parsed value or the supplied default. """ if key not in os.environ: return default return _parse_value(key, os.path.expandvars(os.environ[key]), default, type_hint)
@overload def get_config_val_with_aliases( key: str, aliases: "Sequence[str]", default: bool, type_hint: "_UnsetType" = _UNSET ) -> bool: ... @overload def get_config_val_with_aliases( key: str, aliases: "Sequence[str]", default: int, type_hint: "_UnsetType" = _UNSET ) -> int: ... @overload def get_config_val_with_aliases( key: str, aliases: "Sequence[str]", default: float, type_hint: "_UnsetType" = _UNSET ) -> float: ... @overload def get_config_val_with_aliases( key: str, aliases: "Sequence[str]", default: str, type_hint: "_UnsetType" = _UNSET ) -> str: ... @overload def get_config_val_with_aliases( key: str, aliases: "Sequence[str]", default: Path, type_hint: "_UnsetType" = _UNSET ) -> Path: ... @overload def get_config_val_with_aliases( key: str, aliases: "Sequence[str]", default: list[Any], type_hint: "_UnsetType" = _UNSET ) -> list[Any]: ... @overload def get_config_val_with_aliases( key: str, aliases: "Sequence[str]", default: dict[str, Any], type_hint: "_UnsetType" = _UNSET ) -> dict[str, Any]: ... @overload def get_config_val_with_aliases( key: str, aliases: "Sequence[str]", default: None, type_hint: "_UnsetType" = _UNSET ) -> "str | None": ... @overload def get_config_val_with_aliases(key: str, aliases: "Sequence[str]", default: ParseType, type_hint: object) -> Any: ...
[docs] def get_config_val_with_aliases( key: str, aliases: "Sequence[str]", default: ParseType, type_hint: object = _UNSET ) -> Any: """Parse a canonical environment variable, falling back to aliases. Args: key: Canonical environment variable name. aliases: Fallback variable names checked in order. default: Value returned when none of the variables are set. type_hint: Optional parse target, including generic aliases such as ``list[int]``. Returns: Parsed value or the supplied default. """ for env_key in (key, *aliases): if env_key in os.environ: return get_config_val(env_key, default, type_hint) return default
[docs] def is_env_set(key: str, aliases: "Sequence[str]" = ()) -> bool: """Return whether a canonical environment variable or alias is present. Args: key: Canonical environment variable name. aliases: Fallback variable names to check. Returns: ``True`` when any key is present in ``os.environ``. """ return any(env_key in os.environ for env_key in (key, *aliases))
def _coerce_bool(key: str, value: Any) -> bool: if isinstance(value, bool): return value normalized = str(value).strip().lower() if normalized in TRUE_VALUES: return True if normalized in FALSE_VALUES: return False msg = f"Cannot convert value for key '{key}' to bool: {value!r}" raise ValueError(msg) def _coerce_item(key: str, value: Any, item_type: object) -> Any: if item_type is bool: return _coerce_bool(key, value) if item_type is Path: return Path(str(value)) if item_type is int: try: return int(value) except (TypeError, ValueError) as e: msg = f"Cannot convert list item for key '{key}' to int: {value!r}" raise ValueError(msg) from e if item_type is float: try: return float(value) except (TypeError, ValueError) as e: msg = f"Cannot convert list item for key '{key}' to float: {value!r}" raise ValueError(msg) from e if item_type is str or item_type is _UNSET: return str(value) if isinstance(item_type, type): try: return item_type(value) except (TypeError, ValueError) as e: msg = f"Cannot convert list item for key '{key}': {value!r}" raise ValueError(msg) from e return value def _parse_basic_type(key: str, value: str, target_type: object) -> Any: if target_type is bool: return _coerce_bool(key, value) if target_type is int: try: return int(value) except ValueError as e: msg = f"Cannot convert value for key '{key}' to int: {value!r}" raise ValueError(msg) from e if target_type is float: try: return float(value) except ValueError as e: msg = f"Cannot convert value for key '{key}' to float: {value!r}" raise ValueError(msg) from e if target_type is Path: return Path(value) return value def _parse_dict(key: str, value: str) -> dict[str, Any]: stripped = value.strip() if stripped.startswith("{"): try: parsed = json.loads(stripped) except json.JSONDecodeError as e: msg = f"'{key}' is not valid JSON" raise ValueError(msg) from e if not isinstance(parsed, dict): msg = f"'{key}' is not a valid dict representation" raise ValueError(msg) return parsed result: dict[str, Any] = {} for item in value.split(","): stripped_item = item.strip() if not stripped_item: continue if "=" not in stripped_item: msg = f"'{key}' invalid dict format: missing '=' in {stripped_item!r}" raise ValueError(msg) item_key, item_value = stripped_item.split("=", 1) result[item_key.strip()] = item_value.strip() return result def _parse_list(key: str, value: str, item_type: object) -> list[Any]: stripped = value.strip() if stripped.startswith("["): try: parsed = json.loads(stripped) except json.JSONDecodeError as e: msg = f"'{key}' is not valid JSON" raise ValueError(msg) from e if not isinstance(parsed, list): msg = f"'{key}' is not a valid list representation" raise ValueError(msg) return [_coerce_item(key, item, item_type) for item in parsed] if stripped.startswith("{"): msg = f"'{key}' is not a valid list representation" raise ValueError(msg) return [_coerce_item(key, item.strip(), item_type) for item in value.split(",") if item.strip()] def _resolve_target(default: Any, type_hint: object) -> tuple[object, object]: if type_hint is not _UNSET: origin = get_origin(type_hint) if origin is list: args = get_args(type_hint) item_type = args[0] if args else str return list, item_type if origin is dict: return dict, _UNSET return type_hint, _UNSET if isinstance(default, list): item_type = type(default[0]) if default else str return list, item_type if isinstance(default, dict): return dict, _UNSET if isinstance(default, Path): return Path, _UNSET if default is None: return str, _UNSET return type(default), _UNSET def _parse_value(key: str, value: str, default: ParseType, type_hint: object) -> Any: target_type, item_type = _resolve_target(default, type_hint) if target_type is list: return _parse_list(key, value, item_type) if target_type is dict: return _parse_dict(key, value) return _parse_basic_type(key, value, target_type)