fastapi-toolsets 0.5.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/PKG-INFO +2 -2
  2. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/README.md +1 -1
  3. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/pyproject.toml +1 -1
  4. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/__init__.py +1 -1
  5. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/cli/__init__.py +1 -2
  6. fastapi_toolsets-0.6.0/src/fastapi_toolsets/cli/app.py +30 -0
  7. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/cli/commands/fixtures.py +16 -35
  8. fastapi_toolsets-0.6.0/src/fastapi_toolsets/cli/config.py +107 -0
  9. fastapi_toolsets-0.6.0/src/fastapi_toolsets/cli/pyproject.py +43 -0
  10. fastapi_toolsets-0.6.0/src/fastapi_toolsets/dependencies/__init__.py +5 -0
  11. fastapi_toolsets-0.6.0/src/fastapi_toolsets/dependencies/factory.py +139 -0
  12. fastapi_toolsets-0.5.0/src/fastapi_toolsets/cli/app.py +0 -25
  13. fastapi_toolsets-0.5.0/src/fastapi_toolsets/cli/config.py +0 -92
  14. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/LICENSE +0 -0
  15. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/cli/commands/__init__.py +0 -0
  16. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/cli/utils.py +0 -0
  17. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/crud/__init__.py +0 -0
  18. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/crud/factory.py +0 -0
  19. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/crud/search.py +0 -0
  20. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/db.py +0 -0
  21. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/exceptions/__init__.py +0 -0
  22. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/exceptions/exceptions.py +0 -0
  23. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/exceptions/handler.py +0 -0
  24. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/fixtures/__init__.py +0 -0
  25. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/fixtures/enum.py +0 -0
  26. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/fixtures/registry.py +0 -0
  27. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/fixtures/utils.py +0 -0
  28. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/py.typed +0 -0
  29. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/pytest/__init__.py +0 -0
  30. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/pytest/plugin.py +0 -0
  31. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/pytest/utils.py +0 -0
  32. {fastapi_toolsets-0.5.0 → fastapi_toolsets-0.6.0}/src/fastapi_toolsets/schemas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-toolsets
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Reusable tools for FastAPI: async CRUD, fixtures, CLI, and standardized responses for SQLAlchemy + PostgreSQL
5
5
  Keywords: fastapi,sqlalchemy,postgresql
6
6
  Author: d3vyce
@@ -77,7 +77,7 @@ uv add fastapi-toolsets
77
77
 
78
78
  - **CRUD**: Generic async CRUD operations with `CrudFactory`
79
79
  - **Fixtures**: Fixture system with dependency management, context support and pytest integration
80
- - **CLI**: Django-like command-line interface for fixtures and custom commands
80
+ - **CLI**: Django-like command-line interface with fixture management and custom commands support
81
81
  - **Standardized API Responses**: Consistent response format across your API
82
82
  - **Exception Handling**: Structured error responses with automatic OpenAPI documentation
83
83
 
@@ -28,7 +28,7 @@ uv add fastapi-toolsets
28
28
 
29
29
  - **CRUD**: Generic async CRUD operations with `CrudFactory`
30
30
  - **Fixtures**: Fixture system with dependency management, context support and pytest integration
31
- - **CLI**: Django-like command-line interface for fixtures and custom commands
31
+ - **CLI**: Django-like command-line interface with fixture management and custom commands support
32
32
  - **Standardized API Responses**: Consistent response format across your API
33
33
  - **Exception Handling**: Structured error responses with automatic OpenAPI documentation
34
34
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastapi-toolsets"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "Reusable tools for FastAPI: async CRUD, fixtures, CLI, and standardized responses for SQLAlchemy + PostgreSQL"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -21,4 +21,4 @@ Example usage:
21
21
  return Response(data={"user": user.username}, message="Success")
