django-ninja-aio-crud 0.10.2__py3-none-any.whl → 2.4.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.
- django_ninja_aio_crud-2.4.0.dist-info/METADATA +382 -0
- django_ninja_aio_crud-2.4.0.dist-info/RECORD +29 -0
- ninja_aio/__init__.py +1 -1
- ninja_aio/api.py +24 -2
- ninja_aio/auth.py +186 -4
- ninja_aio/decorators/__init__.py +23 -0
- ninja_aio/decorators/operations.py +9 -0
- ninja_aio/decorators/views.py +219 -0
- ninja_aio/exceptions.py +36 -1
- ninja_aio/factory/__init__.py +3 -0
- ninja_aio/factory/operations.py +296 -0
- ninja_aio/helpers/__init__.py +0 -0
- ninja_aio/helpers/api.py +506 -0
- ninja_aio/helpers/query.py +108 -0
- ninja_aio/models/__init__.py +4 -0
- ninja_aio/models/serializers.py +738 -0
- ninja_aio/models/utils.py +894 -0
- ninja_aio/renders.py +26 -26
- ninja_aio/schemas/__init__.py +23 -0
- ninja_aio/{schemas.py → schemas/api.py} +0 -5
- ninja_aio/schemas/generics.py +5 -0
- ninja_aio/schemas/helpers.py +170 -0
- ninja_aio/types.py +3 -1
- ninja_aio/views/__init__.py +3 -0
- ninja_aio/views/api.py +582 -0
- ninja_aio/views/mixins.py +275 -0
- django_ninja_aio_crud-0.10.2.dist-info/METADATA +0 -526
- django_ninja_aio_crud-0.10.2.dist-info/RECORD +0 -14
- ninja_aio/models.py +0 -549
- ninja_aio/views.py +0 -522
- {django_ninja_aio_crud-0.10.2.dist-info → django_ninja_aio_crud-2.4.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-0.10.2.dist-info → django_ninja_aio_crud-2.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ninja import Schema
|
|
6
|
+
from ninja.orm import fields
|
|
7
|
+
from ninja.errors import ConfigError
|
|
8
|
+
|
|
9
|
+
from django.db import models
|
|
10
|
+
from django.http import HttpRequest
|
|
11
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
12
|
+
from asgiref.sync import sync_to_async
|
|
13
|
+
from django.db.models.fields.related_descriptors import (
|
|
14
|
+
ReverseManyToOneDescriptor,
|
|
15
|
+
ReverseOneToOneDescriptor,
|
|
16
|
+
ManyToManyDescriptor,
|
|
17
|
+
ForwardManyToOneDescriptor,
|
|
18
|
+
ForwardOneToOneDescriptor,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from ninja_aio.exceptions import SerializeError, NotFoundError
|
|
22
|
+
from ninja_aio.models.serializers import ModelSerializer
|
|
23
|
+
from ninja_aio.types import ModelSerializerMeta
|
|
24
|
+
from ninja_aio.schemas.helpers import (
|
|
25
|
+
QuerySchema,
|
|
26
|
+
ObjectQuerySchema,
|
|
27
|
+
ObjectsQuerySchema,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def agetattr(obj, name: str, default=None):
|
|
32
|
+
"""
|
|
33
|
+
Async wrapper around getattr using sync_to_async.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
obj : Any
|
|
38
|
+
Object from which to retrieve the attribute.
|
|
39
|
+
name : str
|
|
40
|
+
Attribute name.
|
|
41
|
+
default : Any, optional
|
|
42
|
+
Default value if attribute is missing.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
Any
|
|
47
|
+
Attribute value (or default).
|
|
48
|
+
"""
|
|
49
|
+
return await sync_to_async(getattr)(obj, name, default)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ModelUtil:
|
|
53
|
+
"""
|
|
54
|
+
ModelUtil
|
|
55
|
+
=========
|
|
56
|
+
Async utility bound to a Django model class (or a ModelSerializer subclass)
|
|
57
|
+
providing high‑level CRUD helpers plus (de)serialization glue for Django Ninja.
|
|
58
|
+
|
|
59
|
+
Overview
|
|
60
|
+
--------
|
|
61
|
+
Central responsibilities:
|
|
62
|
+
- Introspect model metadata (field list, pk name, verbose names).
|
|
63
|
+
- Normalize inbound payloads (custom / optional fields, FK resolution, base64 decoding).
|
|
64
|
+
- Normalize outbound payloads (resolve nested relation dicts into model instances).
|
|
65
|
+
- Prefetch reverse relations to mitigate N+1 issues.
|
|
66
|
+
- Invoke optional serializer hooks: custom_actions(), post_create(), queryset_request().
|
|
67
|
+
|
|
68
|
+
Compatible With
|
|
69
|
+
---------------
|
|
70
|
+
- Plain Django models.
|
|
71
|
+
- Models using ModelSerializerMeta exposing:
|
|
72
|
+
get_fields(mode), is_custom(name), is_optional(name),
|
|
73
|
+
queryset_request(request), custom_actions(payload), post_create().
|
|
74
|
+
|
|
75
|
+
Key Methods
|
|
76
|
+
-----------
|
|
77
|
+
- get_object()
|
|
78
|
+
- parse_input_data()
|
|
79
|
+
- parse_output_data()
|
|
80
|
+
- create_s / read_s / update_s / delete_s
|
|
81
|
+
|
|
82
|
+
Error Handling
|
|
83
|
+
--------------
|
|
84
|
+
- Missing objects -> NotFoundError(...)
|
|
85
|
+
- Bad base64 -> SerializeError({...}, 400)
|
|
86
|
+
|
|
87
|
+
Performance Notes
|
|
88
|
+
-----------------
|
|
89
|
+
- Each FK resolution is an async DB hit; batch when necessary externally.
|
|
90
|
+
|
|
91
|
+
Design
|
|
92
|
+
------
|
|
93
|
+
- Stateless wrapper; safe per-request instantiation.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self, model: type["ModelSerializer"] | models.Model, serializer_class=None
|
|
98
|
+
):
|
|
99
|
+
"""
|
|
100
|
+
Initialize with a Django model or ModelSerializer subclass.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
model : Model | ModelSerializerMeta
|
|
105
|
+
Target model class.
|
|
106
|
+
"""
|
|
107
|
+
from ninja_aio.models.serializers import Serializer
|
|
108
|
+
|
|
109
|
+
self.model = model
|
|
110
|
+
self.serializer_class: Serializer = serializer_class
|
|
111
|
+
if serializer_class is not None and isinstance(model, ModelSerializerMeta):
|
|
112
|
+
raise ConfigError(
|
|
113
|
+
"ModelUtil cannot accept both model and serializer_class if the model is a ModelSerializer."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def pk_field_type(self):
|
|
118
|
+
"""
|
|
119
|
+
Python type corresponding to the model's primary key field.
|
|
120
|
+
|
|
121
|
+
Resolution
|
|
122
|
+
----------
|
|
123
|
+
Uses the Django field's internal type and ninja.orm.fields.TYPES mapping.
|
|
124
|
+
If the internal type is unknown, instructs how to register a custom mapping.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
type
|
|
129
|
+
Native Python type for the PK suitable for schema generation.
|
|
130
|
+
|
|
131
|
+
Raises
|
|
132
|
+
------
|
|
133
|
+
ConfigError
|
|
134
|
+
If the internal type is not registered in ninja.orm.fields.TYPES.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
internal_type = self.model._meta.pk.get_internal_type()
|
|
138
|
+
return fields.TYPES[internal_type]
|
|
139
|
+
except KeyError as e:
|
|
140
|
+
msg = [
|
|
141
|
+
f"Do not know how to convert django field '{internal_type}'.",
|
|
142
|
+
"Try: from ninja.orm import register_field",
|
|
143
|
+
"register_field('{internal_type}', <your-python-type>)",
|
|
144
|
+
]
|
|
145
|
+
raise ConfigError("\n".join(msg)) from e
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def serializable_fields(self):
|
|
149
|
+
"""
|
|
150
|
+
List of fields considered serializable for read operations.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
list[str]
|
|
155
|
+
Explicit read fields if ModelSerializerMeta, otherwise all model fields.
|
|
156
|
+
"""
|
|
157
|
+
if isinstance(self.model, ModelSerializerMeta):
|
|
158
|
+
return self.model.get_fields("read")
|
|
159
|
+
return self.model_fields
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def model_fields(self):
|
|
163
|
+
"""
|
|
164
|
+
Raw model field names (including forward relations).
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
list[str]
|
|
169
|
+
"""
|
|
170
|
+
return [field.name for field in self.model._meta.get_fields()]
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def model_name(self) -> str:
|
|
174
|
+
"""
|
|
175
|
+
Django internal model name.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
str
|
|
180
|
+
"""
|
|
181
|
+
return self.model._meta.model_name
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def model_pk_name(self) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Primary key attribute name (attname).
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
str
|
|
191
|
+
"""
|
|
192
|
+
return self.model._meta.pk.attname
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def model_verbose_name_plural(self) -> str:
|
|
196
|
+
"""
|
|
197
|
+
Human readable plural verbose name.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
str
|
|
202
|
+
"""
|
|
203
|
+
return self.model._meta.verbose_name_plural
|
|
204
|
+
|
|
205
|
+
def verbose_name_path_resolver(self) -> str:
|
|
206
|
+
"""
|
|
207
|
+
Slugify plural verbose name for URL path usage.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
str
|
|
212
|
+
"""
|
|
213
|
+
return "-".join(self.model_verbose_name_plural.split(" "))
|
|
214
|
+
|
|
215
|
+
def verbose_name_view_resolver(self) -> str:
|
|
216
|
+
"""
|
|
217
|
+
Camel-case plural verbose name for view name usage.
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
str
|
|
222
|
+
"""
|
|
223
|
+
return self.model_verbose_name_plural.replace(" ", "")
|
|
224
|
+
|
|
225
|
+
async def _get_base_queryset(
|
|
226
|
+
self,
|
|
227
|
+
request: HttpRequest,
|
|
228
|
+
query_data: QuerySchema,
|
|
229
|
+
with_qs_request: bool,
|
|
230
|
+
is_for_read: bool,
|
|
231
|
+
) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
|
|
232
|
+
"""
|
|
233
|
+
Build base queryset with optimizations and filters.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
request : HttpRequest
|
|
238
|
+
The HTTP request object.
|
|
239
|
+
query_data : QuerySchema
|
|
240
|
+
Query configuration with filters and optimizations.
|
|
241
|
+
with_qs_request : bool
|
|
242
|
+
Whether to apply queryset_request hook.
|
|
243
|
+
is_for_read : bool
|
|
244
|
+
Whether this is a read operation.
|
|
245
|
+
|
|
246
|
+
Returns
|
|
247
|
+
-------
|
|
248
|
+
models.QuerySet
|
|
249
|
+
Optimized and filtered queryset.
|
|
250
|
+
"""
|
|
251
|
+
# Start with base queryset
|
|
252
|
+
obj_qs = (
|
|
253
|
+
self.model.objects.all()
|
|
254
|
+
if self.serializer_class is None
|
|
255
|
+
else await self.serializer_class.queryset_request(request)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Apply query optimizations
|
|
259
|
+
obj_qs = self._apply_query_optimizations(obj_qs, query_data, is_for_read)
|
|
260
|
+
|
|
261
|
+
# Apply queryset_request hook if available
|
|
262
|
+
if isinstance(self.model, ModelSerializerMeta) and with_qs_request:
|
|
263
|
+
obj_qs = await self.model.queryset_request(request)
|
|
264
|
+
|
|
265
|
+
# Apply filters if present
|
|
266
|
+
if hasattr(query_data, "filters") and query_data.filters:
|
|
267
|
+
obj_qs = obj_qs.filter(**query_data.filters)
|
|
268
|
+
|
|
269
|
+
return obj_qs
|
|
270
|
+
|
|
271
|
+
async def get_objects(
|
|
272
|
+
self,
|
|
273
|
+
request: HttpRequest,
|
|
274
|
+
query_data: ObjectsQuerySchema = None,
|
|
275
|
+
with_qs_request=True,
|
|
276
|
+
is_for_read: bool = False,
|
|
277
|
+
) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
|
|
278
|
+
"""
|
|
279
|
+
Retrieve a queryset with optimized database queries.
|
|
280
|
+
|
|
281
|
+
This method fetches a queryset applying query optimizations including
|
|
282
|
+
select_related and prefetch_related based on the model's relationships
|
|
283
|
+
and the query parameters.
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
request : HttpRequest
|
|
288
|
+
The HTTP request object, used for queryset_request hooks.
|
|
289
|
+
query_data : ObjectsQuerySchema, optional
|
|
290
|
+
Schema containing filters and query optimization parameters.
|
|
291
|
+
Defaults to an empty ObjectsQuerySchema instance.
|
|
292
|
+
with_qs_request : bool, optional
|
|
293
|
+
Whether to apply the model's queryset_request hook if available.
|
|
294
|
+
Defaults to True.
|
|
295
|
+
is_for_read : bool, optional
|
|
296
|
+
Flag indicating if the query is for read operations, which may affect
|
|
297
|
+
query optimization strategies. Defaults to False.
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
models.QuerySet[type["ModelSerializer"] | models.Model]
|
|
302
|
+
A QuerySet of model instances.
|
|
303
|
+
|
|
304
|
+
Notes
|
|
305
|
+
-----
|
|
306
|
+
- Query optimizations are automatically applied based on discovered relationships
|
|
307
|
+
- The queryset_request hook is called if the model implements ModelSerializerMeta
|
|
308
|
+
"""
|
|
309
|
+
if query_data is None:
|
|
310
|
+
query_data = ObjectsQuerySchema()
|
|
311
|
+
|
|
312
|
+
return await self._get_base_queryset(
|
|
313
|
+
request, query_data, with_qs_request, is_for_read
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
async def get_object(
|
|
317
|
+
self,
|
|
318
|
+
request: HttpRequest,
|
|
319
|
+
pk: int | str = None,
|
|
320
|
+
query_data: ObjectQuerySchema = None,
|
|
321
|
+
with_qs_request=True,
|
|
322
|
+
is_for_read: bool = False,
|
|
323
|
+
) -> type["ModelSerializer"] | models.Model:
|
|
324
|
+
"""
|
|
325
|
+
Retrieve a single object with optimized database queries.
|
|
326
|
+
|
|
327
|
+
This method handles single-object retrieval with automatic query optimizations
|
|
328
|
+
including select_related and prefetch_related based on the model's relationships
|
|
329
|
+
and the query parameters.
|
|
330
|
+
|
|
331
|
+
Parameters
|
|
332
|
+
----------
|
|
333
|
+
request : HttpRequest
|
|
334
|
+
The HTTP request object, used for queryset_request hooks.
|
|
335
|
+
pk : int | str, optional
|
|
336
|
+
Primary key value for single object lookup. Defaults to None.
|
|
337
|
+
query_data : ObjectQuerySchema, optional
|
|
338
|
+
Schema containing getters and query optimization parameters.
|
|
339
|
+
Defaults to an empty ObjectQuerySchema instance.
|
|
340
|
+
with_qs_request : bool, optional
|
|
341
|
+
Whether to apply the model's queryset_request hook if available.
|
|
342
|
+
Defaults to True.
|
|
343
|
+
is_for_read : bool, optional
|
|
344
|
+
Flag indicating if the query is for read operations, which may affect
|
|
345
|
+
query optimization strategies. Defaults to False.
|
|
346
|
+
|
|
347
|
+
Returns
|
|
348
|
+
-------
|
|
349
|
+
type["ModelSerializer"] | models.Model
|
|
350
|
+
A single model instance.
|
|
351
|
+
|
|
352
|
+
Raises
|
|
353
|
+
------
|
|
354
|
+
ValueError
|
|
355
|
+
If neither pk nor getters are provided.
|
|
356
|
+
NotFoundError
|
|
357
|
+
If no matching object exists in the database.
|
|
358
|
+
|
|
359
|
+
Notes
|
|
360
|
+
-----
|
|
361
|
+
- Query optimizations are automatically applied based on discovered relationships
|
|
362
|
+
- The queryset_request hook is called if the model implements ModelSerializerMeta
|
|
363
|
+
"""
|
|
364
|
+
if query_data is None:
|
|
365
|
+
query_data = ObjectQuerySchema()
|
|
366
|
+
|
|
367
|
+
if not query_data.getters and pk is None:
|
|
368
|
+
raise ValueError(
|
|
369
|
+
"Either pk or getters must be provided for single object retrieval."
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Build lookup query and get optimized queryset
|
|
373
|
+
get_q = self._build_lookup_query(pk, query_data.getters)
|
|
374
|
+
obj_qs = await self._get_base_queryset(
|
|
375
|
+
request, query_data, with_qs_request, is_for_read
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Perform lookup
|
|
379
|
+
try:
|
|
380
|
+
obj = await obj_qs.aget(**get_q)
|
|
381
|
+
except ObjectDoesNotExist:
|
|
382
|
+
raise NotFoundError(self.model)
|
|
383
|
+
|
|
384
|
+
return obj
|
|
385
|
+
|
|
386
|
+
def _build_lookup_query(self, pk: int | str = None, getters: dict = None) -> dict:
|
|
387
|
+
"""
|
|
388
|
+
Build lookup query dict from pk and additional getters.
|
|
389
|
+
|
|
390
|
+
Parameters
|
|
391
|
+
----------
|
|
392
|
+
pk : int | str, optional
|
|
393
|
+
Primary key value.
|
|
394
|
+
getters : dict, optional
|
|
395
|
+
Additional field lookups.
|
|
396
|
+
|
|
397
|
+
Returns
|
|
398
|
+
-------
|
|
399
|
+
dict
|
|
400
|
+
Combined lookup criteria.
|
|
401
|
+
"""
|
|
402
|
+
get_q = {self.model_pk_name: pk} if pk is not None else {}
|
|
403
|
+
if getters:
|
|
404
|
+
get_q |= getters
|
|
405
|
+
return get_q
|
|
406
|
+
|
|
407
|
+
def _apply_query_optimizations(
|
|
408
|
+
self,
|
|
409
|
+
queryset: models.QuerySet,
|
|
410
|
+
query_data: QuerySchema,
|
|
411
|
+
is_for_read: bool,
|
|
412
|
+
) -> models.QuerySet:
|
|
413
|
+
"""
|
|
414
|
+
Apply select_related and prefetch_related optimizations to queryset.
|
|
415
|
+
|
|
416
|
+
Parameters
|
|
417
|
+
----------
|
|
418
|
+
queryset : QuerySet
|
|
419
|
+
Base queryset to optimize.
|
|
420
|
+
query_data : ModelQuerySchema
|
|
421
|
+
Query configuration with select_related/prefetch_related lists.
|
|
422
|
+
is_for_read : bool
|
|
423
|
+
Whether to include model-level relation discovery.
|
|
424
|
+
|
|
425
|
+
Returns
|
|
426
|
+
-------
|
|
427
|
+
QuerySet
|
|
428
|
+
Optimized queryset.
|
|
429
|
+
"""
|
|
430
|
+
select_related = (
|
|
431
|
+
query_data.select_related + self.get_select_relateds()
|
|
432
|
+
if is_for_read
|
|
433
|
+
else query_data.select_related
|
|
434
|
+
)
|
|
435
|
+
prefetch_related = (
|
|
436
|
+
query_data.prefetch_related + self.get_reverse_relations()
|
|
437
|
+
if is_for_read
|
|
438
|
+
else query_data.prefetch_related
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if select_related:
|
|
442
|
+
queryset = queryset.select_related(*select_related)
|
|
443
|
+
if prefetch_related:
|
|
444
|
+
queryset = queryset.prefetch_related(*prefetch_related)
|
|
445
|
+
|
|
446
|
+
return queryset
|
|
447
|
+
|
|
448
|
+
def get_reverse_relations(self) -> list[str]:
|
|
449
|
+
"""
|
|
450
|
+
Discover reverse relation names for safe prefetching.
|
|
451
|
+
|
|
452
|
+
Returns
|
|
453
|
+
-------
|
|
454
|
+
list[str]
|
|
455
|
+
Relation attribute names.
|
|
456
|
+
"""
|
|
457
|
+
reverse_rels = []
|
|
458
|
+
for f in self.serializable_fields:
|
|
459
|
+
field_obj = getattr(self.model, f)
|
|
460
|
+
if isinstance(field_obj, ManyToManyDescriptor):
|
|
461
|
+
reverse_rels.append(f)
|
|
462
|
+
continue
|
|
463
|
+
if isinstance(field_obj, ReverseManyToOneDescriptor):
|
|
464
|
+
reverse_rels.append(field_obj.field._related_name)
|
|
465
|
+
continue
|
|
466
|
+
if isinstance(field_obj, ReverseOneToOneDescriptor):
|
|
467
|
+
reverse_rels.append(field_obj.related.name)
|
|
468
|
+
return reverse_rels
|
|
469
|
+
|
|
470
|
+
def get_select_relateds(self) -> list[str]:
|
|
471
|
+
"""
|
|
472
|
+
Discover forward relation names for safe select_related.
|
|
473
|
+
|
|
474
|
+
Returns
|
|
475
|
+
-------
|
|
476
|
+
list[str]
|
|
477
|
+
Relation attribute names.
|
|
478
|
+
"""
|
|
479
|
+
select_rels = []
|
|
480
|
+
for f in self.serializable_fields:
|
|
481
|
+
field_obj = getattr(self.model, f)
|
|
482
|
+
if isinstance(field_obj, ForwardManyToOneDescriptor):
|
|
483
|
+
select_rels.append(f)
|
|
484
|
+
continue
|
|
485
|
+
if isinstance(field_obj, ForwardOneToOneDescriptor):
|
|
486
|
+
select_rels.append(f)
|
|
487
|
+
return select_rels
|
|
488
|
+
|
|
489
|
+
async def _get_field(self, k: str):
|
|
490
|
+
return (await agetattr(self.model, k)).field
|
|
491
|
+
|
|
492
|
+
def _decode_binary(self, payload: dict, k: str, v: Any, field_obj: models.Field):
|
|
493
|
+
if not isinstance(field_obj, models.BinaryField):
|
|
494
|
+
return
|
|
495
|
+
try:
|
|
496
|
+
payload[k] = base64.b64decode(v)
|
|
497
|
+
except Exception as exc:
|
|
498
|
+
raise SerializeError({k: ". ".join(exc.args)}, 400)
|
|
499
|
+
|
|
500
|
+
async def _resolve_fk(
|
|
501
|
+
self,
|
|
502
|
+
request: HttpRequest,
|
|
503
|
+
payload: dict,
|
|
504
|
+
k: str,
|
|
505
|
+
v: Any,
|
|
506
|
+
field_obj: models.Field,
|
|
507
|
+
):
|
|
508
|
+
if not isinstance(field_obj, models.ForeignKey):
|
|
509
|
+
return
|
|
510
|
+
rel_util = ModelUtil(field_obj.related_model)
|
|
511
|
+
rel = await rel_util.get_object(request, v, with_qs_request=False)
|
|
512
|
+
payload[k] = rel
|
|
513
|
+
|
|
514
|
+
async def _bump_object_from_schema(
|
|
515
|
+
self, obj: type["ModelSerializer"] | models.Model, schema: Schema
|
|
516
|
+
):
|
|
517
|
+
return (await sync_to_async(schema.from_orm)(obj)).model_dump(mode="json")
|
|
518
|
+
|
|
519
|
+
def _validate_read_params(self, request: HttpRequest, query_data: QuerySchema):
|
|
520
|
+
"""Validate required parameters for read operations."""
|
|
521
|
+
if request is None:
|
|
522
|
+
raise SerializeError(
|
|
523
|
+
{"request": "must be provided when object is not given"}, 400
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
if query_data is None:
|
|
527
|
+
raise SerializeError(
|
|
528
|
+
{"query_data": "must be provided when object is not given"}, 400
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
if (
|
|
532
|
+
hasattr(query_data, "filters")
|
|
533
|
+
and hasattr(query_data, "getters")
|
|
534
|
+
and query_data.filters
|
|
535
|
+
and query_data.getters
|
|
536
|
+
):
|
|
537
|
+
raise SerializeError(
|
|
538
|
+
{"query_data": "cannot contain both filters and getters"}, 400
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
async def _handle_query_mode(
|
|
542
|
+
self,
|
|
543
|
+
request: HttpRequest,
|
|
544
|
+
query_data: QuerySchema,
|
|
545
|
+
schema: Schema,
|
|
546
|
+
is_for_read: bool,
|
|
547
|
+
):
|
|
548
|
+
"""Handle different query modes (filters vs getters)."""
|
|
549
|
+
if hasattr(query_data, "filters") and query_data.filters:
|
|
550
|
+
return await self._serialize_queryset(
|
|
551
|
+
request, query_data, schema, is_for_read
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if hasattr(query_data, "getters") and query_data.getters:
|
|
555
|
+
return await self._serialize_single_object(
|
|
556
|
+
request, query_data, schema, is_for_read
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
raise SerializeError(
|
|
560
|
+
{"query_data": "must contain either filters or getters"}, 400
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
async def _serialize_queryset(
|
|
564
|
+
self,
|
|
565
|
+
request: HttpRequest,
|
|
566
|
+
query_data: QuerySchema,
|
|
567
|
+
schema: Schema,
|
|
568
|
+
is_for_read: bool,
|
|
569
|
+
):
|
|
570
|
+
"""Serialize a queryset of objects."""
|
|
571
|
+
objs = await self.get_objects(
|
|
572
|
+
request, query_data=query_data, is_for_read=is_for_read
|
|
573
|
+
)
|
|
574
|
+
return [await self._bump_object_from_schema(obj, schema) async for obj in objs]
|
|
575
|
+
|
|
576
|
+
async def _serialize_single_object(
|
|
577
|
+
self,
|
|
578
|
+
request: HttpRequest,
|
|
579
|
+
query_data: QuerySchema,
|
|
580
|
+
obj_schema: Schema,
|
|
581
|
+
is_for_read: bool,
|
|
582
|
+
):
|
|
583
|
+
"""Serialize a single object."""
|
|
584
|
+
obj = await self.get_object(
|
|
585
|
+
request, query_data=query_data, is_for_read=is_for_read
|
|
586
|
+
)
|
|
587
|
+
return await self._bump_object_from_schema(obj, obj_schema)
|
|
588
|
+
|
|
589
|
+
async def parse_input_data(self, request: HttpRequest, data: Schema):
|
|
590
|
+
"""
|
|
591
|
+
Transform inbound schema data to a model-ready payload.
|
|
592
|
+
|
|
593
|
+
Steps
|
|
594
|
+
-----
|
|
595
|
+
- Strip custom fields (retain separately).
|
|
596
|
+
- Drop optional fields with None (ModelSerializer only).
|
|
597
|
+
- Decode BinaryField base64 values.
|
|
598
|
+
- Resolve ForeignKey ids to model instances.
|
|
599
|
+
|
|
600
|
+
Parameters
|
|
601
|
+
----------
|
|
602
|
+
request : HttpRequest
|
|
603
|
+
data : Schema
|
|
604
|
+
Incoming validated schema instance.
|
|
605
|
+
|
|
606
|
+
Returns
|
|
607
|
+
-------
|
|
608
|
+
tuple[dict, dict]
|
|
609
|
+
(payload_without_customs, customs_dict)
|
|
610
|
+
|
|
611
|
+
Raises
|
|
612
|
+
------
|
|
613
|
+
SerializeError
|
|
614
|
+
On base64 decoding failure.
|
|
615
|
+
"""
|
|
616
|
+
payload = data.model_dump(mode="json")
|
|
617
|
+
|
|
618
|
+
is_serializer = isinstance(self.model, ModelSerializerMeta)
|
|
619
|
+
|
|
620
|
+
# Collect custom and optional fields (only if ModelSerializerMeta)
|
|
621
|
+
customs: dict[str, Any] = {}
|
|
622
|
+
optionals: list[str] = []
|
|
623
|
+
if is_serializer:
|
|
624
|
+
customs = {
|
|
625
|
+
k: v
|
|
626
|
+
for k, v in payload.items()
|
|
627
|
+
if self.model.is_custom(k) and k not in self.model_fields
|
|
628
|
+
}
|
|
629
|
+
optionals = [
|
|
630
|
+
k for k, v in payload.items() if self.model.is_optional(k) and v is None
|
|
631
|
+
]
|
|
632
|
+
|
|
633
|
+
skip_keys = set()
|
|
634
|
+
if is_serializer:
|
|
635
|
+
# Keys to skip during model field processing
|
|
636
|
+
skip_keys = {
|
|
637
|
+
k
|
|
638
|
+
for k, v in payload.items()
|
|
639
|
+
if (self.model.is_custom(k) and k not in self.model_fields)
|
|
640
|
+
or (self.model.is_optional(k) and v is None)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
# Process payload fields
|
|
644
|
+
for k, v in payload.items():
|
|
645
|
+
if k in skip_keys:
|
|
646
|
+
continue
|
|
647
|
+
field_obj = await self._get_field(k)
|
|
648
|
+
self._decode_binary(payload, k, v, field_obj)
|
|
649
|
+
await self._resolve_fk(request, payload, k, v, field_obj)
|
|
650
|
+
|
|
651
|
+
# Preserve original exclusion semantics (customs if present else optionals)
|
|
652
|
+
exclude_keys = customs.keys() or optionals
|
|
653
|
+
new_payload = {k: v for k, v in payload.items() if k not in exclude_keys}
|
|
654
|
+
|
|
655
|
+
return new_payload, customs
|
|
656
|
+
|
|
657
|
+
async def create_s(self, request: HttpRequest, data: Schema, obj_schema: Schema):
|
|
658
|
+
"""
|
|
659
|
+
Create a new instance and return serialized output.
|
|
660
|
+
|
|
661
|
+
Applies custom_actions + post_create hooks if available.
|
|
662
|
+
|
|
663
|
+
Parameters
|
|
664
|
+
----------
|
|
665
|
+
request : HttpRequest
|
|
666
|
+
data : Schema
|
|
667
|
+
Input schema instance.
|
|
668
|
+
obj_schema : Schema
|
|
669
|
+
Read schema class for output.
|
|
670
|
+
|
|
671
|
+
Returns
|
|
672
|
+
-------
|
|
673
|
+
dict
|
|
674
|
+
Serialized created object.
|
|
675
|
+
"""
|
|
676
|
+
payload, customs = await self.parse_input_data(request, data)
|
|
677
|
+
pk = (await self.model.objects.acreate(**payload)).pk
|
|
678
|
+
obj = await self.get_object(request, pk)
|
|
679
|
+
if isinstance(self.model, ModelSerializerMeta):
|
|
680
|
+
await asyncio.gather(obj.custom_actions(customs), obj.post_create())
|
|
681
|
+
return await self.read_s(obj_schema, request, obj)
|
|
682
|
+
|
|
683
|
+
async def _read_s(
|
|
684
|
+
self,
|
|
685
|
+
schema: Schema,
|
|
686
|
+
request: HttpRequest = None,
|
|
687
|
+
instance: models.QuerySet[type["ModelSerializer"] | models.Model]
|
|
688
|
+
| type["ModelSerializer"]
|
|
689
|
+
| models.Model = None,
|
|
690
|
+
query_data: QuerySchema = None,
|
|
691
|
+
is_for_read: bool = False,
|
|
692
|
+
):
|
|
693
|
+
"""
|
|
694
|
+
Internal serialization method handling both single instances and querysets.
|
|
695
|
+
|
|
696
|
+
Parameters
|
|
697
|
+
----------
|
|
698
|
+
schema : Schema
|
|
699
|
+
Read schema class for serialization.
|
|
700
|
+
request : HttpRequest, optional
|
|
701
|
+
HTTP request object, required when instance is None.
|
|
702
|
+
instance : QuerySet | Model, optional
|
|
703
|
+
Instance(s) to serialize. If None, fetches based on query_data.
|
|
704
|
+
query_data : QuerySchema, optional
|
|
705
|
+
Query parameters for fetching objects when instance is None.
|
|
706
|
+
is_for_read : bool, optional
|
|
707
|
+
Whether to apply read-specific query optimizations.
|
|
708
|
+
|
|
709
|
+
Returns
|
|
710
|
+
-------
|
|
711
|
+
dict | list[dict]
|
|
712
|
+
Serialized instance(s).
|
|
713
|
+
|
|
714
|
+
Raises
|
|
715
|
+
------
|
|
716
|
+
SerializeError
|
|
717
|
+
If schema is None or validation fails.
|
|
718
|
+
"""
|
|
719
|
+
if schema is None:
|
|
720
|
+
raise SerializeError({"schema": "must be provided"}, 400)
|
|
721
|
+
|
|
722
|
+
if instance is not None:
|
|
723
|
+
if isinstance(instance, models.QuerySet):
|
|
724
|
+
return [
|
|
725
|
+
await self._bump_object_from_schema(obj, schema)
|
|
726
|
+
async for obj in instance
|
|
727
|
+
]
|
|
728
|
+
return await self._bump_object_from_schema(instance, schema)
|
|
729
|
+
|
|
730
|
+
self._validate_read_params(request, query_data)
|
|
731
|
+
return await self._handle_query_mode(request, query_data, schema, is_for_read)
|
|
732
|
+
|
|
733
|
+
async def read_s(
|
|
734
|
+
self,
|
|
735
|
+
schema: Schema,
|
|
736
|
+
request: HttpRequest = None,
|
|
737
|
+
instance: type["ModelSerializer"] = None,
|
|
738
|
+
query_data: ObjectQuerySchema = None,
|
|
739
|
+
is_for_read: bool = False,
|
|
740
|
+
) -> dict:
|
|
741
|
+
"""
|
|
742
|
+
Serialize a single model instance or fetch and serialize using query parameters.
|
|
743
|
+
|
|
744
|
+
This method handles single-object serialization. It can serialize a provided
|
|
745
|
+
instance directly or fetch and serialize a single object using query_data.getters.
|
|
746
|
+
|
|
747
|
+
Parameters
|
|
748
|
+
----------
|
|
749
|
+
schema : Schema
|
|
750
|
+
Read schema class for serialization output.
|
|
751
|
+
request : HttpRequest, optional
|
|
752
|
+
HTTP request object, required when instance is None.
|
|
753
|
+
instance : ModelSerializer | Model, optional
|
|
754
|
+
Single instance to serialize. If None, fetched based on query_data.
|
|
755
|
+
query_data : ObjectQuerySchema, optional
|
|
756
|
+
Query parameters with getters for single object lookup.
|
|
757
|
+
Required when instance is None.
|
|
758
|
+
is_for_read : bool, optional
|
|
759
|
+
Whether to apply read-specific query optimizations. Defaults to False.
|
|
760
|
+
|
|
761
|
+
Returns
|
|
762
|
+
-------
|
|
763
|
+
dict
|
|
764
|
+
Serialized model instance as dictionary.
|
|
765
|
+
|
|
766
|
+
Raises
|
|
767
|
+
------
|
|
768
|
+
SerializeError
|
|
769
|
+
- If schema is None
|
|
770
|
+
- If instance is None and request or query_data is None
|
|
771
|
+
- If query_data validation fails
|
|
772
|
+
NotFoundError
|
|
773
|
+
If using getters and no matching object is found.
|
|
774
|
+
|
|
775
|
+
Notes
|
|
776
|
+
-----
|
|
777
|
+
- Uses Pydantic's from_orm() with mode="json" for serialization
|
|
778
|
+
- When instance is provided, request and query_data are ignored
|
|
779
|
+
- Query optimizations applied when is_for_read=True
|
|
780
|
+
"""
|
|
781
|
+
return await self._read_s(
|
|
782
|
+
schema,
|
|
783
|
+
request,
|
|
784
|
+
instance,
|
|
785
|
+
query_data,
|
|
786
|
+
is_for_read,
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
async def list_read_s(
|
|
790
|
+
self,
|
|
791
|
+
schema: Schema,
|
|
792
|
+
request: HttpRequest = None,
|
|
793
|
+
instances: models.QuerySet[type["ModelSerializer"] | models.Model] = None,
|
|
794
|
+
query_data: ObjectsQuerySchema = None,
|
|
795
|
+
is_for_read: bool = False,
|
|
796
|
+
) -> list[dict]:
|
|
797
|
+
"""
|
|
798
|
+
Serialize multiple model instances or fetch and serialize using query parameters.
|
|
799
|
+
|
|
800
|
+
This method handles queryset serialization. It can serialize provided instances
|
|
801
|
+
directly or fetch and serialize multiple objects using query_data.filters.
|
|
802
|
+
|
|
803
|
+
Parameters
|
|
804
|
+
----------
|
|
805
|
+
schema : Schema
|
|
806
|
+
Read schema class for serialization output.
|
|
807
|
+
request : HttpRequest, optional
|
|
808
|
+
HTTP request object, required when instances is None.
|
|
809
|
+
instances : QuerySet, optional
|
|
810
|
+
Queryset of instances to serialize. If None, fetched based on query_data.
|
|
811
|
+
query_data : ObjectsQuerySchema, optional
|
|
812
|
+
Query parameters with filters for multiple object lookup.
|
|
813
|
+
Required when instances is None.
|
|
814
|
+
is_for_read : bool, optional
|
|
815
|
+
Whether to apply read-specific query optimizations. Defaults to False.
|
|
816
|
+
|
|
817
|
+
Returns
|
|
818
|
+
-------
|
|
819
|
+
list[dict]
|
|
820
|
+
List of serialized model instances as dictionaries.
|
|
821
|
+
|
|
822
|
+
Raises
|
|
823
|
+
------
|
|
824
|
+
SerializeError
|
|
825
|
+
- If schema is None
|
|
826
|
+
- If instances is None and request or query_data is None
|
|
827
|
+
- If query_data validation fails
|
|
828
|
+
|
|
829
|
+
Notes
|
|
830
|
+
-----
|
|
831
|
+
- Uses Pydantic's from_orm() with mode="json" for serialization
|
|
832
|
+
- When instances is provided, request and query_data are ignored
|
|
833
|
+
- Query optimizations applied when is_for_read=True
|
|
834
|
+
- Processes queryset asynchronously for efficiency
|
|
835
|
+
"""
|
|
836
|
+
return await self._read_s(
|
|
837
|
+
schema,
|
|
838
|
+
request,
|
|
839
|
+
instances,
|
|
840
|
+
query_data,
|
|
841
|
+
is_for_read,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
async def update_s(
|
|
845
|
+
self, request: HttpRequest, data: Schema, pk: int | str, obj_schema: Schema
|
|
846
|
+
):
|
|
847
|
+
"""
|
|
848
|
+
Update an existing instance and return serialized output.
|
|
849
|
+
|
|
850
|
+
Only non-null fields are applied to the instance.
|
|
851
|
+
|
|
852
|
+
Parameters
|
|
853
|
+
----------
|
|
854
|
+
request : HttpRequest
|
|
855
|
+
data : Schema
|
|
856
|
+
Input update schema instance.
|
|
857
|
+
pk : int | str
|
|
858
|
+
Primary key of target object.
|
|
859
|
+
obj_schema : Schema
|
|
860
|
+
Read schema class for output.
|
|
861
|
+
|
|
862
|
+
Returns
|
|
863
|
+
-------
|
|
864
|
+
dict
|
|
865
|
+
Serialized updated object.
|
|
866
|
+
"""
|
|
867
|
+
obj = await self.get_object(request, pk)
|
|
868
|
+
payload, customs = await self.parse_input_data(request, data)
|
|
869
|
+
for k, v in payload.items():
|
|
870
|
+
if v is not None:
|
|
871
|
+
setattr(obj, k, v)
|
|
872
|
+
if isinstance(self.model, ModelSerializerMeta):
|
|
873
|
+
await obj.custom_actions(customs)
|
|
874
|
+
await obj.asave()
|
|
875
|
+
updated_object = await self.get_object(request, pk)
|
|
876
|
+
return await self.read_s(obj_schema, request, updated_object)
|
|
877
|
+
|
|
878
|
+
async def delete_s(self, request: HttpRequest, pk: int | str):
|
|
879
|
+
"""
|
|
880
|
+
Delete an instance by primary key.
|
|
881
|
+
|
|
882
|
+
Parameters
|
|
883
|
+
----------
|
|
884
|
+
request : HttpRequest
|
|
885
|
+
pk : int | str
|
|
886
|
+
Primary key.
|
|
887
|
+
|
|
888
|
+
Returns
|
|
889
|
+
-------
|
|
890
|
+
None
|
|
891
|
+
"""
|
|
892
|
+
obj = await self.get_object(request, pk)
|
|
893
|
+
await obj.adelete()
|
|
894
|
+
return None
|