Source code for fluxlit.config.project
"""Load optional project defaults from ``fluxlit.toml`` or ``pyproject.toml``."""
from __future__ import annotations
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
if sys.version_info >= (3, 11):
import tomllib # pragma: no cover
else:
import tomli as tomllib
[docs]
@dataclass
class ProjectConfig:
"""Structured defaults read from a project file (not environment).
Populated from top-level keys in ``fluxlit.toml`` or from ``[tool.fluxlit]`` in
``pyproject.toml``. Only known keys are stored; others are ignored.
Fields mirror CLI-related settings: ``target`` (e.g. ``app:app``), bind defaults,
``log_level``, and optional ``api_mount_path`` / ``root_path``.
"""
target: str | None = None
gateway_host: str | None = None
gateway_port: int | None = None
log_level: str | None = None
api_mount_path: str | None = None
root_path: str | None = None
_ALLOWED_KEYS = frozenset(
{
"target",
"gateway_host",
"gateway_port",
"log_level",
"api_mount_path",
"root_path",
}
)
def _parse_table(raw: dict[str, Any]) -> ProjectConfig:
kwargs: dict[str, Any] = {}
for key in _ALLOWED_KEYS:
if key not in raw:
continue
val = raw[key]
if key == "gateway_port":
if isinstance(val, bool) or val is None:
continue
try:
kwargs[key] = int(val)
except (TypeError, ValueError):
continue
elif isinstance(val, str):
kwargs[key] = val
return ProjectConfig(**kwargs)
[docs]
def load_project_config(cwd: Path | None = None) -> ProjectConfig | None:
"""Parse ``fluxlit.toml`` or ``[tool.fluxlit]`` in ``pyproject.toml``.
If both files exist, ``fluxlit.toml`` takes precedence. Returns ``None`` if
neither file exists, the relevant section is missing, the file is not valid TOML,
or the top-level value is not a table (dict).
Args:
cwd: Directory to search; defaults to :func:`pathlib.Path.cwd`.
Returns:
Parsed config, or ``None`` when no project file applies.
"""
root = cwd or Path.cwd()
fluxlit_path = root / "fluxlit.toml"
pyproject_path = root / "pyproject.toml"
if fluxlit_path.is_file():
try:
data = tomllib.loads(fluxlit_path.read_text(encoding="utf-8"))
except tomllib.TOMLDecodeError:
return None
if not isinstance(data, dict):
return None
return _parse_table(data)
if pyproject_path.is_file():
try:
data = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
except tomllib.TOMLDecodeError:
return None
tool = data.get("tool")
if not isinstance(tool, dict):
return None
section = tool.get("fluxlit")
if not isinstance(section, dict):
return None
return _parse_table(section)
return None
[docs]
def resolve_target(cli_target: str | None, pc: ProjectConfig | None) -> str:
"""Pick the app import target: CLI argument, then project file, then ``app:app``."""
if cli_target is not None:
return cli_target
if pc and pc.target:
return pc.target
return "app:app"
[docs]
def resolve_binding(
*,
cli_host: str | None,
cli_port: int | None,
cli_log_level: str | None,
pc: ProjectConfig | None,
settings_gateway_host: str,
settings_gateway_port: int,
settings_log_level: str,
) -> tuple[str, int, str]:
"""Resolve host, port, and log level: CLI > project file > ``FluxlitSettings``.
``settings_*`` arguments should come from the loaded :class:`~fluxlit.app.FluxLit`
instance (already merged with environment).
"""
host = cli_host
if host is None and pc and pc.gateway_host is not None:
host = pc.gateway_host
host = host or settings_gateway_host
port = cli_port
if port is None and pc and pc.gateway_port is not None:
port = pc.gateway_port
port = port or settings_gateway_port
log_level = cli_log_level
if log_level is None and pc and pc.log_level is not None:
log_level = pc.log_level
log_level = log_level or settings_log_level
return host, port, log_level
__all__ = [
"ProjectConfig",
"load_project_config",
"resolve_binding",
"resolve_target",
]