django-ninja-aio-crud 1.0.5__tar.gz → 2.0.0rc1__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.
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/PKG-INFO +2 -2
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/api.py +0 -2
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/auth.py +3 -4
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/decorators.py +3 -0
- django_ninja_aio_crud-2.0.0rc1/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/helpers/api.py +11 -6
- django_ninja_aio_crud-2.0.0rc1/ninja_aio/helpers/query.py +103 -0
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/models.py +504 -168
- django_ninja_aio_crud-2.0.0rc1/ninja_aio/schemas/__init__.py +23 -0
- django_ninja_aio_crud-2.0.0rc1/ninja_aio/schemas/api.py +24 -0
- django_ninja_aio_crud-2.0.0rc1/ninja_aio/schemas/generics.py +5 -0
- django_ninja_aio_crud-1.0.5/ninja_aio/schemas.py → django_ninja_aio_crud-2.0.0rc1/ninja_aio/schemas/helpers.py +31 -31
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/views.py +33 -16
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/pyproject.toml +1 -1
- django_ninja_aio_crud-1.0.5/ninja_aio/helpers/__init__.py +0 -3
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/LICENSE +0 -0
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/README.md +0 -0
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-ninja-aio-crud
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0rc1
|
|
4
4
|
Summary: Django Ninja AIO CRUD - Rest Framework
|
|
5
5
|
Author: Giuseppe Casillo
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -23,7 +23,7 @@ Classifier: Framework :: AsyncIO
|
|
|
23
23
|
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
24
24
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
25
|
License-File: LICENSE
|
|
26
|
-
Requires-Dist: django-ninja >=1.3.0, <=1.
|
|
26
|
+
Requires-Dist: django-ninja >=1.3.0, <=1.5.0
|
|
27
27
|
Requires-Dist: joserfc >=1.0.0, <= 1.4.1
|
|
28
28
|
Requires-Dist: orjson >= 3.10.7, <= 3.11.4
|
|
29
29
|
Requires-Dist: coverage ; extra == "test"
|
|
@@ -23,7 +23,6 @@ class NinjaAIO(NinjaAPI):
|
|
|
23
23
|
docs_decorator=None,
|
|
24
24
|
servers: list[dict[str, Any]] | None = None,
|
|
25
25
|
urls_namespace: str | None = None,
|
|
26
|
-
csrf: bool = False,
|
|
27
26
|
auth: Sequence[Any] | NOT_SET_TYPE = NOT_SET,
|
|
28
27
|
throttle: BaseThrottle | list[BaseThrottle] | NOT_SET_TYPE = NOT_SET,
|
|
29
28
|
default_router: Router | None = None,
|
|
@@ -39,7 +38,6 @@ class NinjaAIO(NinjaAPI):
|
|
|
39
38
|
docs_decorator=docs_decorator,
|
|
40
39
|
servers=servers,
|
|
41
40
|
urls_namespace=urls_namespace,
|
|
42
|
-
csrf=csrf,
|
|
43
41
|
auth=auth,
|
|
44
42
|
throttle=throttle,
|
|
45
43
|
default_router=default_router,
|
|
@@ -2,8 +2,6 @@ from joserfc import jwt, jwk, errors
|
|
|
2
2
|
from django.http.request import HttpRequest
|
|
3
3
|
from ninja.security.http import HttpBearer
|
|
4
4
|
|
|
5
|
-
from .exceptions import AuthError
|
|
6
|
-
|
|
7
5
|
|
|
8
6
|
class AsyncJwtBearer(HttpBearer):
|
|
9
7
|
"""
|
|
@@ -71,6 +69,7 @@ class AsyncJwtBearer(HttpBearer):
|
|
|
71
69
|
Return Semantics:
|
|
72
70
|
- authenticate -> user object (success) | False (failure)
|
|
73
71
|
"""
|
|
72
|
+
|
|
74
73
|
jwt_public: jwk.RSAKey
|
|
75
74
|
claims: dict[str, dict]
|
|
76
75
|
algorithms: list[str] = ["RS256"]
|
|
@@ -96,13 +95,13 @@ class AsyncJwtBearer(HttpBearer):
|
|
|
96
95
|
"""
|
|
97
96
|
try:
|
|
98
97
|
self.dcd = jwt.decode(token, self.jwt_public, algorithms=self.algorithms)
|
|
99
|
-
except ValueError
|
|
98
|
+
except ValueError:
|
|
100
99
|
# raise AuthError(", ".join(exc.args), 401)
|
|
101
100
|
return False
|
|
102
101
|
|
|
103
102
|
try:
|
|
104
103
|
self.validate_claims(self.dcd.claims)
|
|
105
|
-
except errors.JoseError
|
|
104
|
+
except errors.JoseError:
|
|
106
105
|
return False
|
|
107
106
|
|
|
108
107
|
return await self.auth_handler(request)
|
|
@@ -46,10 +46,12 @@ def aatomic(func):
|
|
|
46
46
|
your async ORM / database backend.
|
|
47
47
|
- Only use on async functions.
|
|
48
48
|
"""
|
|
49
|
+
|
|
49
50
|
@wraps(func)
|
|
50
51
|
async def wrapper(*args, **kwargs):
|
|
51
52
|
async with AsyncAtomicContextManager():
|
|
52
53
|
return await func(*args, **kwargs)
|
|
54
|
+
|
|
53
55
|
return wrapper
|
|
54
56
|
|
|
55
57
|
|
|
@@ -117,6 +119,7 @@ def unique_view(self: object | str, plural: bool = False):
|
|
|
117
119
|
- Ensure that the modified name does not conflict with other functions after decoration.
|
|
118
120
|
- Use cautiously when decorators relying on original __name__ appear earlier in the chain.
|
|
119
121
|
"""
|
|
122
|
+
|
|
120
123
|
def decorator(func):
|
|
121
124
|
# Allow usage as unique_view(self_instance) or unique_view("model_name")
|
|
122
125
|
if isinstance(self, str):
|
|
File without changes
|
|
@@ -6,6 +6,7 @@ from ninja import Path, Query
|
|
|
6
6
|
from ninja.pagination import paginate
|
|
7
7
|
from ninja_aio.decorators import unique_view
|
|
8
8
|
from ninja_aio.models import ModelSerializer, ModelUtil
|
|
9
|
+
from ninja_aio.schemas.helpers import ObjectsQuerySchema
|
|
9
10
|
from ninja_aio.schemas import (
|
|
10
11
|
GenericMessageSchema,
|
|
11
12
|
M2MRelationSchema,
|
|
@@ -260,7 +261,9 @@ class ManyToManyAPI:
|
|
|
260
261
|
rel_model_name = model._meta.verbose_name.capitalize()
|
|
261
262
|
for obj_pk in objs_pks:
|
|
262
263
|
rel_obj = await (
|
|
263
|
-
await ModelUtil(model).
|
|
264
|
+
await ModelUtil(model).get_objects(
|
|
265
|
+
request, query_data=ObjectsQuerySchema(filters={"pk": obj_pk})
|
|
266
|
+
)
|
|
264
267
|
).afirst()
|
|
265
268
|
if rel_obj is None:
|
|
266
269
|
errors.append(f"{rel_model_name} with pk {obj_pk} not found.")
|
|
@@ -325,12 +328,14 @@ class ManyToManyAPI:
|
|
|
325
328
|
|
|
326
329
|
query_handler = self._get_query_handler(related_name)
|
|
327
330
|
if filters is not None and query_handler:
|
|
328
|
-
|
|
331
|
+
if asyncio.iscoroutinefunction(query_handler):
|
|
332
|
+
related_qs = await query_handler(related_qs, filters.model_dump())
|
|
333
|
+
else:
|
|
334
|
+
related_qs = query_handler(related_qs, filters.model_dump())
|
|
329
335
|
|
|
330
|
-
return
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
]
|
|
336
|
+
return await rel_util.list_read_s(
|
|
337
|
+
related_schema, request, related_qs
|
|
338
|
+
)
|
|
334
339
|
|
|
335
340
|
def _resolve_action_schema(self, add: bool, remove: bool):
|
|
336
341
|
return self.views_action_map[(add, remove)]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from ninja_aio.types import ModelSerializerMeta
|
|
2
|
+
from ninja_aio.schemas.helpers import (
|
|
3
|
+
ModelQuerySetSchema,
|
|
4
|
+
QueryUtilBaseScopesSchema,
|
|
5
|
+
ModelQuerySetExtraSchema,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ScopeNamespace:
|
|
10
|
+
def __init__(self, **scopes):
|
|
11
|
+
for key, value in scopes.items():
|
|
12
|
+
setattr(self, key, value)
|
|
13
|
+
|
|
14
|
+
def __iter__(self):
|
|
15
|
+
return iter(self.__dict__.values())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QueryUtil:
|
|
19
|
+
"""
|
|
20
|
+
Helper class to manage queryset optimizations based on predefined scopes.
|
|
21
|
+
Attributes:
|
|
22
|
+
model (ModelSerializerMeta): The model serializer meta to which this utility is attached.
|
|
23
|
+
SCOPES (ScopeNamespace): An enumeration-like object containing available scopes.
|
|
24
|
+
read_config (ModelQuerySetSchema): Configuration for the 'read' scope.
|
|
25
|
+
queryset_request_config (ModelQuerySetSchema): Configuration for the 'queryset_request' scope
|
|
26
|
+
extra_configs (dict): Additional configurations for custom scopes.
|
|
27
|
+
Methods:
|
|
28
|
+
apply_queryset_optimizations(queryset, scope): Applies select_related and prefetch_related
|
|
29
|
+
optimizations to the given queryset based on the specified scope.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
query_util = QueryUtil(MyModelSerializer) or MyModel.query_util
|
|
33
|
+
qs = MyModel.objects.all()
|
|
34
|
+
optimized_qs = query_util.apply_queryset_optimizations(qs, query_util.SCOPES.READ)
|
|
35
|
+
|
|
36
|
+
# Applying optimizations for a custom scope
|
|
37
|
+
class MyModelSerializer(ModelSerializer):
|
|
38
|
+
class QuerySet:
|
|
39
|
+
extras = [
|
|
40
|
+
ModelQuerySetExtraSchema(
|
|
41
|
+
scope="custom_scope",
|
|
42
|
+
select_related=["custom_fk_field"],
|
|
43
|
+
prefetch_related=["custom_m2m_field"],
|
|
44
|
+
)
|
|
45
|
+
]
|
|
46
|
+
query_util = MyModelSerializer.query_util
|
|
47
|
+
qs = MyModelSerializer.objects.all()
|
|
48
|
+
optimized_qs_custom = query_util.apply_queryset_optimizations(qs, "custom_scope")
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
SCOPES: QueryUtilBaseScopesSchema
|
|
52
|
+
|
|
53
|
+
def __init__(self, model: ModelSerializerMeta):
|
|
54
|
+
self.model = model
|
|
55
|
+
self._configuration = getattr(self.model, "QuerySet", None)
|
|
56
|
+
self._extra_configuration: list[ModelQuerySetExtraSchema] = getattr(
|
|
57
|
+
self._configuration, "extras", []
|
|
58
|
+
)
|
|
59
|
+
self._BASE_SCOPES = QueryUtilBaseScopesSchema().model_dump()
|
|
60
|
+
self.SCOPES = ScopeNamespace(
|
|
61
|
+
**self._BASE_SCOPES,
|
|
62
|
+
**{extra.scope: extra.scope for extra in self._extra_configuration},
|
|
63
|
+
)
|
|
64
|
+
self.extra_configs = {extra.scope: extra for extra in self._extra_configuration}
|
|
65
|
+
self._configs = {
|
|
66
|
+
**{scope: self._get_config(scope) for scope in self._BASE_SCOPES.values()},
|
|
67
|
+
**self.extra_configs,
|
|
68
|
+
}
|
|
69
|
+
self.read_config: ModelQuerySetSchema = self._configs.get(self.SCOPES.READ, ModelQuerySetSchema())
|
|
70
|
+
self.queryset_request_config: ModelQuerySetSchema = self._configs.get(
|
|
71
|
+
self.SCOPES.QUERYSET_REQUEST, ModelQuerySetSchema()
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _get_config(self, conf_name: str) -> ModelQuerySetSchema:
|
|
75
|
+
"""Helper method to retrieve configuration attributes."""
|
|
76
|
+
return getattr(self._configuration, conf_name, ModelQuerySetSchema())
|
|
77
|
+
|
|
78
|
+
def apply_queryset_optimizations(self, queryset, scope: str):
|
|
79
|
+
"""
|
|
80
|
+
Apply select_related and prefetch_related optimizations to the queryset
|
|
81
|
+
according to the specified scope.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
queryset (QuerySet): The Django queryset to optimize.
|
|
85
|
+
scope (str): The scope to apply. Must be in self.SCOPES.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
QuerySet: The optimized queryset.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If the given scope is not supported.
|
|
92
|
+
"""
|
|
93
|
+
if scope not in self._configs:
|
|
94
|
+
valid_scopes = list(self._configs.keys())
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Invalid scope '{scope}' for QueryUtil. Supported scopes: {valid_scopes}"
|
|
97
|
+
)
|
|
98
|
+
config = self._configs.get(scope, ModelQuerySetSchema())
|
|
99
|
+
if config.select_related:
|
|
100
|
+
queryset = queryset.select_related(*config.select_related)
|
|
101
|
+
if config.prefetch_related:
|
|
102
|
+
queryset = queryset.prefetch_related(*config.prefetch_related)
|
|
103
|
+
return queryset
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import base64
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, ClassVar
|
|
4
4
|
|
|
5
5
|
from ninja import Schema
|
|
6
|
-
from ninja.orm import create_schema
|
|
6
|
+
from ninja.orm import create_schema, fields
|
|
7
|
+
from ninja.errors import ConfigError
|
|
7
8
|
|
|
8
9
|
from django.db import models
|
|
9
10
|
from django.http import HttpRequest
|
|
@@ -17,8 +18,16 @@ from django.db.models.fields.related_descriptors import (
|
|
|
17
18
|
ForwardOneToOneDescriptor,
|
|
18
19
|
)
|
|
19
20
|
|
|
20
|
-
from .exceptions import SerializeError, NotFoundError
|
|
21
|
-
from .types import S_TYPES, F_TYPES, SCHEMA_TYPES, ModelSerializerMeta
|
|
21
|
+
from ninja_aio.exceptions import SerializeError, NotFoundError
|
|
22
|
+
from ninja_aio.types import S_TYPES, F_TYPES, SCHEMA_TYPES, ModelSerializerMeta
|
|
23
|
+
from ninja_aio.schemas.helpers import (
|
|
24
|
+
ModelQuerySetSchema,
|
|
25
|
+
ModelQuerySetExtraSchema,
|
|
26
|
+
QuerySchema,
|
|
27
|
+
ObjectQuerySchema,
|
|
28
|
+
ObjectsQuerySchema,
|
|
29
|
+
)
|
|
30
|
+
from ninja_aio.helpers.query import QueryUtil
|
|
22
31
|
|
|
23
32
|
|
|
24
33
|
async def agetattr(obj, name: str, default=None):
|
|
@@ -97,6 +106,37 @@ class ModelUtil:
|
|
|
97
106
|
"""
|
|
98
107
|
self.model = model
|
|
99
108
|
|
|
109
|
+
@property
|
|
110
|
+
def pk_field_type(self):
|
|
111
|
+
"""
|
|
112
|
+
Python type corresponding to the model's primary key field.
|
|
113
|
+
|
|
114
|
+
Resolution
|
|
115
|
+
----------
|
|
116
|
+
Uses the Django field's internal type and ninja.orm.fields.TYPES mapping.
|
|
117
|
+
If the internal type is unknown, instructs how to register a custom mapping.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
type
|
|
122
|
+
Native Python type for the PK suitable for schema generation.
|
|
123
|
+
|
|
124
|
+
Raises
|
|
125
|
+
------
|
|
126
|
+
ConfigError
|
|
127
|
+
If the internal type is not registered in ninja.orm.fields.TYPES.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
internal_type = self.model._meta.pk.get_internal_type()
|
|
131
|
+
return fields.TYPES[internal_type]
|
|
132
|
+
except KeyError as e:
|
|
133
|
+
msg = [
|
|
134
|
+
f"Do not know how to convert django field '{internal_type}'.",
|
|
135
|
+
"Try: from ninja.orm import register_field",
|
|
136
|
+
"register_field('{internal_type}', <your-python-type>)",
|
|
137
|
+
]
|
|
138
|
+
raise ConfigError("\n".join(msg)) from e
|
|
139
|
+
|
|
100
140
|
@property
|
|
101
141
|
def serializable_fields(self):
|
|
102
142
|
"""
|
|
@@ -175,61 +215,156 @@ class ModelUtil:
|
|
|
175
215
|
"""
|
|
176
216
|
return self.model_verbose_name_plural.replace(" ", "")
|
|
177
217
|
|
|
218
|
+
async def _get_base_queryset(
|
|
219
|
+
self,
|
|
220
|
+
request: HttpRequest,
|
|
221
|
+
query_data: QuerySchema,
|
|
222
|
+
with_qs_request: bool,
|
|
223
|
+
is_for_read: bool,
|
|
224
|
+
) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
|
|
225
|
+
"""
|
|
226
|
+
Build base queryset with optimizations and filters.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
request : HttpRequest
|
|
231
|
+
The HTTP request object.
|
|
232
|
+
query_data : QuerySchema
|
|
233
|
+
Query configuration with filters and optimizations.
|
|
234
|
+
with_qs_request : bool
|
|
235
|
+
Whether to apply queryset_request hook.
|
|
236
|
+
is_for_read : bool
|
|
237
|
+
Whether this is a read operation.
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
models.QuerySet
|
|
242
|
+
Optimized and filtered queryset.
|
|
243
|
+
"""
|
|
244
|
+
# Start with base queryset
|
|
245
|
+
obj_qs = self.model.objects.all()
|
|
246
|
+
|
|
247
|
+
# Apply query optimizations
|
|
248
|
+
obj_qs = self._apply_query_optimizations(obj_qs, query_data, is_for_read)
|
|
249
|
+
|
|
250
|
+
# Apply queryset_request hook if available
|
|
251
|
+
if isinstance(self.model, ModelSerializerMeta) and with_qs_request:
|
|
252
|
+
obj_qs = await self.model.queryset_request(request)
|
|
253
|
+
|
|
254
|
+
# Apply filters if present
|
|
255
|
+
if hasattr(query_data, "filters") and query_data.filters:
|
|
256
|
+
obj_qs = obj_qs.filter(**query_data.filters)
|
|
257
|
+
|
|
258
|
+
return obj_qs
|
|
259
|
+
|
|
260
|
+
async def get_objects(
|
|
261
|
+
self,
|
|
262
|
+
request: HttpRequest,
|
|
263
|
+
query_data: ObjectsQuerySchema = None,
|
|
264
|
+
with_qs_request=True,
|
|
265
|
+
is_for_read: bool = False,
|
|
266
|
+
) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
|
|
267
|
+
"""
|
|
268
|
+
Retrieve a queryset with optimized database queries.
|
|
269
|
+
|
|
270
|
+
This method fetches a queryset applying query optimizations including
|
|
271
|
+
select_related and prefetch_related based on the model's relationships
|
|
272
|
+
and the query parameters.
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
request : HttpRequest
|
|
277
|
+
The HTTP request object, used for queryset_request hooks.
|
|
278
|
+
query_data : ObjectsQuerySchema, optional
|
|
279
|
+
Schema containing filters and query optimization parameters.
|
|
280
|
+
Defaults to an empty ObjectsQuerySchema instance.
|
|
281
|
+
with_qs_request : bool, optional
|
|
282
|
+
Whether to apply the model's queryset_request hook if available.
|
|
283
|
+
Defaults to True.
|
|
284
|
+
is_for_read : bool, optional
|
|
285
|
+
Flag indicating if the query is for read operations, which may affect
|
|
286
|
+
query optimization strategies. Defaults to False.
|
|
287
|
+
|
|
288
|
+
Returns
|
|
289
|
+
-------
|
|
290
|
+
models.QuerySet[type["ModelSerializer"] | models.Model]
|
|
291
|
+
A QuerySet of model instances.
|
|
292
|
+
|
|
293
|
+
Notes
|
|
294
|
+
-----
|
|
295
|
+
- Query optimizations are automatically applied based on discovered relationships
|
|
296
|
+
- The queryset_request hook is called if the model implements ModelSerializerMeta
|
|
297
|
+
"""
|
|
298
|
+
if query_data is None:
|
|
299
|
+
query_data = ObjectsQuerySchema()
|
|
300
|
+
|
|
301
|
+
return await self._get_base_queryset(
|
|
302
|
+
request, query_data, with_qs_request, is_for_read
|
|
303
|
+
)
|
|
304
|
+
|
|
178
305
|
async def get_object(
|
|
179
306
|
self,
|
|
180
307
|
request: HttpRequest,
|
|
181
308
|
pk: int | str = None,
|
|
182
|
-
|
|
183
|
-
getters: dict = None,
|
|
309
|
+
query_data: ObjectQuerySchema = None,
|
|
184
310
|
with_qs_request=True,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
| models.Model
|
|
188
|
-
| models.QuerySet[type["ModelSerializer"] | models.Model]
|
|
189
|
-
):
|
|
311
|
+
is_for_read: bool = False,
|
|
312
|
+
) -> type["ModelSerializer"] | models.Model:
|
|
190
313
|
"""
|
|
191
|
-
Retrieve a single
|
|
314
|
+
Retrieve a single object with optimized database queries.
|
|
192
315
|
|
|
193
|
-
|
|
194
|
-
prefetch_related on
|
|
316
|
+
This method handles single-object retrieval with automatic query optimizations
|
|
317
|
+
including select_related and prefetch_related based on the model's relationships
|
|
318
|
+
and the query parameters.
|
|
195
319
|
|
|
196
320
|
Parameters
|
|
197
321
|
----------
|
|
198
322
|
request : HttpRequest
|
|
323
|
+
The HTTP request object, used for queryset_request hooks.
|
|
199
324
|
pk : int | str, optional
|
|
200
|
-
Primary key lookup.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
325
|
+
Primary key value for single object lookup. Defaults to None.
|
|
326
|
+
query_data : ObjectQuerySchema, optional
|
|
327
|
+
Schema containing getters and query optimization parameters.
|
|
328
|
+
Defaults to an empty ObjectQuerySchema instance.
|
|
329
|
+
with_qs_request : bool, optional
|
|
330
|
+
Whether to apply the model's queryset_request hook if available.
|
|
331
|
+
Defaults to True.
|
|
332
|
+
is_for_read : bool, optional
|
|
333
|
+
Flag indicating if the query is for read operations, which may affect
|
|
334
|
+
query optimization strategies. Defaults to False.
|
|
207
335
|
|
|
208
336
|
Returns
|
|
209
337
|
-------
|
|
210
|
-
|
|
211
|
-
|
|
338
|
+
type["ModelSerializer"] | models.Model
|
|
339
|
+
A single model instance.
|
|
212
340
|
|
|
213
341
|
Raises
|
|
214
342
|
------
|
|
343
|
+
ValueError
|
|
344
|
+
If neither pk nor getters are provided.
|
|
215
345
|
NotFoundError
|
|
216
|
-
If
|
|
217
|
-
"""
|
|
218
|
-
get_q = {self.model_pk_name: pk} if pk is not None else {}
|
|
219
|
-
if getters:
|
|
220
|
-
get_q |= getters
|
|
346
|
+
If no matching object exists in the database.
|
|
221
347
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
348
|
+
Notes
|
|
349
|
+
-----
|
|
350
|
+
- Query optimizations are automatically applied based on discovered relationships
|
|
351
|
+
- The queryset_request hook is called if the model implements ModelSerializerMeta
|
|
352
|
+
"""
|
|
353
|
+
if query_data is None:
|
|
354
|
+
query_data = ObjectQuerySchema()
|
|
225
355
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
356
|
+
if not query_data.getters and pk is None:
|
|
357
|
+
raise ValueError(
|
|
358
|
+
"Either pk or getters must be provided for single object retrieval."
|
|
359
|
+
)
|
|
229
360
|
|
|
230
|
-
|
|
231
|
-
|
|
361
|
+
# Build lookup query and get optimized queryset
|
|
362
|
+
get_q = self._build_lookup_query(pk, query_data.getters)
|
|
363
|
+
obj_qs = await self._get_base_queryset(
|
|
364
|
+
request, query_data, with_qs_request, is_for_read
|
|
365
|
+
)
|
|
232
366
|
|
|
367
|
+
# Perform lookup
|
|
233
368
|
try:
|
|
234
369
|
obj = await obj_qs.aget(**get_q)
|
|
235
370
|
except ObjectDoesNotExist:
|
|
@@ -237,6 +372,68 @@ class ModelUtil:
|
|
|
237
372
|
|
|
238
373
|
return obj
|
|
239
374
|
|
|
375
|
+
def _build_lookup_query(self, pk: int | str = None, getters: dict = None) -> dict:
|
|
376
|
+
"""
|
|
377
|
+
Build lookup query dict from pk and additional getters.
|
|
378
|
+
|
|
379
|
+
Parameters
|
|
380
|
+
----------
|
|
381
|
+
pk : int | str, optional
|
|
382
|
+
Primary key value.
|
|
383
|
+
getters : dict, optional
|
|
384
|
+
Additional field lookups.
|
|
385
|
+
|
|
386
|
+
Returns
|
|
387
|
+
-------
|
|
388
|
+
dict
|
|
389
|
+
Combined lookup criteria.
|
|
390
|
+
"""
|
|
391
|
+
get_q = {self.model_pk_name: pk} if pk is not None else {}
|
|
392
|
+
if getters:
|
|
393
|
+
get_q |= getters
|
|
394
|
+
return get_q
|
|
395
|
+
|
|
396
|
+
def _apply_query_optimizations(
|
|
397
|
+
self,
|
|
398
|
+
queryset: models.QuerySet,
|
|
399
|
+
query_data: QuerySchema,
|
|
400
|
+
is_for_read: bool,
|
|
401
|
+
) -> models.QuerySet:
|
|
402
|
+
"""
|
|
403
|
+
Apply select_related and prefetch_related optimizations to queryset.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
queryset : QuerySet
|
|
408
|
+
Base queryset to optimize.
|
|
409
|
+
query_data : ModelQuerySchema
|
|
410
|
+
Query configuration with select_related/prefetch_related lists.
|
|
411
|
+
is_for_read : bool
|
|
412
|
+
Whether to include model-level relation discovery.
|
|
413
|
+
|
|
414
|
+
Returns
|
|
415
|
+
-------
|
|
416
|
+
QuerySet
|
|
417
|
+
Optimized queryset.
|
|
418
|
+
"""
|
|
419
|
+
select_related = (
|
|
420
|
+
query_data.select_related + self.get_select_relateds()
|
|
421
|
+
if is_for_read
|
|
422
|
+
else query_data.select_related
|
|
423
|
+
)
|
|
424
|
+
prefetch_related = (
|
|
425
|
+
query_data.prefetch_related + self.get_reverse_relations()
|
|
426
|
+
if is_for_read
|
|
427
|
+
else query_data.prefetch_related
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if select_related:
|
|
431
|
+
queryset = queryset.select_related(*select_related)
|
|
432
|
+
if prefetch_related:
|
|
433
|
+
queryset = queryset.prefetch_related(*prefetch_related)
|
|
434
|
+
|
|
435
|
+
return queryset
|
|
436
|
+
|
|
240
437
|
def get_reverse_relations(self) -> list[str]:
|
|
241
438
|
"""
|
|
242
439
|
Discover reverse relation names for safe prefetching.
|
|
@@ -259,6 +456,25 @@ class ModelUtil:
|
|
|
259
456
|
reverse_rels.append(field_obj.related.name)
|
|
260
457
|
return reverse_rels
|
|
261
458
|
|
|
459
|
+
def get_select_relateds(self) -> list[str]:
|
|
460
|
+
"""
|
|
461
|
+
Discover forward relation names for safe select_related.
|
|
462
|
+
|
|
463
|
+
Returns
|
|
464
|
+
-------
|
|
465
|
+
list[str]
|
|
466
|
+
Relation attribute names.
|
|
467
|
+
"""
|
|
468
|
+
select_rels = []
|
|
469
|
+
for f in self.serializable_fields:
|
|
470
|
+
field_obj = getattr(self.model, f)
|
|
471
|
+
if isinstance(field_obj, ForwardManyToOneDescriptor):
|
|
472
|
+
select_rels.append(f)
|
|
473
|
+
continue
|
|
474
|
+
if isinstance(field_obj, ForwardOneToOneDescriptor):
|
|
475
|
+
select_rels.append(f)
|
|
476
|
+
return select_rels
|
|
477
|
+
|
|
262
478
|
async def _get_field(self, k: str):
|
|
263
479
|
return (await agetattr(self.model, k)).field
|
|
264
480
|
|
|
@@ -284,49 +500,80 @@ class ModelUtil:
|
|
|
284
500
|
rel = await rel_util.get_object(request, v, with_qs_request=False)
|
|
285
501
|
payload[k] = rel
|
|
286
502
|
|
|
287
|
-
async def
|
|
288
|
-
""
|
|
289
|
-
|
|
290
|
-
""
|
|
291
|
-
descriptor = await agetattr(self.model, field_name, None)
|
|
292
|
-
if descriptor is None:
|
|
293
|
-
return None
|
|
294
|
-
return await agetattr(descriptor, "field", None) or await agetattr(
|
|
295
|
-
descriptor, "related", None
|
|
296
|
-
)
|
|
503
|
+
async def _bump_object_from_schema(
|
|
504
|
+
self, obj: type["ModelSerializer"] | models.Model, schema: Schema
|
|
505
|
+
):
|
|
506
|
+
return (await sync_to_async(schema.from_orm)(obj)).model_dump(mode="json")
|
|
297
507
|
|
|
298
|
-
def
|
|
299
|
-
"""
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
508
|
+
def _validate_read_params(self, request: HttpRequest, query_data: QuerySchema):
|
|
509
|
+
"""Validate required parameters for read operations."""
|
|
510
|
+
if request is None:
|
|
511
|
+
raise SerializeError(
|
|
512
|
+
{"request": "must be provided when object is not given"}, 400
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
if query_data is None:
|
|
516
|
+
raise SerializeError(
|
|
517
|
+
{"query_data": "must be provided when object is not given"}, 400
|
|
518
|
+
)
|
|
305
519
|
|
|
306
|
-
|
|
307
|
-
|
|
520
|
+
if (
|
|
521
|
+
hasattr(query_data, "filters")
|
|
522
|
+
and hasattr(query_data, "getters")
|
|
523
|
+
and query_data.filters
|
|
524
|
+
and query_data.getters
|
|
525
|
+
):
|
|
526
|
+
raise SerializeError(
|
|
527
|
+
{"query_data": "cannot contain both filters and getters"}, 400
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
async def _handle_query_mode(
|
|
531
|
+
self,
|
|
532
|
+
request: HttpRequest,
|
|
533
|
+
query_data: QuerySchema,
|
|
534
|
+
schema: Schema,
|
|
535
|
+
is_for_read: bool,
|
|
308
536
|
):
|
|
309
|
-
"""
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return await rel_util.get_object(request, rel_pk)
|
|
537
|
+
"""Handle different query modes (filters vs getters)."""
|
|
538
|
+
if hasattr(query_data, "filters") and query_data.filters:
|
|
539
|
+
return await self._serialize_queryset(
|
|
540
|
+
request, query_data, schema, is_for_read
|
|
541
|
+
)
|
|
315
542
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
keys_to_rewrite: list[str] = []
|
|
321
|
-
new_nested = nested_dict
|
|
322
|
-
for rel_k in nested_dict.keys():
|
|
323
|
-
attr = await agetattr(rel_obj, rel_k)
|
|
324
|
-
if isinstance(attr, models.ForeignKey):
|
|
325
|
-
keys_to_rewrite.append(rel_k)
|
|
326
|
-
for old_k in keys_to_rewrite:
|
|
327
|
-
new_nested[f"{old_k}_id"] = new_nested.pop(old_k)
|
|
328
|
-
return new_nested
|
|
543
|
+
if hasattr(query_data, "getters") and query_data.getters:
|
|
544
|
+
return await self._serialize_single_object(
|
|
545
|
+
request, query_data, schema, is_for_read
|
|
546
|
+
)
|
|
329
547
|
|
|
548
|
+
raise SerializeError(
|
|
549
|
+
{"query_data": "must contain either filters or getters"}, 400
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
async def _serialize_queryset(
|
|
553
|
+
self,
|
|
554
|
+
request: HttpRequest,
|
|
555
|
+
query_data: QuerySchema,
|
|
556
|
+
schema: Schema,
|
|
557
|
+
is_for_read: bool,
|
|
558
|
+
):
|
|
559
|
+
"""Serialize a queryset of objects."""
|
|
560
|
+
objs = await self.get_objects(
|
|
561
|
+
request, query_data=query_data, is_for_read=is_for_read
|
|
562
|
+
)
|
|
563
|
+
return [await self._bump_object_from_schema(obj, schema) async for obj in objs]
|
|
564
|
+
|
|
565
|
+
async def _serialize_single_object(
|
|
566
|
+
self,
|
|
567
|
+
request: HttpRequest,
|
|
568
|
+
query_data: QuerySchema,
|
|
569
|
+
obj_schema: Schema,
|
|
570
|
+
is_for_read: bool,
|
|
571
|
+
):
|
|
572
|
+
"""Serialize a single object."""
|
|
573
|
+
obj = await self.get_object(
|
|
574
|
+
request, query_data=query_data, is_for_read=is_for_read
|
|
575
|
+
)
|
|
576
|
+
return await self._bump_object_from_schema(obj, obj_schema)
|
|
330
577
|
|
|
331
578
|
async def parse_input_data(self, request: HttpRequest, data: Schema):
|
|
332
579
|
"""
|
|
@@ -396,37 +643,6 @@ class ModelUtil:
|
|
|
396
643
|
|
|
397
644
|
return new_payload, customs
|
|
398
645
|
|
|
399
|
-
async def parse_output_data(self, request: HttpRequest, data: Schema):
|
|
400
|
-
"""
|
|
401
|
-
Post-process serialized output.
|
|
402
|
-
|
|
403
|
-
For nested FK / OneToOne dicts:
|
|
404
|
-
- Replace dict with authoritative related instance.
|
|
405
|
-
- Rewrite nested FK keys to <name>_id for nested foreign keys.
|
|
406
|
-
|
|
407
|
-
Parameters
|
|
408
|
-
----------
|
|
409
|
-
request : HttpRequest
|
|
410
|
-
data : Schema
|
|
411
|
-
Schema (from_orm) instance.
|
|
412
|
-
|
|
413
|
-
Returns
|
|
414
|
-
-------
|
|
415
|
-
dict
|
|
416
|
-
Normalized output payload.
|
|
417
|
-
"""
|
|
418
|
-
payload = data.model_dump(mode="json")
|
|
419
|
-
|
|
420
|
-
for k, v in payload.items():
|
|
421
|
-
field_obj = await self._extract_field_obj(k)
|
|
422
|
-
if not self._should_process_nested(v, field_obj):
|
|
423
|
-
continue
|
|
424
|
-
rel_instance = await self._fetch_related_instance(request, field_obj, v)
|
|
425
|
-
if isinstance(field_obj, models.ForeignKey):
|
|
426
|
-
v = await self._rewrite_nested_foreign_keys(rel_instance, v)
|
|
427
|
-
payload[k] = rel_instance
|
|
428
|
-
return payload
|
|
429
|
-
|
|
430
646
|
async def create_s(self, request: HttpRequest, data: Schema, obj_schema: Schema):
|
|
431
647
|
"""
|
|
432
648
|
Create a new instance and return serialized output.
|
|
@@ -451,39 +667,167 @@ class ModelUtil:
|
|
|
451
667
|
obj = await self.get_object(request, pk)
|
|
452
668
|
if isinstance(self.model, ModelSerializerMeta):
|
|
453
669
|
await asyncio.gather(obj.custom_actions(customs), obj.post_create())
|
|
454
|
-
return await self.read_s(request, obj
|
|
670
|
+
return await self.read_s(obj_schema, request, obj)
|
|
455
671
|
|
|
456
|
-
async def
|
|
672
|
+
async def _read_s(
|
|
457
673
|
self,
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
674
|
+
schema: Schema,
|
|
675
|
+
request: HttpRequest = None,
|
|
676
|
+
instance: models.QuerySet[type["ModelSerializer"] | models.Model]
|
|
677
|
+
| type["ModelSerializer"]
|
|
678
|
+
| models.Model = None,
|
|
679
|
+
query_data: QuerySchema = None,
|
|
680
|
+
is_for_read: bool = False,
|
|
461
681
|
):
|
|
462
682
|
"""
|
|
463
|
-
|
|
683
|
+
Internal serialization method handling both single instances and querysets.
|
|
464
684
|
|
|
465
685
|
Parameters
|
|
466
686
|
----------
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
687
|
+
schema : Schema
|
|
688
|
+
Read schema class for serialization.
|
|
689
|
+
request : HttpRequest, optional
|
|
690
|
+
HTTP request object, required when instance is None.
|
|
691
|
+
instance : QuerySet | Model, optional
|
|
692
|
+
Instance(s) to serialize. If None, fetches based on query_data.
|
|
693
|
+
query_data : QuerySchema, optional
|
|
694
|
+
Query parameters for fetching objects when instance is None.
|
|
695
|
+
is_for_read : bool, optional
|
|
696
|
+
Whether to apply read-specific query optimizations.
|
|
697
|
+
|
|
698
|
+
Returns
|
|
699
|
+
-------
|
|
700
|
+
dict | list[dict]
|
|
701
|
+
Serialized instance(s).
|
|
702
|
+
|
|
703
|
+
Raises
|
|
704
|
+
------
|
|
705
|
+
SerializeError
|
|
706
|
+
If schema is None or validation fails.
|
|
707
|
+
"""
|
|
708
|
+
if schema is None:
|
|
709
|
+
raise SerializeError({"schema": "must be provided"}, 400)
|
|
710
|
+
|
|
711
|
+
if instance is not None:
|
|
712
|
+
if isinstance(instance, models.QuerySet):
|
|
713
|
+
return [
|
|
714
|
+
await self._bump_object_from_schema(obj, schema)
|
|
715
|
+
async for obj in instance
|
|
716
|
+
]
|
|
717
|
+
return await self._bump_object_from_schema(instance, schema)
|
|
718
|
+
|
|
719
|
+
self._validate_read_params(request, query_data)
|
|
720
|
+
return await self._handle_query_mode(request, query_data, schema, is_for_read)
|
|
721
|
+
|
|
722
|
+
async def read_s(
|
|
723
|
+
self,
|
|
724
|
+
schema: Schema,
|
|
725
|
+
request: HttpRequest = None,
|
|
726
|
+
instance: type["ModelSerializer"] = None,
|
|
727
|
+
query_data: ObjectQuerySchema = None,
|
|
728
|
+
is_for_read: bool = False,
|
|
729
|
+
) -> dict:
|
|
730
|
+
"""
|
|
731
|
+
Serialize a single model instance or fetch and serialize using query parameters.
|
|
732
|
+
|
|
733
|
+
This method handles single-object serialization. It can serialize a provided
|
|
734
|
+
instance directly or fetch and serialize a single object using query_data.getters.
|
|
735
|
+
|
|
736
|
+
Parameters
|
|
737
|
+
----------
|
|
738
|
+
schema : Schema
|
|
739
|
+
Read schema class for serialization output.
|
|
740
|
+
request : HttpRequest, optional
|
|
741
|
+
HTTP request object, required when instance is None.
|
|
742
|
+
instance : ModelSerializer | Model, optional
|
|
743
|
+
Single instance to serialize. If None, fetched based on query_data.
|
|
744
|
+
query_data : ObjectQuerySchema, optional
|
|
745
|
+
Query parameters with getters for single object lookup.
|
|
746
|
+
Required when instance is None.
|
|
747
|
+
is_for_read : bool, optional
|
|
748
|
+
Whether to apply read-specific query optimizations. Defaults to False.
|
|
472
749
|
|
|
473
750
|
Returns
|
|
474
751
|
-------
|
|
475
752
|
dict
|
|
476
|
-
Serialized
|
|
753
|
+
Serialized model instance as dictionary.
|
|
477
754
|
|
|
478
755
|
Raises
|
|
479
756
|
------
|
|
480
757
|
SerializeError
|
|
481
|
-
If
|
|
758
|
+
- If schema is None
|
|
759
|
+
- If instance is None and request or query_data is None
|
|
760
|
+
- If query_data validation fails
|
|
761
|
+
NotFoundError
|
|
762
|
+
If using getters and no matching object is found.
|
|
763
|
+
|
|
764
|
+
Notes
|
|
765
|
+
-----
|
|
766
|
+
- Uses Pydantic's from_orm() with mode="json" for serialization
|
|
767
|
+
- When instance is provided, request and query_data are ignored
|
|
768
|
+
- Query optimizations applied when is_for_read=True
|
|
769
|
+
"""
|
|
770
|
+
return await self._read_s(
|
|
771
|
+
schema,
|
|
772
|
+
request,
|
|
773
|
+
instance,
|
|
774
|
+
query_data,
|
|
775
|
+
is_for_read,
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
async def list_read_s(
|
|
779
|
+
self,
|
|
780
|
+
schema: Schema,
|
|
781
|
+
request: HttpRequest = None,
|
|
782
|
+
instances: models.QuerySet[type["ModelSerializer"] | models.Model] = None,
|
|
783
|
+
query_data: ObjectsQuerySchema = None,
|
|
784
|
+
is_for_read: bool = False,
|
|
785
|
+
) -> list[dict]:
|
|
482
786
|
"""
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
787
|
+
Serialize multiple model instances or fetch and serialize using query parameters.
|
|
788
|
+
|
|
789
|
+
This method handles queryset serialization. It can serialize provided instances
|
|
790
|
+
directly or fetch and serialize multiple objects using query_data.filters.
|
|
791
|
+
|
|
792
|
+
Parameters
|
|
793
|
+
----------
|
|
794
|
+
schema : Schema
|
|
795
|
+
Read schema class for serialization output.
|
|
796
|
+
request : HttpRequest, optional
|
|
797
|
+
HTTP request object, required when instances is None.
|
|
798
|
+
instances : QuerySet, optional
|
|
799
|
+
Queryset of instances to serialize. If None, fetched based on query_data.
|
|
800
|
+
query_data : ObjectsQuerySchema, optional
|
|
801
|
+
Query parameters with filters for multiple object lookup.
|
|
802
|
+
Required when instances is None.
|
|
803
|
+
is_for_read : bool, optional
|
|
804
|
+
Whether to apply read-specific query optimizations. Defaults to False.
|
|
805
|
+
|
|
806
|
+
Returns
|
|
807
|
+
-------
|
|
808
|
+
list[dict]
|
|
809
|
+
List of serialized model instances as dictionaries.
|
|
810
|
+
|
|
811
|
+
Raises
|
|
812
|
+
------
|
|
813
|
+
SerializeError
|
|
814
|
+
- If schema is None
|
|
815
|
+
- If instances is None and request or query_data is None
|
|
816
|
+
- If query_data validation fails
|
|
817
|
+
|
|
818
|
+
Notes
|
|
819
|
+
-----
|
|
820
|
+
- Uses Pydantic's from_orm() with mode="json" for serialization
|
|
821
|
+
- When instances is provided, request and query_data are ignored
|
|
822
|
+
- Query optimizations applied when is_for_read=True
|
|
823
|
+
- Processes queryset asynchronously for efficiency
|
|
824
|
+
"""
|
|
825
|
+
return await self._read_s(
|
|
826
|
+
schema,
|
|
827
|
+
request,
|
|
828
|
+
instances,
|
|
829
|
+
query_data,
|
|
830
|
+
is_for_read,
|
|
487
831
|
)
|
|
488
832
|
|
|
489
833
|
async def update_s(
|
|
@@ -518,7 +862,7 @@ class ModelUtil:
|
|
|
518
862
|
await obj.custom_actions(customs)
|
|
519
863
|
await obj.asave()
|
|
520
864
|
updated_object = await self.get_object(request, pk)
|
|
521
|
-
return await self.read_s(request, updated_object
|
|
865
|
+
return await self.read_s(obj_schema, request, updated_object)
|
|
522
866
|
|
|
523
867
|
async def delete_s(self, request: HttpRequest, pk: int | str):
|
|
524
868
|
"""
|
|
@@ -555,9 +899,39 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
|
|
|
555
899
|
See inline docstrings for per-method behavior.
|
|
556
900
|
"""
|
|
557
901
|
|
|
902
|
+
util: ClassVar[ModelUtil]
|
|
903
|
+
query_util: ClassVar[QueryUtil]
|
|
904
|
+
|
|
558
905
|
class Meta:
|
|
559
906
|
abstract = True
|
|
560
907
|
|
|
908
|
+
def __init_subclass__(cls, **kwargs):
|
|
909
|
+
super().__init_subclass__(**kwargs)
|
|
910
|
+
# Bind a ModelUtil instance to the subclass for convenient access
|
|
911
|
+
cls.util = ModelUtil(cls)
|
|
912
|
+
cls.query_util = QueryUtil(cls)
|
|
913
|
+
|
|
914
|
+
class QuerySet:
|
|
915
|
+
"""
|
|
916
|
+
Configuration container describing how to build query schemas for a model.
|
|
917
|
+
Purpose
|
|
918
|
+
-------
|
|
919
|
+
Describes which fields and extras are available when querying for model
|
|
920
|
+
instances. A factory/metaclass can read this configuration to generate
|
|
921
|
+
Pydantic / Ninja query schemas.
|
|
922
|
+
Attributes
|
|
923
|
+
----------
|
|
924
|
+
read : ModelQuerySetSchema
|
|
925
|
+
Schema configuration for read operations.
|
|
926
|
+
queryset_request : ModelQuerySetSchema
|
|
927
|
+
Schema configuration for queryset_request hook.
|
|
928
|
+
extras : list[ModelQuerySetExtraSchema]
|
|
929
|
+
Additional computed / synthetic query parameters.
|
|
930
|
+
"""
|
|
931
|
+
read = ModelQuerySetSchema()
|
|
932
|
+
queryset_request = ModelQuerySetSchema()
|
|
933
|
+
extras: list[ModelQuerySetExtraSchema] = []
|
|
934
|
+
|
|
561
935
|
class CreateSerializer:
|
|
562
936
|
"""Configuration container describing how to build a create (input) schema for a model.
|
|
563
937
|
|
|
@@ -621,48 +995,6 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
|
|
|
621
995
|
optionals: list[tuple[str, type]] = []
|
|
622
996
|
excludes: list[str] = []
|
|
623
997
|
|
|
624
|
-
@property
|
|
625
|
-
def has_custom_fields_create(self):
|
|
626
|
-
"""
|
|
627
|
-
Whether CreateSerializer declares custom fields.
|
|
628
|
-
"""
|
|
629
|
-
return hasattr(self.CreateSerializer, "customs")
|
|
630
|
-
|
|
631
|
-
@property
|
|
632
|
-
def has_custom_fields_update(self):
|
|
633
|
-
"""
|
|
634
|
-
Whether UpdateSerializer declares custom fields.
|
|
635
|
-
"""
|
|
636
|
-
return hasattr(self.UpdateSerializer, "customs")
|
|
637
|
-
|
|
638
|
-
@property
|
|
639
|
-
def has_custom_fields(self):
|
|
640
|
-
"""
|
|
641
|
-
Whether any serializer declares custom fields.
|
|
642
|
-
"""
|
|
643
|
-
return self.has_custom_fields_create or self.has_custom_fields_update
|
|
644
|
-
|
|
645
|
-
@property
|
|
646
|
-
def has_optional_fields_create(self):
|
|
647
|
-
"""
|
|
648
|
-
Whether CreateSerializer declares optional fields.
|
|
649
|
-
"""
|
|
650
|
-
return hasattr(self.CreateSerializer, "optionals")
|
|
651
|
-
|
|
652
|
-
@property
|
|
653
|
-
def has_optional_fields_update(self):
|
|
654
|
-
"""
|
|
655
|
-
Whether UpdateSerializer declares optional fields.
|
|
656
|
-
"""
|
|
657
|
-
return hasattr(self.UpdateSerializer, "optionals")
|
|
658
|
-
|
|
659
|
-
@property
|
|
660
|
-
def has_optional_fields(self):
|
|
661
|
-
"""
|
|
662
|
-
Whether any serializer declares optional fields.
|
|
663
|
-
"""
|
|
664
|
-
return self.has_optional_fields_create or self.has_optional_fields_update
|
|
665
|
-
|
|
666
998
|
@classmethod
|
|
667
999
|
def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
|
|
668
1000
|
"""
|
|
@@ -823,7 +1155,10 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
|
|
|
823
1155
|
-------
|
|
824
1156
|
QuerySet
|
|
825
1157
|
"""
|
|
826
|
-
return cls.
|
|
1158
|
+
return cls.query_util.apply_queryset_optimizations(
|
|
1159
|
+
queryset=cls.objects.all(),
|
|
1160
|
+
scope=cls.query_util.SCOPES.QUERYSET_REQUEST,
|
|
1161
|
+
)
|
|
827
1162
|
|
|
828
1163
|
async def post_create(self) -> None:
|
|
829
1164
|
"""
|
|
@@ -1179,11 +1514,12 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
|
|
|
1179
1514
|
"""
|
|
1180
1515
|
Override save lifecycle to inject create/update hooks.
|
|
1181
1516
|
"""
|
|
1182
|
-
|
|
1517
|
+
state_adding = self._state.adding
|
|
1518
|
+
if state_adding:
|
|
1183
1519
|
self.on_create_before_save()
|
|
1184
1520
|
self.before_save()
|
|
1185
1521
|
super().save(*args, **kwargs)
|
|
1186
|
-
if
|
|
1522
|
+
if state_adding:
|
|
1187
1523
|
self.on_create_after_save()
|
|
1188
1524
|
self.after_save()
|
|
1189
1525
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .generics import GenericMessageSchema
|
|
2
|
+
from .api import (
|
|
3
|
+
M2MDetailSchema,
|
|
4
|
+
M2MSchemaOut,
|
|
5
|
+
M2MAddSchemaIn,
|
|
6
|
+
M2MRemoveSchemaIn,
|
|
7
|
+
M2MSchemaIn,
|
|
8
|
+
)
|
|
9
|
+
from .helpers import M2MRelationSchema, QuerySchema, ModelQuerySetSchema, ObjectQuerySchema, ObjectsQuerySchema
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"GenericMessageSchema",
|
|
13
|
+
"M2MDetailSchema",
|
|
14
|
+
"M2MSchemaOut",
|
|
15
|
+
"M2MAddSchemaIn",
|
|
16
|
+
"M2MRemoveSchemaIn",
|
|
17
|
+
"M2MSchemaIn",
|
|
18
|
+
"M2MRelationSchema",
|
|
19
|
+
"QuerySchema",
|
|
20
|
+
"ModelQuerySetSchema",
|
|
21
|
+
"ObjectQuerySchema",
|
|
22
|
+
"ObjectsQuerySchema",
|
|
23
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from ninja import Schema
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class M2MDetailSchema(Schema):
|
|
5
|
+
count: int
|
|
6
|
+
details: list[str]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class M2MSchemaOut(Schema):
|
|
10
|
+
errors: M2MDetailSchema
|
|
11
|
+
results: M2MDetailSchema
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class M2MAddSchemaIn(Schema):
|
|
15
|
+
add: list = []
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class M2MRemoveSchemaIn(Schema):
|
|
19
|
+
remove: list = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class M2MSchemaIn(Schema):
|
|
23
|
+
add: list = []
|
|
24
|
+
remove: list = []
|
|
@@ -1,36 +1,9 @@
|
|
|
1
1
|
from typing import Optional, Type
|
|
2
2
|
|
|
3
3
|
from ninja import Schema
|
|
4
|
-
from .
|
|
4
|
+
from ninja_aio.types import ModelSerializerMeta
|
|
5
5
|
from django.db.models import Model
|
|
6
|
-
from pydantic import BaseModel,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class GenericMessageSchema(RootModel[dict[str, str]]):
|
|
10
|
-
root: dict[str, str]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class M2MDetailSchema(Schema):
|
|
14
|
-
count: int
|
|
15
|
-
details: list[str]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class M2MSchemaOut(Schema):
|
|
19
|
-
errors: M2MDetailSchema
|
|
20
|
-
results: M2MDetailSchema
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class M2MAddSchemaIn(Schema):
|
|
24
|
-
add: list = []
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class M2MRemoveSchemaIn(Schema):
|
|
28
|
-
remove: list = []
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class M2MSchemaIn(Schema):
|
|
32
|
-
add: list = []
|
|
33
|
-
remove: list = []
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
34
7
|
|
|
35
8
|
|
|
36
9
|
class M2MRelationSchema(BaseModel):
|
|
@@ -55,7 +28,7 @@ class M2MRelationSchema(BaseModel):
|
|
|
55
28
|
)
|
|
56
29
|
"""
|
|
57
30
|
|
|
58
|
-
model:
|
|
31
|
+
model: ModelSerializerMeta | Type[Model]
|
|
59
32
|
related_name: str
|
|
60
33
|
add: bool = True
|
|
61
34
|
remove: bool = True
|
|
@@ -74,9 +47,36 @@ class M2MRelationSchema(BaseModel):
|
|
|
74
47
|
if related_schema is not None:
|
|
75
48
|
return data
|
|
76
49
|
model = data.get("model")
|
|
77
|
-
if not
|
|
50
|
+
if not isinstance(model, ModelSerializerMeta):
|
|
78
51
|
raise ValueError(
|
|
79
52
|
"related_schema must be provided if model is not a ModelSerializer",
|
|
80
53
|
)
|
|
81
54
|
data["related_schema"] = model.generate_related_s()
|
|
82
55
|
return data
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ModelQuerySetSchema(BaseModel):
|
|
59
|
+
select_related: Optional[list[str]] = []
|
|
60
|
+
prefetch_related: Optional[list[str]] = []
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ModelQuerySetExtraSchema(ModelQuerySetSchema):
|
|
64
|
+
scope: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ObjectQuerySchema(ModelQuerySetSchema):
|
|
68
|
+
getters: Optional[dict] = {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ObjectsQuerySchema(ModelQuerySetSchema):
|
|
72
|
+
filters: Optional[dict] = {}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class QuerySchema(ModelQuerySetSchema):
|
|
76
|
+
filters: Optional[dict] = {}
|
|
77
|
+
getters: Optional[dict] = {}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class QueryUtilBaseScopesSchema(BaseModel):
|
|
81
|
+
READ: str = "read"
|
|
82
|
+
QUERYSET_REQUEST: str = "queryset_request"
|
|
@@ -7,12 +7,14 @@ from django.http import HttpRequest
|
|
|
7
7
|
from django.db.models import Model, QuerySet
|
|
8
8
|
from pydantic import create_model
|
|
9
9
|
|
|
10
|
+
from ninja_aio.schemas.helpers import ModelQuerySetSchema, QuerySchema
|
|
11
|
+
|
|
10
12
|
from .models import ModelSerializer, ModelUtil
|
|
11
13
|
from .schemas import (
|
|
12
14
|
GenericMessageSchema,
|
|
13
15
|
M2MRelationSchema,
|
|
14
16
|
)
|
|
15
|
-
from .helpers import ManyToManyAPI
|
|
17
|
+
from .helpers.api import ManyToManyAPI
|
|
16
18
|
from .types import ModelSerializerMeta, VIEW_TYPES
|
|
17
19
|
from .decorators import unique_view
|
|
18
20
|
|
|
@@ -178,7 +180,11 @@ class APIViewSet:
|
|
|
178
180
|
|
|
179
181
|
def __init__(self) -> None:
|
|
180
182
|
self.error_codes = ERROR_CODES
|
|
181
|
-
self.model_util =
|
|
183
|
+
self.model_util = (
|
|
184
|
+
ModelUtil(self.model)
|
|
185
|
+
if not isinstance(self.model, ModelSerializerMeta)
|
|
186
|
+
else self.model.util
|
|
187
|
+
)
|
|
182
188
|
self.schema_out, self.schema_in, self.schema_update = self.get_schemas()
|
|
183
189
|
self.path_schema = self._generate_path_schema()
|
|
184
190
|
self.filters_schema = self._generate_filters_schema()
|
|
@@ -241,7 +247,7 @@ class APIViewSet:
|
|
|
241
247
|
Schema containing only the primary key field for path resolution.
|
|
242
248
|
"""
|
|
243
249
|
return self._generate_schema(
|
|
244
|
-
{self.model_util.model_pk_name:
|
|
250
|
+
{self.model_util.model_pk_name: self.model_util.pk_field_type}, "PathSchema"
|
|
245
251
|
)
|
|
246
252
|
|
|
247
253
|
def _generate_filters_schema(self):
|
|
@@ -256,6 +262,16 @@ class APIViewSet:
|
|
|
256
262
|
"""
|
|
257
263
|
return data.model_dump()[self.model_util.model_pk_name]
|
|
258
264
|
|
|
265
|
+
def _get_query_data(self) -> ModelQuerySetSchema:
|
|
266
|
+
"""
|
|
267
|
+
Return default query data for list/retrieve views.
|
|
268
|
+
"""
|
|
269
|
+
return (
|
|
270
|
+
ModelQuerySetSchema()
|
|
271
|
+
if not isinstance(self.model, ModelSerializerMeta)
|
|
272
|
+
else self.model.query_util.read_config
|
|
273
|
+
)
|
|
274
|
+
|
|
259
275
|
def get_schemas(self):
|
|
260
276
|
"""
|
|
261
277
|
Return (schema_out, schema_in, schema_update), generating them if model is a ModelSerializer.
|
|
@@ -317,19 +333,14 @@ class APIViewSet:
|
|
|
317
333
|
request: HttpRequest,
|
|
318
334
|
filters: Query[self.filters_schema] = None, # type: ignore
|
|
319
335
|
):
|
|
320
|
-
qs = self.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
qs = qs.prefetch_related(*rels)
|
|
336
|
+
qs = await self.model_util.get_objects(
|
|
337
|
+
request,
|
|
338
|
+
query_data=self._get_query_data(),
|
|
339
|
+
is_for_read=True,
|
|
340
|
+
)
|
|
326
341
|
if filters is not None:
|
|
327
342
|
qs = await self.query_params_handler(qs, filters.model_dump())
|
|
328
|
-
|
|
329
|
-
await self.model_util.read_s(request, obj, self.schema_out)
|
|
330
|
-
async for obj in qs.all()
|
|
331
|
-
]
|
|
332
|
-
return objs
|
|
343
|
+
return await self.model_util.list_read_s(self.schema_out, request, qs)
|
|
333
344
|
|
|
334
345
|
return list
|
|
335
346
|
|
|
@@ -347,8 +358,14 @@ class APIViewSet:
|
|
|
347
358
|
)
|
|
348
359
|
@unique_view(self)
|
|
349
360
|
async def retrieve(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
|
|
350
|
-
|
|
351
|
-
return await self.model_util.read_s(
|
|
361
|
+
query_data = self._get_query_data()
|
|
362
|
+
return await self.model_util.read_s(
|
|
363
|
+
self.schema_out,
|
|
364
|
+
request,
|
|
365
|
+
query_data=QuerySchema(
|
|
366
|
+
getters={"pk": self._get_pk(pk)}, **query_data.model_dump()
|
|
367
|
+
),
|
|
368
|
+
)
|
|
352
369
|
|
|
353
370
|
return retrieve
|
|
354
371
|
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Internet :: WWW/HTTP",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
-
requires = ["django-ninja >=1.3.0, <=1.
|
|
29
|
+
requires = ["django-ninja >=1.3.0, <=1.5.0", "joserfc >=1.0.0, <= 1.4.1", "orjson >= 3.10.7, <= 3.11.4"]
|
|
30
30
|
description-file = "README.md"
|
|
31
31
|
requires-python = ">=3.10"
|
|
32
32
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|