fastapi-toolsets 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- fastapi_toolsets/__init__.py +1 -1
- fastapi_toolsets/cli/__init__.py +3 -2
- fastapi_toolsets/cli/app.py +10 -82
- fastapi_toolsets/cli/commands/fixtures.py +35 -141
- fastapi_toolsets/cli/config.py +92 -0
- fastapi_toolsets/cli/utils.py +27 -0
- fastapi_toolsets/crud/__init__.py +0 -2
- fastapi_toolsets/crud/factory.py +106 -6
- fastapi_toolsets/crud/search.py +4 -3
- fastapi_toolsets/exceptions/__init__.py +4 -2
- fastapi_toolsets/fixtures/registry.py +43 -5
- {fastapi_toolsets-0.4.0.dist-info → fastapi_toolsets-0.5.0.dist-info}/METADATA +1 -1
- fastapi_toolsets-0.5.0.dist-info/RECORD +28 -0
- {fastapi_toolsets-0.4.0.dist-info → fastapi_toolsets-0.5.0.dist-info}/WHEEL +1 -1
- fastapi_toolsets-0.5.0.dist-info/entry_points.txt +3 -0
- fastapi_toolsets-0.4.0.dist-info/RECORD +0 -26
- fastapi_toolsets-0.4.0.dist-info/entry_points.txt +0 -3
- {fastapi_toolsets-0.4.0.dist-info → fastapi_toolsets-0.5.0.dist-info}/licenses/LICENSE +0 -0
fastapi_toolsets/__init__.py
CHANGED
fastapi_toolsets/cli/__init__.py
CHANGED
fastapi_toolsets/cli/app.py
CHANGED
|
@@ -1,97 +1,25 @@
|
|
|
1
1
|
"""Main CLI application."""
|
|
2
2
|
|
|
3
|
-
import importlib.util
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Annotated
|
|
7
|
-
|
|
8
3
|
import typer
|
|
9
4
|
|
|
10
|
-
from .
|
|
5
|
+
from .config import load_config
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
name="
|
|
7
|
+
cli = typer.Typer(
|
|
8
|
+
name="manager",
|
|
14
9
|
help="CLI utilities for FastAPI projects.",
|
|
15
10
|
no_args_is_help=True,
|
|
16
11
|
)
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
app.add_typer(fixtures.app, name="fixtures")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def register_command(command: typer.Typer, name: str) -> None:
|
|
23
|
-
"""Register a custom command group.
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
command: Typer app for the command group
|
|
27
|
-
name: Name for the command group
|
|
13
|
+
_config = load_config()
|
|
28
14
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
import typer
|
|
32
|
-
from fastapi_toolsets.cli import app, register_command
|
|
15
|
+
if _config.fixtures:
|
|
16
|
+
from .commands.fixtures import fixture_cli
|
|
33
17
|
|
|
34
|
-
|
|
18
|
+
cli.add_typer(fixture_cli, name="fixtures")
|
|
35
19
|
|
|
36
|
-
@my_commands.command()
|
|
37
|
-
def seed():
|
|
38
|
-
'''Seed the database.'''
|
|
39
|
-
...
|
|
40
20
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"""
|
|
44
|
-
app.add_typer(command, name=name)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@app.callback()
|
|
48
|
-
def main(
|
|
49
|
-
ctx: typer.Context,
|
|
50
|
-
config: Annotated[
|
|
51
|
-
Path | None,
|
|
52
|
-
typer.Option(
|
|
53
|
-
"--config",
|
|
54
|
-
"-c",
|
|
55
|
-
help="Path to project config file (Python module with fixtures registry).",
|
|
56
|
-
envvar="FASTAPI_TOOLSETS_CONFIG",
|
|
57
|
-
),
|
|
58
|
-
] = None,
|
|
59
|
-
) -> None:
|
|
21
|
+
@cli.callback()
|
|
22
|
+
def main(ctx: typer.Context) -> None:
|
|
60
23
|
"""FastAPI utilities CLI."""
|
|
61
24
|
ctx.ensure_object(dict)
|
|
62
|
-
|
|
63
|
-
if config:
|
|
64
|
-
ctx.obj["config_path"] = config
|
|
65
|
-
# Load the config module
|
|
66
|
-
config_module = _load_module_from_path(config)
|
|
67
|
-
ctx.obj["config_module"] = config_module
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _load_module_from_path(path: Path) -> object:
|
|
71
|
-
"""Load a Python module from a file path.
|
|
72
|
-
|
|
73
|
-
Handles both absolute and relative imports by adding the config's
|
|
74
|
-
parent directory to sys.path temporarily.
|
|
75
|
-
"""
|
|
76
|
-
path = path.resolve()
|
|
77
|
-
|
|
78
|
-
# Add the parent directory to sys.path to support relative imports
|
|
79
|
-
parent_dir = str(
|
|
80
|
-
path.parent.parent
|
|
81
|
-
) # Go up two levels (e.g., from app/cli_config.py to project root)
|
|
82
|
-
if parent_dir not in sys.path:
|
|
83
|
-
sys.path.insert(0, parent_dir)
|
|
84
|
-
|
|
85
|
-
# Also add immediate parent for direct module imports
|
|
86
|
-
immediate_parent = str(path.parent)
|
|
87
|
-
if immediate_parent not in sys.path:
|
|
88
|
-
sys.path.insert(0, immediate_parent)
|
|
89
|
-
|
|
90
|
-
spec = importlib.util.spec_from_file_location("config", path)
|
|
91
|
-
if spec is None or spec.loader is None:
|
|
92
|
-
raise typer.BadParameter(f"Cannot load module from {path}")
|
|
93
|
-
|
|
94
|
-
module = importlib.util.module_from_spec(spec)
|
|
95
|
-
sys.modules["config"] = module
|
|
96
|
-
spec.loader.exec_module(module)
|
|
97
|
-
return module
|
|
25
|
+
ctx.obj["config"] = _config
|
|
@@ -1,55 +1,29 @@
|
|
|
1
1
|
"""Fixture management commands."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
from typing import Annotated
|
|
5
4
|
|
|
6
5
|
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
7
8
|
|
|
8
|
-
from ...fixtures import Context,
|
|
9
|
+
from ...fixtures import Context, LoadStrategy, load_fixtures_by_context
|
|
10
|
+
from ..config import CliConfig
|
|
11
|
+
from ..utils import async_command
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
fixture_cli = typer.Typer(
|
|
11
14
|
name="fixtures",
|
|
12
15
|
help="Manage database fixtures.",
|
|
13
16
|
no_args_is_help=True,
|
|
14
17
|
)
|
|
18
|
+
console = Console()
|
|
15
19
|
|
|
16
20
|
|
|
17
|
-
def
|
|
18
|
-
"""Get
|
|
19
|
-
|
|
20
|
-
if config is None:
|
|
21
|
-
raise typer.BadParameter(
|
|
22
|
-
"No config provided. Use --config to specify a config file with a 'fixtures' registry."
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
registry = getattr(config, "fixtures", None)
|
|
26
|
-
if registry is None:
|
|
27
|
-
raise typer.BadParameter(
|
|
28
|
-
"Config module must have a 'fixtures' attribute (FixtureRegistry instance)."
|
|
29
|
-
)
|
|
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
|
-
|
|
21
|
+
def _get_config(ctx: typer.Context) -> CliConfig:
|
|
22
|
+
"""Get CLI config from context."""
|
|
23
|
+
return ctx.obj["config"]
|
|
38
24
|
|
|
39
|
-
def _get_db_context(ctx: typer.Context):
|
|
40
|
-
"""Get database context manager from config."""
|
|
41
|
-
config = ctx.obj.get("config_module") if ctx.obj else None
|
|
42
|
-
if config is None:
|
|
43
|
-
raise typer.BadParameter("No config provided.")
|
|
44
25
|
|
|
45
|
-
|
|
46
|
-
if get_db_context is None:
|
|
47
|
-
raise typer.BadParameter("Config module must have a 'get_db_context' function.")
|
|
48
|
-
|
|
49
|
-
return get_db_context
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@app.command("list")
|
|
26
|
+
@fixture_cli.command("list")
|
|
53
27
|
def list_fixtures(
|
|
54
28
|
ctx: typer.Context,
|
|
55
29
|
context: Annotated[
|
|
@@ -62,64 +36,28 @@ def list_fixtures(
|
|
|
62
36
|
] = None,
|
|
63
37
|
) -> None:
|
|
64
38
|
"""List all registered fixtures."""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if context
|
|
68
|
-
fixtures = registry.get_by_context(context)
|
|
69
|
-
else:
|
|
70
|
-
fixtures = registry.get_all()
|
|
39
|
+
config = _get_config(ctx)
|
|
40
|
+
registry = config.get_fixtures_registry()
|
|
41
|
+
fixtures = registry.get_by_context(context) if context else registry.get_all()
|
|
71
42
|
|
|
72
43
|
if not fixtures:
|
|
73
|
-
|
|
44
|
+
print("No fixtures found.")
|
|
74
45
|
return
|
|
75
46
|
|
|
76
|
-
|
|
77
|
-
typer.echo("-" * 80)
|
|
47
|
+
table = Table("Name", "Contexts", "Dependencies")
|
|
78
48
|
|
|
79
49
|
for fixture in fixtures:
|
|
80
50
|
contexts = ", ".join(fixture.contexts)
|
|
81
51
|
deps = ", ".join(fixture.depends_on) if fixture.depends_on else "-"
|
|
82
|
-
|
|
52
|
+
table.add_row(fixture.name, contexts, deps)
|
|
83
53
|
|
|
84
|
-
|
|
54
|
+
console.print(table)
|
|
55
|
+
print(f"\nTotal: {len(fixtures)} fixture(s)")
|
|
85
56
|
|
|
86
57
|
|
|
87
|
-
@
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
fixture_name: Annotated[
|
|
91
|
-
str | None,
|
|
92
|
-
typer.Argument(help="Show dependencies for a specific fixture."),
|
|
93
|
-
] = None,
|
|
94
|
-
) -> None:
|
|
95
|
-
"""Show fixture dependency graph."""
|
|
96
|
-
registry = _get_registry(ctx)
|
|
97
|
-
|
|
98
|
-
if fixture_name:
|
|
99
|
-
try:
|
|
100
|
-
order = registry.resolve_dependencies(fixture_name)
|
|
101
|
-
typer.echo(f"\nDependency chain for '{fixture_name}':\n")
|
|
102
|
-
for i, name in enumerate(order):
|
|
103
|
-
indent = " " * i
|
|
104
|
-
arrow = "└─> " if i > 0 else ""
|
|
105
|
-
typer.echo(f"{indent}{arrow}{name}")
|
|
106
|
-
except KeyError:
|
|
107
|
-
typer.echo(f"Fixture '{fixture_name}' not found.", err=True)
|
|
108
|
-
raise typer.Exit(1)
|
|
109
|
-
else:
|
|
110
|
-
# Show full graph
|
|
111
|
-
fixtures = registry.get_all()
|
|
112
|
-
|
|
113
|
-
typer.echo("\nFixture Dependency Graph:\n")
|
|
114
|
-
for fixture in fixtures:
|
|
115
|
-
deps = (
|
|
116
|
-
f" -> [{', '.join(fixture.depends_on)}]" if fixture.depends_on else ""
|
|
117
|
-
)
|
|
118
|
-
typer.echo(f" {fixture.name}{deps}")
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
@app.command("load")
|
|
122
|
-
def load(
|
|
58
|
+
@fixture_cli.command("load")
|
|
59
|
+
@async_command
|
|
60
|
+
async def load(
|
|
123
61
|
ctx: typer.Context,
|
|
124
62
|
contexts: Annotated[
|
|
125
63
|
list[str] | None,
|
|
@@ -141,16 +79,12 @@ def load(
|
|
|
141
79
|
] = False,
|
|
142
80
|
) -> None:
|
|
143
81
|
"""Load fixtures into the database."""
|
|
144
|
-
|
|
145
|
-
|
|
82
|
+
config = _get_config(ctx)
|
|
83
|
+
registry = config.get_fixtures_registry()
|
|
84
|
+
get_db_context = config.get_db_context()
|
|
146
85
|
|
|
147
|
-
|
|
148
|
-
if contexts:
|
|
149
|
-
context_list = contexts
|
|
150
|
-
else:
|
|
151
|
-
context_list = [Context.BASE]
|
|
86
|
+
context_list = contexts if contexts else [Context.BASE]
|
|
152
87
|
|
|
153
|
-
# Parse strategy
|
|
154
88
|
try:
|
|
155
89
|
load_strategy = LoadStrategy(strategy)
|
|
156
90
|
except ValueError:
|
|
@@ -159,67 +93,27 @@ def load(
|
|
|
159
93
|
)
|
|
160
94
|
raise typer.Exit(1)
|
|
161
95
|
|
|
162
|
-
# Resolve what will be loaded
|
|
163
96
|
ordered = registry.resolve_context_dependencies(*context_list)
|
|
164
97
|
|
|
165
98
|
if not ordered:
|
|
166
|
-
|
|
99
|
+
print("No fixtures to load for the specified context(s).")
|
|
167
100
|
return
|
|
168
101
|
|
|
169
|
-
|
|
102
|
+
print(f"\nFixtures to load ({load_strategy.value} strategy):")
|
|
170
103
|
for name in ordered:
|
|
171
104
|
fixture = registry.get(name)
|
|
172
105
|
instances = list(fixture.func())
|
|
173
106
|
model_name = type(instances[0]).__name__ if instances else "?"
|
|
174
|
-
|
|
107
|
+
print(f" - {name}: {len(instances)} {model_name}(s)")
|
|
175
108
|
|
|
176
109
|
if dry_run:
|
|
177
|
-
|
|
110
|
+
print("\n[Dry run - no changes made]")
|
|
178
111
|
return
|
|
179
112
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
result = await load_fixtures_by_context(
|
|
185
|
-
session, registry, *context_list, strategy=load_strategy
|
|
186
|
-
)
|
|
187
|
-
return result
|
|
188
|
-
|
|
189
|
-
result = asyncio.run(do_load())
|
|
113
|
+
async with get_db_context() as session:
|
|
114
|
+
result = await load_fixtures_by_context(
|
|
115
|
+
session, registry, *context_list, strategy=load_strategy
|
|
116
|
+
)
|
|
190
117
|
|
|
191
118
|
total = sum(len(items) for items in result.values())
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
@app.command("show")
|
|
196
|
-
def show_fixture(
|
|
197
|
-
ctx: typer.Context,
|
|
198
|
-
name: Annotated[str, typer.Argument(help="Fixture name to show.")],
|
|
199
|
-
) -> None:
|
|
200
|
-
"""Show details of a specific fixture."""
|
|
201
|
-
registry = _get_registry(ctx)
|
|
202
|
-
|
|
203
|
-
try:
|
|
204
|
-
fixture = registry.get(name)
|
|
205
|
-
except KeyError:
|
|
206
|
-
typer.echo(f"Fixture '{name}' not found.", err=True)
|
|
207
|
-
raise typer.Exit(1)
|
|
208
|
-
|
|
209
|
-
typer.echo(f"\nFixture: {fixture.name}")
|
|
210
|
-
typer.echo(f"Contexts: {', '.join(fixture.contexts)}")
|
|
211
|
-
typer.echo(
|
|
212
|
-
f"Dependencies: {', '.join(fixture.depends_on) if fixture.depends_on else 'None'}"
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
# Show instances
|
|
216
|
-
instances = list(fixture.func())
|
|
217
|
-
if instances:
|
|
218
|
-
model_name = type(instances[0]).__name__
|
|
219
|
-
typer.echo(f"\nInstances ({len(instances)} {model_name}):")
|
|
220
|
-
for instance in instances[:10]: # Limit to 10
|
|
221
|
-
typer.echo(f" - {instance!r}")
|
|
222
|
-
if len(instances) > 10:
|
|
223
|
-
typer.echo(f" ... and {len(instances) - 10} more")
|
|
224
|
-
else:
|
|
225
|
-
typer.echo("\nNo instances (empty fixture)")
|
|
119
|
+
print(f"\nLoaded {total} record(s) successfully.")
|
|
@@ -0,0 +1,92 @@
|
|
|
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()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""CLI utility functions."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
5
|
+
from collections.abc import Callable, Coroutine
|
|
6
|
+
from typing import Any, ParamSpec, TypeVar
|
|
7
|
+
|
|
8
|
+
P = ParamSpec("P")
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def async_command(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
|
|
13
|
+
"""Decorator to run an async function as a sync CLI command.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
@fixture_cli.command("load")
|
|
17
|
+
@async_command
|
|
18
|
+
async def load(ctx: typer.Context) -> None:
|
|
19
|
+
async with get_db_context() as session:
|
|
20
|
+
await load_fixtures(session, registry)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@functools.wraps(func)
|
|
24
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
25
|
+
return asyncio.run(func(*args, **kwargs))
|
|
26
|
+
|
|
27
|
+
return wrapper
|
|
@@ -4,7 +4,6 @@ from ..exceptions import NoSearchableFieldsError
|
|
|
4
4
|
from .factory import CrudFactory
|
|
5
5
|
from .search import (
|
|
6
6
|
SearchConfig,
|
|
7
|
-
SearchFieldType,
|
|
8
7
|
get_searchable_fields,
|
|
9
8
|
)
|
|
10
9
|
|
|
@@ -13,5 +12,4 @@ __all__ = [
|
|
|
13
12
|
"get_searchable_fields",
|
|
14
13
|
"NoSearchableFieldsError",
|
|
15
14
|
"SearchConfig",
|
|
16
|
-
"SearchFieldType",
|
|
17
15
|
]
|
fastapi_toolsets/crud/factory.py
CHANGED
|
@@ -17,6 +17,7 @@ from ..exceptions import NotFoundError
|
|
|
17
17
|
from .search import SearchConfig, SearchFieldType, build_search_filters
|
|
18
18
|
|
|
19
19
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
|
20
|
+
JoinType = list[tuple[type[DeclarativeBase], Any]]
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class AsyncCrud(Generic[ModelType]):
|
|
@@ -55,6 +56,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
55
56
|
session: AsyncSession,
|
|
56
57
|
filters: list[Any],
|
|
57
58
|
*,
|
|
59
|
+
joins: JoinType | None = None,
|
|
60
|
+
outer_join: bool = False,
|
|
58
61
|
with_for_update: bool = False,
|
|
59
62
|
load_options: list[Any] | None = None,
|
|
60
63
|
) -> ModelType:
|
|
@@ -63,6 +66,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
63
66
|
Args:
|
|
64
67
|
session: DB async session
|
|
65
68
|
filters: List of SQLAlchemy filter conditions
|
|
69
|
+
joins: List of (model, condition) tuples for joining related tables
|
|
70
|
+
outer_join: Use LEFT OUTER JOIN instead of INNER JOIN
|
|
66
71
|
with_for_update: Lock the row for update
|
|
67
72
|
load_options: SQLAlchemy loader options (e.g., selectinload)
|
|
68
73
|
|
|
@@ -73,7 +78,15 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
73
78
|
NotFoundError: If no record found
|
|
74
79
|
MultipleResultsFound: If more than one record found
|
|
75
80
|
"""
|
|
76
|
-
q = select(cls.model)
|
|
81
|
+
q = select(cls.model)
|
|
82
|
+
if joins:
|
|
83
|
+
for model, condition in joins:
|
|
84
|
+
q = (
|
|
85
|
+
q.outerjoin(model, condition)
|
|
86
|
+
if outer_join
|
|
87
|
+
else q.join(model, condition)
|
|
88
|
+
)
|
|
89
|
+
q = q.where(and_(*filters))
|
|
77
90
|
if load_options:
|
|
78
91
|
q = q.options(*load_options)
|
|
79
92
|
if with_for_update:
|
|
@@ -90,6 +103,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
90
103
|
session: AsyncSession,
|
|
91
104
|
filters: list[Any] | None = None,
|
|
92
105
|
*,
|
|
106
|
+
joins: JoinType | None = None,
|
|
107
|
+
outer_join: bool = False,
|
|
93
108
|
load_options: list[Any] | None = None,
|
|
94
109
|
) -> ModelType | None:
|
|
95
110
|
"""Get the first matching record, or None.
|
|
@@ -97,12 +112,21 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
97
112
|
Args:
|
|
98
113
|
session: DB async session
|
|
99
114
|
filters: List of SQLAlchemy filter conditions
|
|
115
|
+
joins: List of (model, condition) tuples for joining related tables
|
|
116
|
+
outer_join: Use LEFT OUTER JOIN instead of INNER JOIN
|
|
100
117
|
load_options: SQLAlchemy loader options
|
|
101
118
|
|
|
102
119
|
Returns:
|
|
103
120
|
Model instance or None
|
|
104
121
|
"""
|
|
105
122
|
q = select(cls.model)
|
|
123
|
+
if joins:
|
|
124
|
+
for model, condition in joins:
|
|
125
|
+
q = (
|
|
126
|
+
q.outerjoin(model, condition)
|
|
127
|
+
if outer_join
|
|
128
|
+
else q.join(model, condition)
|
|
129
|
+
)
|
|
106
130
|
if filters:
|
|
107
131
|
q = q.where(and_(*filters))
|
|
108
132
|
if load_options:
|
|
@@ -116,6 +140,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
116
140
|
session: AsyncSession,
|
|
117
141
|
*,
|
|
118
142
|
filters: list[Any] | None = None,
|
|
143
|
+
joins: JoinType | None = None,
|
|
144
|
+
outer_join: bool = False,
|
|
119
145
|
load_options: list[Any] | None = None,
|
|
120
146
|
order_by: Any | None = None,
|
|
121
147
|
limit: int | None = None,
|
|
@@ -126,6 +152,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
126
152
|
Args:
|
|
127
153
|
session: DB async session
|
|
128
154
|
filters: List of SQLAlchemy filter conditions
|
|
155
|
+
joins: List of (model, condition) tuples for joining related tables
|
|
156
|
+
outer_join: Use LEFT OUTER JOIN instead of INNER JOIN
|
|
129
157
|
load_options: SQLAlchemy loader options
|
|
130
158
|
order_by: Column or list of columns to order by
|
|
131
159
|
limit: Max number of rows to return
|
|
@@ -135,6 +163,13 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
135
163
|
List of model instances
|
|
136
164
|
"""
|
|
137
165
|
q = select(cls.model)
|
|
166
|
+
if joins:
|
|
167
|
+
for model, condition in joins:
|
|
168
|
+
q = (
|
|
169
|
+
q.outerjoin(model, condition)
|
|
170
|
+
if outer_join
|
|
171
|
+
else q.join(model, condition)
|
|
172
|
+
)
|
|
138
173
|
if filters:
|
|
139
174
|
q = q.where(and_(*filters))
|
|
140
175
|
if load_options:
|
|
@@ -254,17 +289,29 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
254
289
|
cls: type[Self],
|
|
255
290
|
session: AsyncSession,
|
|
256
291
|
filters: list[Any] | None = None,
|
|
292
|
+
*,
|
|
293
|
+
joins: JoinType | None = None,
|
|
294
|
+
outer_join: bool = False,
|
|
257
295
|
) -> int:
|
|
258
296
|
"""Count records matching the filters.
|
|
259
297
|
|
|
260
298
|
Args:
|
|
261
299
|
session: DB async session
|
|
262
300
|
filters: List of SQLAlchemy filter conditions
|
|
301
|
+
joins: List of (model, condition) tuples for joining related tables
|
|
302
|
+
outer_join: Use LEFT OUTER JOIN instead of INNER JOIN
|
|
263
303
|
|
|
264
304
|
Returns:
|
|
265
305
|
Number of matching records
|
|
266
306
|
"""
|
|
267
307
|
q = select(func.count()).select_from(cls.model)
|
|
308
|
+
if joins:
|
|
309
|
+
for model, condition in joins:
|
|
310
|
+
q = (
|
|
311
|
+
q.outerjoin(model, condition)
|
|
312
|
+
if outer_join
|
|
313
|
+
else q.join(model, condition)
|
|
314
|
+
)
|
|
268
315
|
if filters:
|
|
269
316
|
q = q.where(and_(*filters))
|
|
270
317
|
result = await session.execute(q)
|
|
@@ -275,17 +322,30 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
275
322
|
cls: type[Self],
|
|
276
323
|
session: AsyncSession,
|
|
277
324
|
filters: list[Any],
|
|
325
|
+
*,
|
|
326
|
+
joins: JoinType | None = None,
|
|
327
|
+
outer_join: bool = False,
|
|
278
328
|
) -> bool:
|
|
279
329
|
"""Check if a record exists.
|
|
280
330
|
|
|
281
331
|
Args:
|
|
282
332
|
session: DB async session
|
|
283
333
|
filters: List of SQLAlchemy filter conditions
|
|
334
|
+
joins: List of (model, condition) tuples for joining related tables
|
|
335
|
+
outer_join: Use LEFT OUTER JOIN instead of INNER JOIN
|
|
284
336
|
|
|
285
337
|
Returns:
|
|
286
338
|
True if at least one record matches
|
|
287
339
|
"""
|
|
288
|
-
q = select(cls.model)
|
|
340
|
+
q = select(cls.model)
|
|
341
|
+
if joins:
|
|
342
|
+
for model, condition in joins:
|
|
343
|
+
q = (
|
|
344
|
+
q.outerjoin(model, condition)
|
|
345
|
+
if outer_join
|
|
346
|
+
else q.join(model, condition)
|
|
347
|
+
)
|
|
348
|
+
q = q.where(and_(*filters)).exists().select()
|
|
289
349
|
result = await session.execute(q)
|
|
290
350
|
return bool(result.scalar())
|
|
291
351
|
|
|
@@ -295,6 +355,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
295
355
|
session: AsyncSession,
|
|
296
356
|
*,
|
|
297
357
|
filters: list[Any] | None = None,
|
|
358
|
+
joins: JoinType | None = None,
|
|
359
|
+
outer_join: bool = False,
|
|
298
360
|
load_options: list[Any] | None = None,
|
|
299
361
|
order_by: Any | None = None,
|
|
300
362
|
page: int = 1,
|
|
@@ -307,6 +369,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
307
369
|
Args:
|
|
308
370
|
session: DB async session
|
|
309
371
|
filters: List of SQLAlchemy filter conditions
|
|
372
|
+
joins: List of (model, condition) tuples for joining related tables
|
|
373
|
+
outer_join: Use LEFT OUTER JOIN instead of INNER JOIN
|
|
310
374
|
load_options: SQLAlchemy loader options
|
|
311
375
|
order_by: Column or list of columns to order by
|
|
312
376
|
page: Page number (1-indexed)
|
|
@@ -319,7 +383,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
319
383
|
"""
|
|
320
384
|
filters = list(filters) if filters else []
|
|
321
385
|
offset = (page - 1) * items_per_page
|
|
322
|
-
|
|
386
|
+
search_joins: list[Any] = []
|
|
323
387
|
|
|
324
388
|
# Build search filters
|
|
325
389
|
if search:
|
|
@@ -330,11 +394,21 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
330
394
|
default_fields=cls.searchable_fields,
|
|
331
395
|
)
|
|
332
396
|
filters.extend(search_filters)
|
|
333
|
-
joins.extend(search_joins)
|
|
334
397
|
|
|
335
398
|
# Build query with joins
|
|
336
399
|
q = select(cls.model)
|
|
337
|
-
|
|
400
|
+
|
|
401
|
+
# Apply explicit joins
|
|
402
|
+
if joins:
|
|
403
|
+
for model, condition in joins:
|
|
404
|
+
q = (
|
|
405
|
+
q.outerjoin(model, condition)
|
|
406
|
+
if outer_join
|
|
407
|
+
else q.join(model, condition)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Apply search joins (always outer joins for search)
|
|
411
|
+
for join_rel in search_joins:
|
|
338
412
|
q = q.outerjoin(join_rel)
|
|
339
413
|
|
|
340
414
|
if filters:
|
|
@@ -352,8 +426,20 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
352
426
|
pk_col = cls.model.__mapper__.primary_key[0]
|
|
353
427
|
count_q = select(func.count(func.distinct(getattr(cls.model, pk_col.name))))
|
|
354
428
|
count_q = count_q.select_from(cls.model)
|
|
355
|
-
|
|
429
|
+
|
|
430
|
+
# Apply explicit joins to count query
|
|
431
|
+
if joins:
|
|
432
|
+
for model, condition in joins:
|
|
433
|
+
count_q = (
|
|
434
|
+
count_q.outerjoin(model, condition)
|
|
435
|
+
if outer_join
|
|
436
|
+
else count_q.join(model, condition)
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Apply search joins to count query
|
|
440
|
+
for join_rel in search_joins:
|
|
356
441
|
count_q = count_q.outerjoin(join_rel)
|
|
442
|
+
|
|
357
443
|
if filters:
|
|
358
444
|
count_q = count_q.where(and_(*filters))
|
|
359
445
|
|
|
@@ -404,6 +490,20 @@ def CrudFactory(
|
|
|
404
490
|
|
|
405
491
|
# With search
|
|
406
492
|
result = await UserCrud.paginate(session, search="john")
|
|
493
|
+
|
|
494
|
+
# With joins (inner join by default):
|
|
495
|
+
users = await UserCrud.get_multi(
|
|
496
|
+
session,
|
|
497
|
+
joins=[(Post, Post.user_id == User.id)],
|
|
498
|
+
filters=[Post.published == True],
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# With outer join:
|
|
502
|
+
users = await UserCrud.get_multi(
|
|
503
|
+
session,
|
|
504
|
+
joins=[(Post, Post.user_id == User.id)],
|
|
505
|
+
outer_join=True,
|
|
506
|
+
)
|
|
407
507
|
"""
|
|
408
508
|
cls = type(
|
|
409
509
|
f"Async{model.__name__}Crud",
|
fastapi_toolsets/crud/search.py
CHANGED
|
@@ -129,11 +129,12 @@ def build_search_filters(
|
|
|
129
129
|
else:
|
|
130
130
|
column = field
|
|
131
131
|
|
|
132
|
-
# Build the filter
|
|
132
|
+
# Build the filter (cast to String for non-text columns)
|
|
133
|
+
column_as_string = column.cast(String)
|
|
133
134
|
if config.case_sensitive:
|
|
134
|
-
filters.append(
|
|
135
|
+
filters.append(column_as_string.like(f"%{query}%"))
|
|
135
136
|
else:
|
|
136
|
-
filters.append(
|
|
137
|
+
filters.append(column_as_string.ilike(f"%{query}%"))
|
|
137
138
|
|
|
138
139
|
if not filters:
|
|
139
140
|
return [], []
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .exceptions import (
|
|
2
|
+
ApiError,
|
|
2
3
|
ApiException,
|
|
3
4
|
ConflictError,
|
|
4
5
|
ForbiddenError,
|
|
@@ -10,11 +11,12 @@ from .exceptions import (
|
|
|
10
11
|
from .handler import init_exceptions_handlers
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
13
|
-
"
|
|
14
|
-
"generate_error_responses",
|
|
14
|
+
"ApiError",
|
|
15
15
|
"ApiException",
|
|
16
16
|
"ConflictError",
|
|
17
17
|
"ForbiddenError",
|
|
18
|
+
"generate_error_responses",
|
|
19
|
+
"init_exceptions_handlers",
|
|
18
20
|
"NoSearchableFieldsError",
|
|
19
21
|
"NotFoundError",
|
|
20
22
|
"UnauthorizedError",
|
|
@@ -50,8 +50,16 @@ class FixtureRegistry:
|
|
|
50
50
|
]
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
|
-
def __init__(
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
contexts: list[str | Context] | None = None,
|
|
56
|
+
) -> None:
|
|
54
57
|
self._fixtures: dict[str, Fixture] = {}
|
|
58
|
+
self._default_contexts: list[str] | None = (
|
|
59
|
+
[c.value if isinstance(c, Context) else c for c in contexts]
|
|
60
|
+
if contexts
|
|
61
|
+
else None
|
|
62
|
+
)
|
|
55
63
|
|
|
56
64
|
def register(
|
|
57
65
|
self,
|
|
@@ -85,10 +93,14 @@ class FixtureRegistry:
|
|
|
85
93
|
fn: Callable[[], Sequence[DeclarativeBase]],
|
|
86
94
|
) -> Callable[[], Sequence[DeclarativeBase]]:
|
|
87
95
|
fixture_name = name or cast(Any, fn).__name__
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
if contexts is not None:
|
|
97
|
+
fixture_contexts = [
|
|
98
|
+
c.value if isinstance(c, Context) else c for c in contexts
|
|
99
|
+
]
|
|
100
|
+
elif self._default_contexts is not None:
|
|
101
|
+
fixture_contexts = self._default_contexts
|
|
102
|
+
else:
|
|
103
|
+
fixture_contexts = [Context.BASE.value]
|
|
92
104
|
|
|
93
105
|
self._fixtures[fixture_name] = Fixture(
|
|
94
106
|
name=fixture_name,
|
|
@@ -102,6 +114,32 @@ class FixtureRegistry:
|
|
|
102
114
|
return decorator(func)
|
|
103
115
|
return decorator
|
|
104
116
|
|
|
117
|
+
def include_registry(self, registry: "FixtureRegistry") -> None:
|
|
118
|
+
"""Include another `FixtureRegistry` in the same current `FixtureRegistry`.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
registry: The `FixtureRegistry` to include
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ValueError: If a fixture name already exists in the current registry
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
registry = FixtureRegistry()
|
|
128
|
+
dev_registry = FixtureRegistry()
|
|
129
|
+
|
|
130
|
+
@dev_registry.register
|
|
131
|
+
def dev_data():
|
|
132
|
+
return [...]
|
|
133
|
+
|
|
134
|
+
registry.include_registry(registry=dev_registry)
|
|
135
|
+
"""
|
|
136
|
+
for name, fixture in registry._fixtures.items():
|
|
137
|
+
if name in self._fixtures:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"Fixture '{name}' already exists in the current registry"
|
|
140
|
+
)
|
|
141
|
+
self._fixtures[name] = fixture
|
|
142
|
+
|
|
105
143
|
def get(self, name: str) -> Fixture:
|
|
106
144
|
"""Get a fixture by name."""
|
|
107
145
|
if name not in self._fixtures:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
fastapi_toolsets/__init__.py,sha256=OLAcxL7oaBcCFw2nD7oCp9JXiI9SCjLvwEkuG4_5TGc,820
|
|
2
|
+
fastapi_toolsets/cli/__init__.py,sha256=CGLeg0ud6-cKZwvE_qNKw9tXmzVfFCJD-U2eMdQ7InI,123
|
|
3
|
+
fastapi_toolsets/cli/app.py,sha256=cpBDDYPngjTv9ui6FaXrTzD4kfKyBBlqMQBT9wWdjuk,483
|
|
4
|
+
fastapi_toolsets/cli/commands/__init__.py,sha256=BogehmsY6olwLdfIBzviuppXP1LLl9znnxtmji3eLwI,29
|
|
5
|
+
fastapi_toolsets/cli/commands/fixtures.py,sha256=yBOMlrMRrMk0y-WD6YYXzIrMKGtM0egNVC-vG5GA9Bs,3348
|
|
6
|
+
fastapi_toolsets/cli/config.py,sha256=tRpvi2NnSEwPlDjyacMCHIapH61ONCp-24vpCxm4ElM,2678
|
|
7
|
+
fastapi_toolsets/cli/utils.py,sha256=KsW-FW9tBCAjUd_qludXa1jy8ch8TpkT-_ltcO0DWVs,733
|
|
8
|
+
fastapi_toolsets/crud/__init__.py,sha256=XSl_2k5qfjr0ZFtS7JgNtiNJmZjYL8bDh9BE5qrDyw0,325
|
|
9
|
+
fastapi_toolsets/crud/factory.py,sha256=wBtmFNC7cqzeZfyJLKW-lj3Dd0zo15bZfeTNA1DvqNE,16663
|
|
10
|
+
fastapi_toolsets/crud/search.py,sha256=OAhbkuDan-elAWyAUz0msieZxYkE76fpJYwaA1BRTQ8,4635
|
|
11
|
+
fastapi_toolsets/db.py,sha256=YUj5CrxCnREg7AqpJLNrLR2RDIOCS7stQCNOSS3cRho,5619
|
|
12
|
+
fastapi_toolsets/exceptions/__init__.py,sha256=vNP19B7aaVKnBpRXG_fdCZ0P_GlPDiZDYydPLxfdhD0,481
|
|
13
|
+
fastapi_toolsets/exceptions/exceptions.py,sha256=hu8_lvE9KmnYID9YgJqlzZMkCD0kASPrGAmN1hUe2bY,5086
|
|
14
|
+
fastapi_toolsets/exceptions/handler.py,sha256=IXfKiIr_LPo-11PRpOIrNRAXBkeQ5TdLcu3Gy-r6ChU,5916
|
|
15
|
+
fastapi_toolsets/fixtures/__init__.py,sha256=i5N6dt4LLVxhC0fBNhDTokuqUf43oXJChBkVMQ94hLA,328
|
|
16
|
+
fastapi_toolsets/fixtures/enum.py,sha256=02T4CrkH3-A3mPxpHaLzBQD4yzqExwjycaDBJr1ameA,715
|
|
17
|
+
fastapi_toolsets/fixtures/registry.py,sha256=O5BlaPh-BOZQmq-Yi0GWD-pYmAtdeIxUc-QJxoPGu5M,6587
|
|
18
|
+
fastapi_toolsets/fixtures/utils.py,sha256=DlsGBVl0zyxtKX5E3CEcV7rhzBYng6KD5hELD7bK0wo,4711
|
|
19
|
+
fastapi_toolsets/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
fastapi_toolsets/pytest/__init__.py,sha256=0GnnFxWNfpaShBBYsfRGIgSygwC1eo8TydmXCA3tmoM,188
|
|
21
|
+
fastapi_toolsets/pytest/plugin.py,sha256=lbEiumS2zi7jARY6eYBUPAlfKCplbLFrZXcmp0-RkcA,6892
|
|
22
|
+
fastapi_toolsets/pytest/utils.py,sha256=VqkxtbpEU8w7-0xfcZG0m8Tpn3LtdnvAJMyqWS7WtIw,3447
|
|
23
|
+
fastapi_toolsets/schemas.py,sha256=LBzrq4s5VWYeQqlUfOEvWDtpFdO8scgY0LRypk9KUAE,2639
|
|
24
|
+
fastapi_toolsets-0.5.0.dist-info/licenses/LICENSE,sha256=V2jCjI-VPB-veGY2Ktb0sU4vT_TldRciZ9lCE98bMoE,1063
|
|
25
|
+
fastapi_toolsets-0.5.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
26
|
+
fastapi_toolsets-0.5.0.dist-info/entry_points.txt,sha256=I-wL3xlhglq38WG6WDitgmE4Hstk9tmSUf4eIjz9ckk,54
|
|
27
|
+
fastapi_toolsets-0.5.0.dist-info/METADATA,sha256=5s38SOEaDNo7Qkn70UQpOtNhqFPwHDUQ72gyoTahlyE,4221
|
|
28
|
+
fastapi_toolsets-0.5.0.dist-info/RECORD,,
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
fastapi_toolsets/__init__.py,sha256=gzOvHPBZ5Kdja8XWMQM-zTxHKPJ7M7ptud7tUsd9bws,820
|
|
2
|
-
fastapi_toolsets/cli/__init__.py,sha256=QAcrenphE7D5toid_Kn777Cy1icSOWiEBjSE_oCuU4o,111
|
|
3
|
-
fastapi_toolsets/cli/app.py,sha256=G3y3PNN3biHs8GtiiGsrX2X8nXNjkrsUhHLXQo2-MXc,2588
|
|
4
|
-
fastapi_toolsets/cli/commands/__init__.py,sha256=BogehmsY6olwLdfIBzviuppXP1LLl9znnxtmji3eLwI,29
|
|
5
|
-
fastapi_toolsets/cli/commands/fixtures.py,sha256=qiC2dcrJ_Rb1PRzx6EycTFQQXwa_GQUVoptpx4chf9k,6679
|
|
6
|
-
fastapi_toolsets/crud/__init__.py,sha256=LRR57jsFna2IqSj6aTomMzFcaQbx7WVo5lnfhcTSU1g,369
|
|
7
|
-
fastapi_toolsets/crud/factory.py,sha256=c5aH2c38Qa44pQ3XKoJTcRox5x4rD4tuEJQ5DKFjSnc,13014
|
|
8
|
-
fastapi_toolsets/crud/search.py,sha256=a46SGMg484uv2DruNfEB-Rq6lJRi_K-Nr8tFTWQ2ouY,4530
|
|
9
|
-
fastapi_toolsets/db.py,sha256=YUj5CrxCnREg7AqpJLNrLR2RDIOCS7stQCNOSS3cRho,5619
|
|
10
|
-
fastapi_toolsets/exceptions/__init__.py,sha256=wlV4pVXuGdOtUvlThRJXmEc8g8Nmwt8MOMG_A-3j9zw,451
|
|
11
|
-
fastapi_toolsets/exceptions/exceptions.py,sha256=hu8_lvE9KmnYID9YgJqlzZMkCD0kASPrGAmN1hUe2bY,5086
|
|
12
|
-
fastapi_toolsets/exceptions/handler.py,sha256=IXfKiIr_LPo-11PRpOIrNRAXBkeQ5TdLcu3Gy-r6ChU,5916
|
|
13
|
-
fastapi_toolsets/fixtures/__init__.py,sha256=i5N6dt4LLVxhC0fBNhDTokuqUf43oXJChBkVMQ94hLA,328
|
|
14
|
-
fastapi_toolsets/fixtures/enum.py,sha256=02T4CrkH3-A3mPxpHaLzBQD4yzqExwjycaDBJr1ameA,715
|
|
15
|
-
fastapi_toolsets/fixtures/registry.py,sha256=lfoLdC6aZeJQR7_l0g_P5Y6DChbs8_zAgLQsR0Plmfg,5276
|
|
16
|
-
fastapi_toolsets/fixtures/utils.py,sha256=DlsGBVl0zyxtKX5E3CEcV7rhzBYng6KD5hELD7bK0wo,4711
|
|
17
|
-
fastapi_toolsets/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
fastapi_toolsets/pytest/__init__.py,sha256=0GnnFxWNfpaShBBYsfRGIgSygwC1eo8TydmXCA3tmoM,188
|
|
19
|
-
fastapi_toolsets/pytest/plugin.py,sha256=lbEiumS2zi7jARY6eYBUPAlfKCplbLFrZXcmp0-RkcA,6892
|
|
20
|
-
fastapi_toolsets/pytest/utils.py,sha256=VqkxtbpEU8w7-0xfcZG0m8Tpn3LtdnvAJMyqWS7WtIw,3447
|
|
21
|
-
fastapi_toolsets/schemas.py,sha256=LBzrq4s5VWYeQqlUfOEvWDtpFdO8scgY0LRypk9KUAE,2639
|
|
22
|
-
fastapi_toolsets-0.4.0.dist-info/licenses/LICENSE,sha256=V2jCjI-VPB-veGY2Ktb0sU4vT_TldRciZ9lCE98bMoE,1063
|
|
23
|
-
fastapi_toolsets-0.4.0.dist-info/WHEEL,sha256=e_m4S054HL0hyR3CpOk-b7Q7fDX6BuFkgL5OjAExXas,80
|
|
24
|
-
fastapi_toolsets-0.4.0.dist-info/entry_points.txt,sha256=pNU38Nn_DXBgYd-nLZCizMvrrdaPhHmkRwouDoBqvzw,63
|
|
25
|
-
fastapi_toolsets-0.4.0.dist-info/METADATA,sha256=cRTaYwgiAEVdU_DD4lG7idF6agtcxgSHpFsFIwp-7HY,4221
|
|
26
|
-
fastapi_toolsets-0.4.0.dist-info/RECORD,,
|
|
File without changes
|