152 lines
4.1 KiB
Python
152 lines
4.1 KiB
Python
import json
|
||
import os
|
||
import re
|
||
from dotenv import dotenv_values
|
||
from pathlib import Path
|
||
from typing import Any, Iterable, List, Optional
|
||
|
||
_TRUTHY = frozenset({"1", "true", "t", "yes", "y", "on", "enable", "enabled"})
|
||
_FALSY = frozenset({"0", "false", "f", "no", "n", "off", "disable", "disabled"})
|
||
_LIST_SEP_RE = re.compile(r"[,\s;]+")
|
||
|
||
|
||
def parse_bool(value: Any, default: bool = False) -> bool:
|
||
"""
|
||
Parse an arbitrary value into a boolean using env style semantics.
|
||
|
||
Truthy tokens (case-insensitive, quotes/whitespace ignored):
|
||
1, true, t, yes, y, on, enable, enabled
|
||
Falsy tokens:
|
||
0, false, f, no, n, off, disable, disabled
|
||
|
||
- bool -> returned as is
|
||
- None -> returns `default`
|
||
- int/float -> False if == 0, else True
|
||
- str/other -> matched against tokens above; non-matching -> `default`
|
||
|
||
Args:
|
||
value: Value to interpret.
|
||
default: Value to return if `value` is None or doesn’t match any token.
|
||
|
||
Returns:
|
||
The interpreted boolean.
|
||
"""
|
||
if isinstance(value, bool):
|
||
return value
|
||
if value is None:
|
||
return default
|
||
if isinstance(value, (int, float)):
|
||
return value != 0
|
||
|
||
s = str(value).strip().strip('"').strip("'").lower()
|
||
if not s:
|
||
return default
|
||
if s in _TRUTHY:
|
||
return True
|
||
if s in _FALSY:
|
||
return False
|
||
return default
|
||
|
||
|
||
def get_env_bool(key: str, default: bool = False) -> bool:
|
||
"""
|
||
Read an environment variable and parse it as a boolean using `parse_bool`.
|
||
|
||
Args:
|
||
key: Environment variable name.
|
||
default: Returned when the variable is unset or does not match any token.
|
||
|
||
Returns:
|
||
Parsed boolean value.
|
||
"""
|
||
return parse_bool(os.getenv(key), default)
|
||
|
||
|
||
def bool_to_env_str(value: bool) -> str:
|
||
"""
|
||
Canonicalize a boolean to the env/dotenv string form: "1" or "0".
|
||
|
||
Args:
|
||
value: Boolean to serialize.
|
||
|
||
Returns:
|
||
"1" if True, "0" if False.
|
||
"""
|
||
return "1" if bool(value) else "0"
|
||
|
||
|
||
def set_env_bool(key: str, value: Optional[bool] = False) -> None:
|
||
"""
|
||
Set an environment variable to a canonical boolean string ("1" or "0").
|
||
|
||
Args:
|
||
key: The environment variable name to set.
|
||
value: The boolean value to store. If None, it is treated as False.
|
||
True -> "1", False/None -> "0".
|
||
|
||
Notes:
|
||
- This function always overwrites the variable in `os.environ`.
|
||
- Use `get_env_bool` to read back and parse the value safely.
|
||
"""
|
||
os.environ[key] = bool_to_env_str(bool(value))
|
||
|
||
|
||
def coerce_to_list(
|
||
v,
|
||
*,
|
||
lower: bool = False,
|
||
allow_json: bool = True,
|
||
sep_re: re.Pattern = _LIST_SEP_RE,
|
||
) -> Optional[List[str]]:
|
||
"""
|
||
Coerce None / str / list / tuple / set into a clean List[str].
|
||
- Accepts JSON arrays ("[...]"") or delimited strings (comma/space/semicolon).
|
||
- Strips whitespace, drops empties, optionally lowercases.
|
||
"""
|
||
if v is None:
|
||
return None
|
||
if isinstance(v, (list, tuple, set)):
|
||
items = list(v)
|
||
else:
|
||
s = str(v).strip()
|
||
if not s:
|
||
return None
|
||
if allow_json and s.startswith("[") and s.endswith("]"):
|
||
try:
|
||
parsed = json.loads(s)
|
||
items = parsed if isinstance(parsed, list) else [s]
|
||
except Exception:
|
||
items = sep_re.split(s)
|
||
else:
|
||
items = sep_re.split(s)
|
||
|
||
out: List[str] = []
|
||
for item in items:
|
||
s = str(item).strip()
|
||
if not s:
|
||
continue
|
||
out.append(s.lower() if lower else s)
|
||
return out or None
|
||
|
||
|
||
def dedupe_preserve_order(items: Iterable[str]) -> List[str]:
|
||
seen = set()
|
||
out: List[str] = []
|
||
for x in items:
|
||
if x not in seen:
|
||
seen.add(x)
|
||
out.append(x)
|
||
return out
|
||
|
||
|
||
def constrain_between(value: float, lo: float, hi: float) -> float:
|
||
"""Return value constrained to the inclusive range [lo, hi]."""
|
||
return min(max(value, lo), hi)
|
||
|
||
|
||
def read_dotenv_file(path: Path) -> dict[str, str]:
|
||
if not path.exists():
|
||
return {}
|
||
values = dotenv_values(path)
|
||
return {key: value for key, value in values.items() if value is not None}
|