feat: simplify CLI feature (#23)

* chore: cleanup + add tests

* chore: remove graph and show fixtures commands

* feat: add async_command wrapper
This commit is contained in:
d3vyce
2026-02-03 14:35:15 +01:00
committed by GitHub
parent 8c287b3ce7
commit 34ef4da317
7 changed files with 492 additions and 228 deletions

View File

@@ -1,97 +1,25 @@
"""Main CLI application."""
import importlib.util
import sys
from pathlib import Path
from typing import Annotated
import typer
from .commands import fixtures
from .config import load_config
app = typer.Typer(
name="fastapi-utils",
cli = typer.Typer(
name="manager",
help="CLI utilities for FastAPI projects.",
no_args_is_help=True,
)
# Register built-in commands
app.add_typer(fixtures.app, name="fixtures")
_config = load_config()
if _config.fixtures:
from .commands.fixtures import fixture_cli
cli.add_typer(fixture_cli, name="fixtures")
def register_command(command: typer.Typer, name: str) -> None:
"""Register a custom command group.
Args:
command: Typer app for the command group
name: Name for the command group
Example:
# In your project's cli.py:
import typer
from fastapi_toolsets.cli import app, register_command
my_commands = typer.Typer()
@my_commands.command()
def seed():
'''Seed the database.'''
...
register_command(my_commands, "db")
# Now available as: fastapi-utils db seed
"""
app.add_typer(command, name=name)
@app.callback()
def main(
ctx: typer.Context,
config: Annotated[
Path | None,
typer.Option(
"--config",
"-c",
help="Path to project config file (Python module with fixtures registry).",
envvar="FASTAPI_TOOLSETS_CONFIG",
),
] = None,
) -> None:
@cli.callback()
def main(ctx: typer.Context) -> None:
"""FastAPI utilities CLI."""
ctx.ensure_object(dict)
if config:
ctx.obj["config_path"] = config
# Load the config module
config_module = _load_module_from_path(config)
ctx.obj["config_module"] = config_module
def _load_module_from_path(path: Path) -> object:
"""Load a Python module from a file path.
Handles both absolute and relative imports by adding the config's
parent directory to sys.path temporarily.
"""
path = path.resolve()
# Add the parent directory to sys.path to support relative imports
parent_dir = str(
path.parent.parent
) # Go up two levels (e.g., from app/cli_config.py to project root)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
# Also add immediate parent for direct module imports
immediate_parent = str(path.parent)
if immediate_parent not in sys.path:
sys.path.insert(0, immediate_parent)
spec = importlib.util.spec_from_file_location("config", path)
if spec is None or spec.loader is None:
raise typer.BadParameter(f"Cannot load module from {path}")
module = importlib.util.module_from_spec(spec)
sys.modules["config"] = module
spec.loader.exec_module(module)
return module
ctx.obj["config"] = _config