mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-09-19 11:19:56 +00:00
* feat: fixture refresh DB-generated values onto returned instances * refactor: replace utils.get_obj_by_attr/get_field_by_attr with registry.obj/field lookups * refactor: log fixture command output instead of print * chore: clean up fixture module
137 lines
3.9 KiB
Python
137 lines
3.9 KiB
Python
"""CLI configuration and dynamic imports."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import sys
|
|
from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
|
|
|
|
import typer
|
|
|
|
from .pyproject import find_pyproject, load_pyproject
|
|
|
|
if TYPE_CHECKING:
|
|
from ..fixtures import FixtureRegistry
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def _ensure_project_in_path():
|
|
"""Add project root to sys.path if not installed in editable mode."""
|
|
pyproject = find_pyproject()
|
|
if pyproject:
|
|
project_root = str(pyproject.parent)
|
|
if project_root not in sys.path:
|
|
sys.path.insert(0, project_root)
|
|
|
|
|
|
def import_from_string(import_path: str) -> Any:
|
|
"""Import an object from a dotted string path.
|
|
|
|
Args:
|
|
import_path: Import path in ``"module.submodule:attribute"`` format
|
|
|
|
Returns:
|
|
The imported attribute
|
|
|
|
Raises:
|
|
typer.BadParameter: If the import path is invalid or import fails
|
|
"""
|
|
if ":" not in import_path:
|
|
raise typer.BadParameter(
|
|
f"Invalid import path '{import_path}'. Expected format: 'module:attribute'"
|
|
)
|
|
|
|
module_path, attr_name = import_path.rsplit(":", 1)
|
|
|
|
_ensure_project_in_path()
|
|
|
|
try:
|
|
module = importlib.import_module(module_path)
|
|
except ImportError as e:
|
|
raise typer.BadParameter(f"Cannot import module '{module_path}': {e}")
|
|
|
|
if not hasattr(module, attr_name):
|
|
raise typer.BadParameter(
|
|
f"Module '{module_path}' has no attribute '{attr_name}'"
|
|
)
|
|
|
|
return getattr(module, attr_name)
|
|
|
|
|
|
@overload
|
|
def get_config_value(key: str, required: Literal[True]) -> Any: ... # pragma: no cover
|
|
@overload
|
|
def get_config_value(
|
|
key: str, required: bool = False
|
|
) -> Any | None: ... # pragma: no cover
|
|
def get_config_value(key: str, required: bool = False) -> Any | None:
|
|
"""Get a configuration value from pyproject.toml.
|
|
|
|
Args:
|
|
key: The configuration key in [tool.fastapi-toolsets].
|
|
required: If True, raises an error when the key is missing.
|
|
|
|
Returns:
|
|
The configuration value, or None if not found and not required.
|
|
|
|
Raises:
|
|
typer.BadParameter: If required=True and the key is missing.
|
|
"""
|
|
config = load_pyproject()
|
|
value = config.get(key)
|
|
|
|
if required and value is None:
|
|
raise typer.BadParameter(
|
|
f"No '{key}' configured. "
|
|
f"Add '{key}' to [tool.fastapi-toolsets] in pyproject.toml."
|
|
)
|
|
|
|
return value
|
|
|
|
|
|
@overload
|
|
def _import_typed(
|
|
key: str, expected_type: type[T], *, required: Literal[True]
|
|
) -> T: ... # pragma: no cover
|
|
@overload
|
|
def _import_typed(
|
|
key: str, expected_type: type[T], *, required: bool
|
|
) -> T | None: ... # pragma: no cover
|
|
def _import_typed(key: str, expected_type: type[T], *, required: bool) -> T | None:
|
|
"""Import a config value by key and validate its type.
|
|
|
|
Raises:
|
|
typer.BadParameter: If required and missing, or if the imported
|
|
value isn't an instance of *expected_type*.
|
|
"""
|
|
import_path = get_config_value(key, required=required)
|
|
if not import_path:
|
|
return None
|
|
|
|
obj = import_from_string(import_path)
|
|
if not isinstance(obj, expected_type):
|
|
raise typer.BadParameter(
|
|
f"'{key}' must be a {expected_type.__name__} instance, got {type(obj).__name__}"
|
|
)
|
|
|
|
return obj
|
|
|
|
|
|
def get_fixtures_registry() -> FixtureRegistry:
|
|
"""Import and return the fixtures registry from config."""
|
|
from ..fixtures import FixtureRegistry
|
|
|
|
return _import_typed("fixtures", FixtureRegistry, required=True)
|
|
|
|
|
|
def get_db_context() -> Any:
|
|
"""Import and return the db_context function from config."""
|
|
import_path = get_config_value("db_context", required=True)
|
|
return import_from_string(import_path)
|
|
|
|
|
|
def get_custom_cli() -> typer.Typer | None:
|
|
"""Import and return the custom CLI Typer instance from config."""
|
|
return _import_typed("custom_cli", typer.Typer, required=False)
|