fastapi-crud-generator 0.0.1__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_crud_generator/__init__.py +9 -0
- fastapi_crud_generator/config.py +50 -0
- fastapi_crud_generator/crud_generator.py +587 -0
- fastapi_crud_generator/deps.py +275 -0
- fastapi_crud_generator/orm/__init__.py +0 -0
- fastapi_crud_generator/orm/base.py +169 -0
- fastapi_crud_generator/orm/sqlalchemy.py +551 -0
- fastapi_crud_generator/orm/sqlmodel.py +504 -0
- fastapi_crud_generator/orm/tortoise.py +521 -0
- fastapi_crud_generator/paginator.py +47 -0
- fastapi_crud_generator/schemas.py +43 -0
- fastapi_crud_generator/strategies.py +144 -0
- fastapi_crud_generator/utils.py +138 -0
- fastapi_crud_generator-0.0.1.dist-info/METADATA +109 -0
- fastapi_crud_generator-0.0.1.dist-info/RECORD +17 -0
- fastapi_crud_generator-0.0.1.dist-info/WHEEL +4 -0
- fastapi_crud_generator-0.0.1.dist-info/licenses/LICENSE.txt +21 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
2
|
+
|
|
3
|
+
from fastapi_crud_generator.config import CRUDConfigDict, NestedConfig, RouterKwargs
|
|
4
|
+
from fastapi_crud_generator.crud_generator import (
|
|
5
|
+
CRUDCollection,
|
|
6
|
+
CRUDCollectionBase,
|
|
7
|
+
)
|
|
8
|
+
from fastapi_crud_generator.schemas import PaginatorPage, Message
|
|
9
|
+
from fastapi_crud_generator.orm.base import ORMAdapterBase
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, TypedDict
|
|
4
|
+
|
|
5
|
+
from fastapi.params import Depends
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RouterKwargs(TypedDict, total=False):
|
|
9
|
+
"""Typed subset of ``APIRouter.include_router`` keyword arguments."""
|
|
10
|
+
|
|
11
|
+
tags: list[str]
|
|
12
|
+
dependencies: Sequence[Depends]
|
|
13
|
+
deprecated: bool
|
|
14
|
+
include_in_schema: bool
|
|
15
|
+
responses: dict[int | str, dict[str, Any]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class NestedConfig:
|
|
20
|
+
"""Configuration for a nested collection.
|
|
21
|
+
|
|
22
|
+
Passed as ``config`` to ``add_nested_collection``.
|
|
23
|
+
|
|
24
|
+
``router_kwargs`` accepts any ``APIRouter.include_router`` parameters
|
|
25
|
+
(tags, dependencies, deprecated, include_in_schema, responses).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
router_kwargs: RouterKwargs = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class CRUDConfigDict:
|
|
33
|
+
"""Configuration for automatic CRUD schema generation.
|
|
34
|
+
|
|
35
|
+
Place on a SQLModel table class as ``crud_config`` to control
|
|
36
|
+
how the ORM adapter generates schemas and routes.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
base_fields: Fields shared across schemas; used as a fallback
|
|
40
|
+
when public_fields, create_fields or update_fields are not set.
|
|
41
|
+
public_fields: Fields exposed in the public (GET) schema.
|
|
42
|
+
create_fields: Fields accepted in the create (POST) schema.
|
|
43
|
+
update_fields: Fields accepted in the update (PATCH) schema.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
base_fields: set[str] = field(default_factory=set)
|
|
48
|
+
public_fields: set[str] = field(default_factory=set)
|
|
49
|
+
create_fields: set[str] = field(default_factory=set)
|
|
50
|
+
update_fields: set[str] = field(default_factory=set)
|
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import re
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Annotated, Any, get_type_hints
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
9
|
+
from fastapi.routing import APIRoute
|
|
10
|
+
from pydantic import BaseModel, Field, create_model
|
|
11
|
+
|
|
12
|
+
from fastapi_crud_generator.config import NestedConfig
|
|
13
|
+
from fastapi_crud_generator.deps import (
|
|
14
|
+
ConstantDependency,
|
|
15
|
+
CreateSchemaDependency,
|
|
16
|
+
FilterSchemaDependency,
|
|
17
|
+
IncludeSchemaDependency,
|
|
18
|
+
PaginatorDependency,
|
|
19
|
+
ParentPKFieldsDependency,
|
|
20
|
+
PKFieldsDependency,
|
|
21
|
+
PublicListSchemaDependency,
|
|
22
|
+
PublicSchemaDependency,
|
|
23
|
+
ReplaceSignatureDependency,
|
|
24
|
+
ReplaceWithAnnotationDependency,
|
|
25
|
+
SortSchemaDependency,
|
|
26
|
+
UpdateSchemaDependency,
|
|
27
|
+
)
|
|
28
|
+
from fastapi_crud_generator.orm.base import ORMAdapterBase
|
|
29
|
+
from fastapi_crud_generator.paginator import PaginatorBase
|
|
30
|
+
from fastapi_crud_generator.schemas import (
|
|
31
|
+
NotFoundError,
|
|
32
|
+
PaginatorPage,
|
|
33
|
+
ParentNotFoundError,
|
|
34
|
+
)
|
|
35
|
+
from fastapi_crud_generator.strategies import RouterStrategy, TopLevelStrategy
|
|
36
|
+
from fastapi_crud_generator.utils import (
|
|
37
|
+
create_filter_model,
|
|
38
|
+
create_sort_schema,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_DEFAULT_STRATEGY: RouterStrategy = TopLevelStrategy()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _init_list_param(
|
|
45
|
+
param: list | None,
|
|
46
|
+
class_default: list | None,
|
|
47
|
+
) -> list:
|
|
48
|
+
"""Return param if provided, else fall back to class_default or []."""
|
|
49
|
+
return param if param is not None else (class_default or [])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CRUDCollectionBase(ABC):
|
|
53
|
+
"""Base class for CRUD route collections.
|
|
54
|
+
|
|
55
|
+
Subclass this (or ``CRUDCollection``) to create a set of CRUD endpoints
|
|
56
|
+
for a given ORM adapter. Override handler methods or class attributes to
|
|
57
|
+
customise behaviour without touching routing or dependency wiring.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
orm_adapter: ORMAdapterBase | None = None
|
|
61
|
+
|
|
62
|
+
public_schema: BaseModel = None
|
|
63
|
+
public_list_schema: BaseModel = None
|
|
64
|
+
|
|
65
|
+
update_schema: BaseModel = None
|
|
66
|
+
update_out_schema = Any
|
|
67
|
+
|
|
68
|
+
create_schema: BaseModel = None
|
|
69
|
+
create_out_schema = Any
|
|
70
|
+
|
|
71
|
+
pk_fields: type[BaseModel] | None = None
|
|
72
|
+
|
|
73
|
+
base_fields: set[str] | None = None
|
|
74
|
+
public_fields: set[str] | None = None
|
|
75
|
+
create_fields: set[str] | None = None
|
|
76
|
+
update_fields: set[str] | None = None
|
|
77
|
+
|
|
78
|
+
filter_schema: BaseModel = None
|
|
79
|
+
sort_schema: BaseModel = None
|
|
80
|
+
# include related data
|
|
81
|
+
include_schema: BaseModel = None
|
|
82
|
+
|
|
83
|
+
dependency_overrides: dict[
|
|
84
|
+
type[ReplaceSignatureDependency],
|
|
85
|
+
ReplaceSignatureDependency | Any,
|
|
86
|
+
] | None = None
|
|
87
|
+
|
|
88
|
+
dependencies: list[Depends] | None = None
|
|
89
|
+
get_one_dependencies: list[Depends] | None = None
|
|
90
|
+
get_many_dependencies: list[Depends] | None = None
|
|
91
|
+
create_dependencies: list[Depends] | None = None
|
|
92
|
+
update_dependencies: list[Depends] | None = None
|
|
93
|
+
delete_dependencies: list[Depends] | None = None
|
|
94
|
+
|
|
95
|
+
disable_get_one: bool = False
|
|
96
|
+
disable_get_many: bool = False
|
|
97
|
+
disable_create: bool = False
|
|
98
|
+
disable_update: bool = False
|
|
99
|
+
disable_delete: bool = False
|
|
100
|
+
|
|
101
|
+
router_kwargs: dict | None = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
orm_adapter: ORMAdapterBase = None,
|
|
107
|
+
public_schema: BaseModel | None = None,
|
|
108
|
+
public_list_schema: BaseModel | None = None,
|
|
109
|
+
update_schema: BaseModel | None = None,
|
|
110
|
+
update_out_schema: BaseModel | None = None,
|
|
111
|
+
create_schema: BaseModel | None = None,
|
|
112
|
+
create_out_schema: BaseModel | None = None,
|
|
113
|
+
base_fields: set[str] | None = None,
|
|
114
|
+
public_fields: set[str] | None = None,
|
|
115
|
+
create_fields: set[str] | None = None,
|
|
116
|
+
update_fields: set[str] | None = None,
|
|
117
|
+
|
|
118
|
+
filter_schema: BaseModel | None = None,
|
|
119
|
+
sort_schema: BaseModel | None = None,
|
|
120
|
+
include_schema: BaseModel | None = None,
|
|
121
|
+
|
|
122
|
+
pk_fields: type[BaseModel] | None = None,
|
|
123
|
+
|
|
124
|
+
dependencies: list[Depends] | None = None,
|
|
125
|
+
get_one_dependencies: list[Depends] | None = None,
|
|
126
|
+
get_many_dependencies: list[Depends] | None = None,
|
|
127
|
+
create_dependencies: list[Depends] | None = None,
|
|
128
|
+
update_dependencies: list[Depends] | None = None,
|
|
129
|
+
delete_dependencies: list[Depends] | None = None,
|
|
130
|
+
|
|
131
|
+
disable_get_one: bool | None = None,
|
|
132
|
+
disable_get_many: bool | None = None,
|
|
133
|
+
disable_create: bool | None = None,
|
|
134
|
+
disable_update: bool | None = None,
|
|
135
|
+
disable_delete: bool | None = None,
|
|
136
|
+
|
|
137
|
+
dependency_overrides: dict[
|
|
138
|
+
type[ReplaceSignatureDependency],
|
|
139
|
+
ReplaceSignatureDependency | Any,
|
|
140
|
+
] | None = None,
|
|
141
|
+
|
|
142
|
+
**router_kwargs: dict,
|
|
143
|
+
|
|
144
|
+
):
|
|
145
|
+
"""Initialise the collection, resolving schemas and dependencies."""
|
|
146
|
+
self.orm_adapter = orm_adapter if orm_adapter else self.orm_adapter
|
|
147
|
+
self.verify_orm_adapter()
|
|
148
|
+
|
|
149
|
+
self.base_fields = base_fields or self.base_fields
|
|
150
|
+
self.public_fields = public_fields or self.public_fields
|
|
151
|
+
self.create_fields = create_fields or self.create_fields
|
|
152
|
+
self.update_fields = update_fields or self.update_fields
|
|
153
|
+
|
|
154
|
+
self.public_schema = (
|
|
155
|
+
public_schema
|
|
156
|
+
or self.public_schema
|
|
157
|
+
or self.orm_adapter.generate_public_schema(
|
|
158
|
+
self.public_fields, self.base_fields,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
self.create_schema = create_schema or self.create_schema
|
|
162
|
+
self.update_schema = (
|
|
163
|
+
update_schema
|
|
164
|
+
or self.update_schema
|
|
165
|
+
or self.orm_adapter.generate_update_schema(
|
|
166
|
+
self.update_fields, self.base_fields,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
self.update_out_schema = update_out_schema or self.update_out_schema
|
|
170
|
+
self.create_out_schema = create_out_schema or self.create_out_schema
|
|
171
|
+
self.public_list_schema = (
|
|
172
|
+
public_list_schema or
|
|
173
|
+
self.public_list_schema or
|
|
174
|
+
PaginatorPage[self.public_schema]
|
|
175
|
+
)
|
|
176
|
+
self.pk_fields = (
|
|
177
|
+
pk_fields or self.pk_fields
|
|
178
|
+
or self.apply_pk_aliases(
|
|
179
|
+
self.orm_adapter.generate_pk_schema(),
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
self.filter_schema = filter_schema or self.filter_schema
|
|
183
|
+
if self.filter_schema is None:
|
|
184
|
+
self.filter_schema = create_filter_model(self.public_schema)
|
|
185
|
+
self.sort_schema = sort_schema or self.sort_schema
|
|
186
|
+
if self.sort_schema is None:
|
|
187
|
+
self.sort_schema = create_sort_schema(self.public_schema)
|
|
188
|
+
self.include_schema = include_schema or self.include_schema
|
|
189
|
+
if self.include_schema is None:
|
|
190
|
+
self.include_schema = self.orm_adapter.generate_include_schema()
|
|
191
|
+
|
|
192
|
+
self.dependencies = _init_list_param(
|
|
193
|
+
dependencies, self.dependencies)
|
|
194
|
+
self.get_one_dependencies = _init_list_param(
|
|
195
|
+
get_one_dependencies, self.get_one_dependencies)
|
|
196
|
+
self.get_many_dependencies = _init_list_param(
|
|
197
|
+
get_many_dependencies, self.get_many_dependencies)
|
|
198
|
+
self.create_dependencies = _init_list_param(
|
|
199
|
+
create_dependencies, self.create_dependencies)
|
|
200
|
+
self.update_dependencies = _init_list_param(
|
|
201
|
+
update_dependencies, self.update_dependencies)
|
|
202
|
+
self.delete_dependencies = _init_list_param(
|
|
203
|
+
delete_dependencies, self.delete_dependencies)
|
|
204
|
+
|
|
205
|
+
self.disable_get_one = (disable_get_one if disable_get_one is not None
|
|
206
|
+
else self.disable_get_one)
|
|
207
|
+
self.disable_get_many = (disable_get_many
|
|
208
|
+
if disable_get_many is not None
|
|
209
|
+
else self.disable_get_many)
|
|
210
|
+
self.disable_create = (disable_create if disable_create is not None
|
|
211
|
+
else self.disable_create)
|
|
212
|
+
self.disable_update = (disable_update if disable_update is not None
|
|
213
|
+
else self.disable_update)
|
|
214
|
+
self.disable_delete = (disable_delete if disable_delete is not None
|
|
215
|
+
else self.disable_delete)
|
|
216
|
+
|
|
217
|
+
self.dependency_overrides = (
|
|
218
|
+
dependency_overrides if dependency_overrides is not None
|
|
219
|
+
else (self.dependency_overrides or {})
|
|
220
|
+
)
|
|
221
|
+
self.router_kwargs = (
|
|
222
|
+
router_kwargs if router_kwargs
|
|
223
|
+
else dict(self.router_kwargs or {})
|
|
224
|
+
)
|
|
225
|
+
self.router_kwargs["dependencies"] = self.dependencies
|
|
226
|
+
self._nested_collections: list[
|
|
227
|
+
tuple[str, CRUDCollectionBase]
|
|
228
|
+
] = []
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def apply_pk_aliases(
|
|
232
|
+
self, pk_schema: type[BaseModel],
|
|
233
|
+
) -> type[BaseModel]:
|
|
234
|
+
"""Return pk_schema with model-name-prefixed aliases on plain fields.
|
|
235
|
+
|
|
236
|
+
Fields whose names contain no underscore get an alias
|
|
237
|
+
``{model_name}_{field}`` (e.g. ``id`` → ``user_id``).
|
|
238
|
+
Override to customise aliasing behaviour.
|
|
239
|
+
"""
|
|
240
|
+
model_name = self.orm_adapter.get_model_name()
|
|
241
|
+
if not model_name:
|
|
242
|
+
return pk_schema
|
|
243
|
+
field_defs = {}
|
|
244
|
+
for name, info in pk_schema.model_fields.items():
|
|
245
|
+
alias = name if "_" in name else f"{model_name}_{name}"
|
|
246
|
+
field_defs[name] = (
|
|
247
|
+
info.annotation, Field(validation_alias=alias),
|
|
248
|
+
)
|
|
249
|
+
return create_model(pk_schema.__name__, **field_defs)
|
|
250
|
+
|
|
251
|
+
def verify_orm_adapter(self) -> None:
|
|
252
|
+
"""Raise if orm_adapter is not set."""
|
|
253
|
+
if not self.orm_adapter:
|
|
254
|
+
msg = (
|
|
255
|
+
f"{type(self).__module__}.{type(self).__name__}."
|
|
256
|
+
f"orm_adapter must be set"
|
|
257
|
+
)
|
|
258
|
+
raise ValueError(msg)
|
|
259
|
+
|
|
260
|
+
def get_resolved_overrides(
|
|
261
|
+
self,
|
|
262
|
+
strategy: RouterStrategy | None = None,
|
|
263
|
+
) -> dict[type[ReplaceSignatureDependency], ReplaceSignatureDependency]:
|
|
264
|
+
"""Merge default overrides with user-supplied dependency_overrides."""
|
|
265
|
+
strategy = strategy or _DEFAULT_STRATEGY
|
|
266
|
+
create_schema = self.create_schema or strategy.get_create_schema(
|
|
267
|
+
self.orm_adapter, self.create_fields, self.base_fields,
|
|
268
|
+
)
|
|
269
|
+
defaults = {
|
|
270
|
+
CreateSchemaDependency: CreateSchemaDependency(create_schema),
|
|
271
|
+
UpdateSchemaDependency: UpdateSchemaDependency(self.update_schema),
|
|
272
|
+
PublicSchemaDependency: PublicSchemaDependency(self.public_schema),
|
|
273
|
+
PublicListSchemaDependency: PublicListSchemaDependency(
|
|
274
|
+
self.public_list_schema),
|
|
275
|
+
FilterSchemaDependency: FilterSchemaDependency(self.filter_schema),
|
|
276
|
+
SortSchemaDependency: SortSchemaDependency(self.sort_schema),
|
|
277
|
+
IncludeSchemaDependency: IncludeSchemaDependency(
|
|
278
|
+
self.include_schema),
|
|
279
|
+
PKFieldsDependency: PKFieldsDependency(self.pk_fields),
|
|
280
|
+
PaginatorDependency: PaginatorDependency(
|
|
281
|
+
self.orm_adapter.paginator_class,
|
|
282
|
+
),
|
|
283
|
+
ParentPKFieldsDependency: ConstantDependency([]),
|
|
284
|
+
}
|
|
285
|
+
for key, val in self.dependency_overrides.items():
|
|
286
|
+
defaults[key] = (
|
|
287
|
+
val if isinstance(val, ReplaceSignatureDependency)
|
|
288
|
+
else ReplaceWithAnnotationDependency(val)
|
|
289
|
+
)
|
|
290
|
+
return defaults
|
|
291
|
+
|
|
292
|
+
def override_dependencies( # noqa: C901
|
|
293
|
+
self,
|
|
294
|
+
original_func: Callable,
|
|
295
|
+
overrides: dict | None = None,
|
|
296
|
+
) -> Callable:
|
|
297
|
+
"""Replace marker annotations in a handler's signature with real DI."""
|
|
298
|
+
sig = inspect.signature(original_func)
|
|
299
|
+
|
|
300
|
+
parameters = list(sig.parameters.values())
|
|
301
|
+
new_parameters = []
|
|
302
|
+
seen_names: set[str] = set()
|
|
303
|
+
|
|
304
|
+
if overrides is None:
|
|
305
|
+
overrides = self.get_resolved_overrides()
|
|
306
|
+
|
|
307
|
+
overrided_params: dict[str, ReplaceSignatureDependency] = {}
|
|
308
|
+
|
|
309
|
+
for param in parameters:
|
|
310
|
+
if (getattr(param.annotation, "__metadata__", None) is not None
|
|
311
|
+
and len(param.annotation.__metadata__) > 0
|
|
312
|
+
):
|
|
313
|
+
annotation = param.annotation.__metadata__[0]
|
|
314
|
+
else:
|
|
315
|
+
annotation = param.annotation
|
|
316
|
+
|
|
317
|
+
if (
|
|
318
|
+
inspect.isclass(annotation)
|
|
319
|
+
and issubclass(annotation, (
|
|
320
|
+
ReplaceSignatureDependency
|
|
321
|
+
))
|
|
322
|
+
):
|
|
323
|
+
if annotation not in overrides:
|
|
324
|
+
msg = (
|
|
325
|
+
f"You need to add {annotation.__name__} "
|
|
326
|
+
f"to {type(self).__name__}'s dependency_overrides"
|
|
327
|
+
)
|
|
328
|
+
raise ValueError(msg)
|
|
329
|
+
override = overrides[annotation]
|
|
330
|
+
fresh_params = [
|
|
331
|
+
p for p in override.get_new_params(param)
|
|
332
|
+
if p.name not in seen_names
|
|
333
|
+
]
|
|
334
|
+
seen_names.update(p.name for p in fresh_params)
|
|
335
|
+
new_parameters.extend(fresh_params)
|
|
336
|
+
overrided_params[param.name] = override
|
|
337
|
+
elif param.name not in seen_names:
|
|
338
|
+
seen_names.add(param.name)
|
|
339
|
+
new_parameters.append(param)
|
|
340
|
+
|
|
341
|
+
new_sig = inspect.Signature(parameters=new_parameters)
|
|
342
|
+
handler_params = set(sig.parameters)
|
|
343
|
+
|
|
344
|
+
@wraps(original_func)
|
|
345
|
+
async def wrapper(**kwargs):
|
|
346
|
+
raw_kwargs = dict(kwargs)
|
|
347
|
+
for name, override in overrided_params.items():
|
|
348
|
+
kwargs[name] = override.pack_to_originals(name, **raw_kwargs)
|
|
349
|
+
kwargs = {k: v for k, v in kwargs.items() if k in handler_params}
|
|
350
|
+
return await original_func(**kwargs)
|
|
351
|
+
|
|
352
|
+
wrapper.__signature__ = new_sig
|
|
353
|
+
|
|
354
|
+
# Remove old annotations
|
|
355
|
+
if overrided_params:
|
|
356
|
+
wrapper.__annotations__ = {
|
|
357
|
+
**get_type_hints(original_func),
|
|
358
|
+
**{ p.name: p.annotation for p in new_parameters },
|
|
359
|
+
}
|
|
360
|
+
all_items = list(wrapper.__annotations__.keys())
|
|
361
|
+
for item_name in all_items:
|
|
362
|
+
if item_name not in new_parameters:
|
|
363
|
+
wrapper.__annotations__.pop(item_name, None)
|
|
364
|
+
|
|
365
|
+
return wrapper
|
|
366
|
+
|
|
367
|
+
async def get_one_handler(
|
|
368
|
+
self,
|
|
369
|
+
pk_field_values: Annotated[BaseModel, PKFieldsDependency],
|
|
370
|
+
include_data: Annotated[BaseModel, IncludeSchemaDependency],
|
|
371
|
+
parent_refs: Annotated[list, ParentPKFieldsDependency],
|
|
372
|
+
) -> object:
|
|
373
|
+
"""Return a single object or call get_one_not_found if missing."""
|
|
374
|
+
result = await self.orm_adapter.get_one(
|
|
375
|
+
pk_field_values, include_data, parent_refs=parent_refs,
|
|
376
|
+
)
|
|
377
|
+
if result is None:
|
|
378
|
+
return await self.get_one_not_found(pk_field_values, include_data)
|
|
379
|
+
return result
|
|
380
|
+
|
|
381
|
+
async def get_one_not_found(
|
|
382
|
+
self, _pk_field_values: BaseModel, _include_data: BaseModel,
|
|
383
|
+
) -> object:
|
|
384
|
+
"""Raise 404 by default; override to customise not-found behavior."""
|
|
385
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
386
|
+
|
|
387
|
+
async def get_many_handler(
|
|
388
|
+
self,
|
|
389
|
+
paginator: Annotated[PaginatorBase, PaginatorDependency],
|
|
390
|
+
filter_data: Annotated[BaseModel, FilterSchemaDependency],
|
|
391
|
+
sort_data: Annotated[BaseModel, SortSchemaDependency],
|
|
392
|
+
include_data: Annotated[BaseModel, IncludeSchemaDependency],
|
|
393
|
+
parent_refs: Annotated[list, ParentPKFieldsDependency],
|
|
394
|
+
) -> object:
|
|
395
|
+
"""Handle GET / — return a paginated, filtered, sorted list."""
|
|
396
|
+
return await self.orm_adapter.get_many(
|
|
397
|
+
filter_data, sort_data, include_data, paginator,
|
|
398
|
+
parent_refs=parent_refs,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
async def create_one_handler(
|
|
402
|
+
self,
|
|
403
|
+
create_data: Annotated[BaseModel, CreateSchemaDependency],
|
|
404
|
+
parent_refs: Annotated[list, ParentPKFieldsDependency],
|
|
405
|
+
) -> object:
|
|
406
|
+
"""Handle POST / — persist a new object and return it."""
|
|
407
|
+
try:
|
|
408
|
+
return await self.orm_adapter.create_one(
|
|
409
|
+
create_data, parent_refs=parent_refs,
|
|
410
|
+
)
|
|
411
|
+
except ParentNotFoundError as exc:
|
|
412
|
+
raise HTTPException(
|
|
413
|
+
status_code=404, detail="Parent not found",
|
|
414
|
+
) from exc
|
|
415
|
+
|
|
416
|
+
async def update_one_handler(
|
|
417
|
+
self,
|
|
418
|
+
pk_field_values: Annotated[BaseModel, PKFieldsDependency],
|
|
419
|
+
update_data: Annotated[BaseModel, UpdateSchemaDependency],
|
|
420
|
+
parent_refs: Annotated[list, ParentPKFieldsDependency],
|
|
421
|
+
) -> None:
|
|
422
|
+
"""Handle PATCH /{pk} — partially update an existing object."""
|
|
423
|
+
try:
|
|
424
|
+
await self.orm_adapter.update_one(
|
|
425
|
+
pk_field_values, update_data, parent_refs=parent_refs,
|
|
426
|
+
)
|
|
427
|
+
except NotFoundError as exc:
|
|
428
|
+
raise HTTPException(status_code=404, detail="Not found") from exc
|
|
429
|
+
|
|
430
|
+
async def delete_one_handler(
|
|
431
|
+
self,
|
|
432
|
+
pk_field_values: Annotated[BaseModel, PKFieldsDependency],
|
|
433
|
+
parent_refs: Annotated[list, ParentPKFieldsDependency],
|
|
434
|
+
) -> object:
|
|
435
|
+
"""Handle DELETE /{pk} — delete an object by primary key."""
|
|
436
|
+
result = await self.orm_adapter.delete_one(
|
|
437
|
+
pk_field_values, parent_refs=parent_refs,
|
|
438
|
+
)
|
|
439
|
+
if result is None:
|
|
440
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
441
|
+
return result
|
|
442
|
+
|
|
443
|
+
def generate_unique_id(self, route: APIRoute) -> str:
|
|
444
|
+
"""Build a unique OpenAPI operation ID from name, path and method."""
|
|
445
|
+
if not route.methods:
|
|
446
|
+
msg = f"Route {route.name!r} has no HTTP methods defined"
|
|
447
|
+
raise ValueError(msg)
|
|
448
|
+
operation_id = re.sub(r"\W", "_", f"{route.name}{route.path_format}")
|
|
449
|
+
method = next(iter(route.methods)).lower()
|
|
450
|
+
return f"{operation_id}_{method}_{type(self.public_schema).__name__}"
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def id_path(self) -> str:
|
|
454
|
+
"""URL path segment for single-object endpoints, e.g. /{user_id}."""
|
|
455
|
+
return "/" + "/".join(
|
|
456
|
+
f"{{{f.validation_alias or n}}}"
|
|
457
|
+
for n, f in self.pk_fields.model_fields.items()
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def add_get_many_route(
|
|
461
|
+
self, router: APIRouter, strategy: RouterStrategy | None = None,
|
|
462
|
+
) -> None:
|
|
463
|
+
"""Register GET / on the router."""
|
|
464
|
+
strategy = strategy or _DEFAULT_STRATEGY
|
|
465
|
+
overrides = strategy.get_overrides(self.get_resolved_overrides(strategy))
|
|
466
|
+
if self.public_schema and not self.disable_get_many:
|
|
467
|
+
router.add_api_route(
|
|
468
|
+
"",
|
|
469
|
+
self.override_dependencies(self.get_many_handler, overrides),
|
|
470
|
+
response_model=self.public_list_schema,
|
|
471
|
+
dependencies=self.get_many_dependencies,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
def add_get_one_route(
|
|
475
|
+
self, router: APIRouter, strategy: RouterStrategy | None = None,
|
|
476
|
+
) -> None:
|
|
477
|
+
"""Register GET /{pk} on the router."""
|
|
478
|
+
strategy = strategy or _DEFAULT_STRATEGY
|
|
479
|
+
overrides = strategy.get_overrides(self.get_resolved_overrides(strategy))
|
|
480
|
+
if self.public_schema and not self.disable_get_one:
|
|
481
|
+
router.add_api_route(
|
|
482
|
+
strategy.compute_id_path(self.pk_fields),
|
|
483
|
+
self.override_dependencies(self.get_one_handler, overrides),
|
|
484
|
+
response_model=self.public_schema,
|
|
485
|
+
generate_unique_id_function=self.generate_unique_id,
|
|
486
|
+
dependencies=self.get_one_dependencies,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
def add_create_one_route(
|
|
490
|
+
self, router: APIRouter, strategy: RouterStrategy | None = None,
|
|
491
|
+
) -> None:
|
|
492
|
+
"""Register POST / on the router."""
|
|
493
|
+
strategy = strategy or _DEFAULT_STRATEGY
|
|
494
|
+
overrides = strategy.get_overrides(self.get_resolved_overrides(strategy))
|
|
495
|
+
if not self.disable_create:
|
|
496
|
+
router.add_api_route(
|
|
497
|
+
"",
|
|
498
|
+
self.override_dependencies(
|
|
499
|
+
self.create_one_handler, overrides,
|
|
500
|
+
),
|
|
501
|
+
methods=["POST"],
|
|
502
|
+
response_model=self.create_out_schema,
|
|
503
|
+
dependencies=self.create_dependencies,
|
|
504
|
+
responses={404: {"description": "Parent not found"}},
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def add_update_one_route(
|
|
508
|
+
self, router: APIRouter, strategy: RouterStrategy | None = None,
|
|
509
|
+
) -> None:
|
|
510
|
+
"""Register PATCH /{pk} on the router."""
|
|
511
|
+
strategy = strategy or _DEFAULT_STRATEGY
|
|
512
|
+
overrides = strategy.get_overrides(self.get_resolved_overrides(strategy))
|
|
513
|
+
if self.update_schema and not self.disable_update:
|
|
514
|
+
router.add_api_route(
|
|
515
|
+
strategy.compute_id_path(self.pk_fields),
|
|
516
|
+
self.override_dependencies(
|
|
517
|
+
self.update_one_handler, overrides,
|
|
518
|
+
),
|
|
519
|
+
methods=["PATCH"],
|
|
520
|
+
response_model=self.update_out_schema,
|
|
521
|
+
dependencies=self.update_dependencies,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def add_delete_one_route(
|
|
525
|
+
self, router: APIRouter, strategy: RouterStrategy | None = None,
|
|
526
|
+
) -> None:
|
|
527
|
+
"""Register DELETE /{pk} on the router."""
|
|
528
|
+
strategy = strategy or _DEFAULT_STRATEGY
|
|
529
|
+
overrides = strategy.get_overrides(self.get_resolved_overrides(strategy))
|
|
530
|
+
if not self.disable_delete:
|
|
531
|
+
router.add_api_route(
|
|
532
|
+
strategy.compute_id_path(self.pk_fields),
|
|
533
|
+
self.override_dependencies(
|
|
534
|
+
self.delete_one_handler, overrides,
|
|
535
|
+
),
|
|
536
|
+
methods=["DELETE"],
|
|
537
|
+
response_model=self.public_schema,
|
|
538
|
+
dependencies=self.delete_dependencies,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
def add_nested_collection(
|
|
542
|
+
self,
|
|
543
|
+
path: str,
|
|
544
|
+
child: "CRUDCollectionBase",
|
|
545
|
+
config: NestedConfig | None = None,
|
|
546
|
+
) -> None:
|
|
547
|
+
"""Register child as a nested collection under this collection."""
|
|
548
|
+
self._nested_collections.append(
|
|
549
|
+
(path, child, config or NestedConfig()),
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def get_router(
|
|
553
|
+
self,
|
|
554
|
+
strategy: RouterStrategy | None = None,
|
|
555
|
+
**router_kwargs: object,
|
|
556
|
+
) -> APIRouter:
|
|
557
|
+
"""Build and return an APIRouter with all enabled CRUD routes."""
|
|
558
|
+
strategy = strategy or _DEFAULT_STRATEGY
|
|
559
|
+
self.verify_orm_adapter()
|
|
560
|
+
effective_kwargs = {**self.router_kwargs, **router_kwargs}
|
|
561
|
+
router = APIRouter(**effective_kwargs)
|
|
562
|
+
|
|
563
|
+
self.add_get_many_route(router, strategy)
|
|
564
|
+
self.add_get_one_route(router, strategy)
|
|
565
|
+
self.add_create_one_route(router, strategy)
|
|
566
|
+
self.add_update_one_route(router, strategy)
|
|
567
|
+
self.add_delete_one_route(router, strategy)
|
|
568
|
+
|
|
569
|
+
if self._nested_collections:
|
|
570
|
+
overrides = strategy.get_overrides(self.get_resolved_overrides(strategy))
|
|
571
|
+
child_strategy = strategy.get_child_strategy(
|
|
572
|
+
pk_dep=overrides[PKFieldsDependency],
|
|
573
|
+
model=self.orm_adapter.model,
|
|
574
|
+
)
|
|
575
|
+
id_path = strategy.compute_id_path(self.pk_fields)
|
|
576
|
+
for path, child, cfg in self._nested_collections:
|
|
577
|
+
prefix = id_path.rstrip("/") + "/" + path.lstrip("/")
|
|
578
|
+
router.include_router(
|
|
579
|
+
child.get_router(strategy=child_strategy),
|
|
580
|
+
prefix=prefix,
|
|
581
|
+
**cfg.router_kwargs,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
return router
|
|
585
|
+
|
|
586
|
+
class CRUDCollection(CRUDCollectionBase):
|
|
587
|
+
"""Concrete CRUD collection — subclass and set orm_adapter to use."""
|