22
22
  """
23
23
 
24
- __version__ = "0.5.0"
24
+ __version__ = "0.6.0"
@@ -1,6 +1,5 @@
1
1
  """CLI for FastAPI projects."""
2
2
 
3
- from .app import cli
4
3
  from .utils import async_command
5
4
 
6
- __all__ = ["async_command", "cli"]
5
+ __all__ = ["async_command"]
@@ -0,0 +1,30 @@
1
+ """Main CLI application."""
2
+
3
+ import typer
4
+
5
+ from .config import get_custom_cli
6
+ from .pyproject import load_pyproject
7
+
8
+ # Use custom CLI if configured, otherwise create default one
9
+ _custom_cli = get_custom_cli()
10
+
11
+ if _custom_cli is not None:
12
+ cli = _custom_cli
13
+ else:
14
+ cli = typer.Typer(
15
+ name="manager",
16
+ help="CLI utilities for FastAPI projects.",
17
+ no_args_is_help=True,
18
+ )
19
+
20
+ _config = load_pyproject()
21
+ if _config.get("fixtures") and _config.get("db_context"):
22
+ from .commands.fixtures import fixture_cli
23
+
24
+ cli.add_typer(fixture_cli, name="fixtures")
25
+
26
+
27
+ @cli.callback()
28
+ def main(ctx: typer.Context) -> None:
29
+ """FastAPI utilities CLI."""
30
+ ctx.ensure_object(dict)
@@ -7,7 +7,7 @@ from rich.console import Console
7
7
  from rich.table import Table
8
8
 
9
9
  from ...fixtures import Context, LoadStrategy, load_fixtures_by_context
10
- from ..config import CliConfig
10
+ from ..config import get_db_context, get_fixtures_registry
11
11
  from ..utils import async_command
12
12
 
13
13
  fixture_cli = typer.Typer(
@@ -18,27 +18,21 @@ fixture_cli = typer.Typer(
18
18
  console = Console()
19
19
 
20
20
 
21
- def _get_config(ctx: typer.Context) -> CliConfig:
22
- """Get CLI config from context."""
23
- return ctx.obj["config"]
24
-
25
-
26
21
  @fixture_cli.command("list")
27
22
  def list_fixtures(
28
23
  ctx: typer.Context,
29
24
  context: Annotated[
30
- str | None,
25
+ Context | None,
31
26
  typer.Option(
32
27
  "--context",
33
28
  "-c",
34
- help="Filter by context (base, production, development, testing).",
29
+ help="Filter by context.",
35
30
  ),
36
31
  ] = None,
37
32
  ) -> None:
38
33
  """List all registered fixtures."""
39
- config = _get_config(ctx)
40
- registry = config.get_fixtures_registry()
41
- fixtures = registry.get_by_context(context) if context else registry.get_all()
34
+ registry = get_fixtures_registry()
35
+ fixtures = registry.get_by_context(context.value) if context else registry.get_all()
42
36
 
43
37
  if not fixtures:
44
38
  print("No fixtures found.")
@@ -60,17 +54,13 @@ def list_fixtures(
60
54
  async def load(
61
55
  ctx: typer.Context,
62
56
  contexts: Annotated[
63
- list[str] | None,
64
- typer.Argument(
65
- help="Contexts to load (base, production, development, testing)."
66
- ),
57
+ list[Context] | None,
58
+ typer.Argument(help="Contexts to load."),
67
59
  ] = None,
68
60
  strategy: Annotated[
69
- str,
70
- typer.Option(
71
- "--strategy", "-s", help="Load strategy: merge, insert, skip_existing."
72
- ),
73
- ] = "merge",
61
+ LoadStrategy,
62
+ typer.Option("--strategy", "-s", help="Load strategy."),
63
+ ] = LoadStrategy.MERGE,
74
64
  dry_run: Annotated[
75
65
  bool,
76
66
  typer.Option(
@@ -79,19 +69,10 @@ async def load(
79
69
  ] = False,
80
70
  ) -> None:
81
71
  """Load fixtures into the database."""
82
- config = _get_config(ctx)
83
- registry = config.get_fixtures_registry()
84
- get_db_context = config.get_db_context()
85
-
86
- context_list = contexts if contexts else [Context.BASE]
72
+ registry = get_fixtures_registry()
73
+ db_context = get_db_context()
87
74
 
88
- try:
89
- load_strategy = LoadStrategy(strategy)
90
- except ValueError:
91
- typer.echo(
92
- f"Invalid strategy: {strategy}. Use: merge, insert, skip_existing", err=True
93
- )
94
- raise typer.Exit(1)
75
+ context_list = [c.value for c in contexts] if contexts else [Context.BASE]
95
76
 
96
77
  ordered = registry.resolve_context_dependencies(*context_list)
97
78
 
@@ -99,7 +80,7 @@ async def load(
99
80
  print("No fixtures to load for the specified context(s).")
100
81
  return
101
82
 
102
- print(f"\nFixtures to load ({load_strategy.value} strategy):")
83
+ print(f"\nFixtures to load ({strategy.value} strategy):")
103
84
  for name in ordered:
104
85
  fixture = registry.get(name)
105
86
  instances = list(fixture.func())
@@ -110,9 +91,9 @@ async def load(
110
91
  print("\n[Dry run - no changes made]")
111
92
  return
112
93
 
113
- async with get_db_context() as session:
94
+ async with db_context() as session:
114
95
  result = await load_fixtures_by_context(
115
- session, registry, *context_list, strategy=load_strategy
96
+ session, registry, *context_list, strategy=strategy
116
97
  )
117
98
 
118
99
  total = sum(len(items) for items in result.values())
@@ -0,0 +1,107 @@
1
+ """CLI configuration and dynamic imports."""
2
+
3
+ import importlib
4
+ import sys
5
+
6
+ import typer
7
+
8
+ from .pyproject import find_pyproject, load_pyproject
9
+
10
+
11
+ def _ensure_project_in_path():
12
+ """Add project root to sys.path if not installed in editable mode."""
13
+ pyproject = find_pyproject()
14
+ if pyproject:
15
+ project_root = str(pyproject.parent)
16
+ if project_root not in sys.path:
17
+ sys.path.insert(0, project_root)
18
+
19
+
20
+ def import_from_string(import_path: str):
21
+ """Import an object from a string path like 'module.submodule:attribute'.
22
+
23
+ Raises:
24
+ typer.BadParameter: If the import path is invalid or import fails.
25
+ """
26
+ if ":" not in import_path:
27
+ raise typer.BadParameter(
28
+ f"Invalid import path '{import_path}'. Expected format: 'module:attribute'"
29
+ )
30
+
31
+ module_path, attr_name = import_path.rsplit(":", 1)
32
+
33
+ _ensure_project_in_path()
34
+
35
+ try:
36
+ module = importlib.import_module(module_path)
37
+ except ImportError as e:
38
+ raise typer.BadParameter(f"Cannot import module '{module_path}': {e}")
39
+
40
+ if not hasattr(module, attr_name):
41
+ raise typer.BadParameter(
42
+ f"Module '{module_path}' has no attribute '{attr_name}'"
43
+ )
44
+
45
+ return getattr(module, attr_name)
46
+
47
+
48
+ def get_config_value(key: str, required: bool = False):
49
+ """Get a configuration value from pyproject.toml.
50
+
51
+ Args:
52
+ key: The configuration key in [tool.fastapi-toolsets].
53
+ required: If True, raises an error when the key is missing.
54
+
55
+ Returns:
56
+ The configuration value, or None if not found and not required.
57
+
58
+ Raises:
59
+ typer.BadParameter: If required=True and the key is missing.
60
+ """
61
+ config = load_pyproject()
62
+ value = config.get(key)
63
+
64
+ if required and value is None:
65
+ raise typer.BadParameter(
66
+ f"No '{key}' configured. "
67
+ f"Add '{key}' to [tool.fastapi-toolsets] in pyproject.toml."
68
+ )
69
+
70
+ return value
71
+
72
+
73
+ def get_fixtures_registry():
74
+ """Import and return the fixtures registry from config."""
75
+ from ..fixtures import FixtureRegistry
76
+
77
+ import_path = get_config_value("fixtures", required=True)
78
+ registry = import_from_string(import_path)
79
+
80
+ if not isinstance(registry, FixtureRegistry):
81
+ raise typer.BadParameter(
82
+ f"'fixtures' must be a FixtureRegistry instance, got {type(registry).__name__}"
83
+ )
84
+
85
+ return registry
86
+
87
+
88
+ def get_db_context():
89
+ """Import and return the db_context function from config."""
90
+ import_path = get_config_value("db_context", required=True)
91
+ return import_from_string(import_path)
92
+
93
+
94
+ def get_custom_cli() -> typer.Typer | None:
95
+ """Import and return the custom CLI Typer instance from config."""
96
+ import_path = get_config_value("custom_cli")
97
+ if not import_path:
98
+ return None
99
+
100
+ custom = import_from_string(import_path)
101
+
102
+ if not isinstance(custom, typer.Typer):
103
+ raise typer.BadParameter(
104
+ f"'custom_cli' must be a Typer instance, got {type(custom).__name__}"
105
+ )
106
+
107
+ return custom
@@ -0,0 +1,43 @@
1
+ """Pyproject.toml discovery and loading."""
2
+
3
+ import tomllib
4
+ from pathlib import Path
5
+
6
+ TOOL_NAME = "fastapi-toolsets"
7
+
8
+
9
+ def find_pyproject(start_path: Path | None = None) -> Path | None:
10
+ """Find pyproject.toml by walking up the directory tree.
11
+
12
+ Similar to how pytest, black, and ruff discover their config files.
13
+ """
14
+ path = (start_path or Path.cwd()).resolve()
15
+
16
+ for directory in [path, *path.parents]:
17
+ pyproject = directory / "pyproject.toml"
18
+ if pyproject.is_file():
19
+ return pyproject
20
+
21
+ return None
22
+
23
+
24
+ def load_pyproject(path: Path | None = None) -> dict:
25
+ """Load tool configuration from pyproject.toml.
26
+
27
+ Args:
28
+ path: Explicit path to pyproject.toml. If None, searches up from cwd.
29
+
30
+ Returns:
31
+ The [tool.fastapi-toolsets] section as a dict, or empty dict if not found.
32
+ """
33
+ pyproject_path = path or find_pyproject()
34
+
35
+ if not pyproject_path:
36
+ return {}
37
+
38
+ try:
39
+ with open(pyproject_path, "rb") as f:
40
+ data = tomllib.load(f)
41
+ return data.get("tool", {}).get(TOOL_NAME, {})
42
+ except (OSError, tomllib.TOMLDecodeError):
43
+ return {}
@@ -0,0 +1,5 @@
1
+ """FastAPI dependency factories for database objects."""
2
+
3
+ from .factory import BodyDependency, PathDependency
4
+
5
+ __all__ = ["BodyDependency", "PathDependency"]
@@ -0,0 +1,139 @@
1
+ """Dependency factories for FastAPI routes."""
2
+
3
+ import inspect
4
+ from collections.abc import AsyncGenerator, Callable
5
+ from typing import Any, TypeVar, cast
6
+
7
+ from fastapi import Depends
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+ from sqlalchemy.orm import DeclarativeBase
10
+
11
+ from ..crud import CrudFactory
12
+
13
+ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
14
+ SessionDependency = Callable[[], AsyncGenerator[AsyncSession, None]]
15
+
16
+
17
+ def PathDependency(
18
+ model: type[ModelType],
19
+ field: Any,
20
+ *,
21
+ session_dep: SessionDependency,
22
+ param_name: str | None = None,
23
+ ) -> ModelType:
24
+ """Create a dependency that fetches a DB object from a path parameter.
25
+
26
+ Args:
27
+ model: SQLAlchemy model class
28
+ field: Model field to filter by (e.g., User.id)
29
+ session_dep: Session dependency function (e.g., get_db)
30
+ param_name: Path parameter name (defaults to model_field, e.g., user_id)
31
+
32
+ Returns:
33
+ A Depends() instance that resolves to the model instance
34
+
35
+ Raises:
36
+ NotFoundError: If no matching record is found
37
+
38
+ Example:
39
+ UserDep = PathDependency(User, User.id, session_dep=get_db)
40
+
41
+ @router.get("/user/{id}")
42
+ async def get(
43
+ user: User = UserDep,
44
+ ): ...
45
+ """
46
+ crud = CrudFactory(model)
47
+ name = (
48
+ param_name
49
+ if param_name is not None
50
+ else "{}_{}".format(model.__name__.lower(), field.key)
51
+ )
52
+ python_type = field.type.python_type
53
+
54
+ async def dependency(
55
+ session: AsyncSession = Depends(session_dep), **kwargs: Any
56
+ ) -> ModelType:
57
+ value = kwargs[name]
58
+ return await crud.get(session, filters=[field == value])
59
+
60
+ setattr(
61
+ dependency,
62
+ "__signature__",
63
+ inspect.Signature(
64
+ parameters=[
65
+ inspect.Parameter(
66
+ name, inspect.Parameter.KEYWORD_ONLY, annotation=python_type
67
+ ),
68
+ inspect.Parameter(
69
+ "session",
70
+ inspect.Parameter.KEYWORD_ONLY,
71
+ annotation=AsyncSession,
72
+ default=Depends(session_dep),
73
+ ),
74
+ ]
75
+ ),
76
+ )
77
+
78
+ return cast(ModelType, Depends(cast(Callable[..., ModelType], dependency)))
79
+
80
+
81
+ def BodyDependency(
82
+ model: type[ModelType],
83
+ field: Any,
84
+ *,
85
+ session_dep: SessionDependency,
86
+ body_field: str,
87
+ ) -> ModelType:
88
+ """Create a dependency that fetches a DB object from a body field.
89
+
90
+ Args:
91
+ model: SQLAlchemy model class
92
+ field: Model field to filter by (e.g., User.id)
93
+ session_dep: Session dependency function (e.g., get_db)
94
+ body_field: Name of the field in the request body
95
+
96
+ Returns:
97
+ A Depends() instance that resolves to the model instance
98
+
99
+ Raises:
100
+ NotFoundError: If no matching record is found
101
+
102
+ Example:
103
+ UserDep = BodyDependency(
104
+ User, User.ctfd_id, session_dep=get_db, body_field="user_id"
105
+ )
106
+
107
+ @router.post("/assign")
108
+ async def assign(
109
+ user: User = UserDep,
110
+ ): ...
111
+ """
112
+ crud = CrudFactory(model)
113
+ python_type = field.type.python_type
114
+
115
+ async def dependency(
116
+ session: AsyncSession = Depends(session_dep), **kwargs: Any
117
+ ) -> ModelType:
118
+ value = kwargs[body_field]
119
+ return await crud.get(session, filters=[field == value])
120
+
121
+ setattr(
122
+ dependency,
123
+ "__signature__",
124
+ inspect.Signature(
125
+ parameters=[
126
+ inspect.Parameter(
127
+ body_field, inspect.Parameter.KEYWORD_ONLY, annotation=python_type
128
+ ),
129
+ inspect.Parameter(
130
+ "session",
131
+ inspect.Parameter.KEYWORD_ONLY,
132
+ annotation=AsyncSession,
133
+ default=Depends(session_dep),
134
+ ),
135
+ ]
136
+ ),
137
+ )
138
+
139
+ return cast(ModelType, Depends(cast(Callable[..., ModelType], dependency)))
@@ -1,25 +0,0 @@
1
- """Main CLI application."""
2
-
3
- import typer
4
-
5
- from .config import load_config
6
-
7
- cli = typer.Typer(
8
- name="manager",
9
- help="CLI utilities for FastAPI projects.",
10
- no_args_is_help=True,
11
- )
12
-
13
- _config = load_config()
14
-
15
- if _config.fixtures:
16
- from .commands.fixtures import fixture_cli
17
-
18
- cli.add_typer(fixture_cli, name="fixtures")
19
-
20
-
21
- @cli.callback()
22
- def main(ctx: typer.Context) -> None:
23
- """FastAPI utilities CLI."""
24
- ctx.ensure_object(dict)
25
- ctx.obj["config"] = _config
@@ -1,92 +0,0 @@
1
- """CLI configuration."""
2
-
3
- import importlib
4
- import sys
5
- import tomllib
6
- from dataclasses import dataclass
7
- from pathlib import Path
8
-
9
- import typer
10
-
11
-
12
- @dataclass
13
- class CliConfig:
14
- """CLI configuration loaded from pyproject.toml."""
15
-
16
- fixtures: str | None = None
17
- db_context: str | None = None
18
-
19
- def get_fixtures_registry(self):
20
- """Import and return the fixtures registry."""
21
- from ..fixtures import FixtureRegistry
22
-
23
- if not self.fixtures:
24
- raise typer.BadParameter(
25
- "No fixtures registry configured. "
26
- "Add 'fixtures' to [tool.fastapi-toolsets] in pyproject.toml."
27
- )
28
-
29
- registry = _import_from_string(self.fixtures)
30
-
31
- if not isinstance(registry, FixtureRegistry):
32
- raise typer.BadParameter(
33
- f"'fixtures' must be a FixtureRegistry instance, got {type(registry).__name__}"
34
- )
35
-
36
- return registry
37
-
38
- def get_db_context(self):
39
- """Import and return the db_context function."""
40
- if not self.db_context:
41
- raise typer.BadParameter(
42
- "No db_context configured. "
43
- "Add 'db_context' to [tool.fastapi-toolsets] in pyproject.toml."
44
- )
45
- return _import_from_string(self.db_context)
46
-
47
-
48
- def _import_from_string(import_path: str):
49
- """Import an object from a string path like 'module.submodule:attribute'."""
50
- if ":" not in import_path:
51
- raise typer.BadParameter(
52
- f"Invalid import path '{import_path}'. Expected format: 'module:attribute'"
53
- )
54
-
55
- module_path, attr_name = import_path.rsplit(":", 1)
56
-
57
- # Add cwd to sys.path for local imports
58
- cwd = str(Path.cwd())
59
- if cwd not in sys.path:
60
- sys.path.insert(0, cwd)
61
-
62
- try:
63
- module = importlib.import_module(module_path)
64
- except ImportError as e:
65
- raise typer.BadParameter(f"Cannot import module '{module_path}': {e}")
66
-
67
- if not hasattr(module, attr_name):
68
- raise typer.BadParameter(
69
- f"Module '{module_path}' has no attribute '{attr_name}'"
70
- )
71
-
72
- return getattr(module, attr_name)
73
-
74
-
75
- def load_config() -> CliConfig:
76
- """Load CLI configuration from pyproject.toml."""
77
- pyproject_path = Path.cwd() / "pyproject.toml"
78
-
79
- if not pyproject_path.exists():
80
- return CliConfig()
81
-
82
- try:
83
- with open(pyproject_path, "rb") as f:
84
- data = tomllib.load(f)
85
-
86
- tool_config = data.get("tool", {}).get("fastapi-toolsets", {})
87
- return CliConfig(
88
- fixtures=tool_config.get("fixtures"),
89
- db_context=tool_config.get("db_context"),
90
- )
91
- except Exception:
92
- return CliConfig()