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.
Files changed (22) hide show
  1. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/PKG-INFO +2 -2
  2. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/__init__.py +1 -1
  3. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/api.py +0 -2
  4. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/auth.py +3 -4
  5. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/decorators.py +3 -0
  6. django_ninja_aio_crud-2.0.0rc1/ninja_aio/helpers/__init__.py +0 -0
  7. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/helpers/api.py +11 -6
  8. django_ninja_aio_crud-2.0.0rc1/ninja_aio/helpers/query.py +103 -0
  9. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/models.py +504 -168
  10. django_ninja_aio_crud-2.0.0rc1/ninja_aio/schemas/__init__.py +23 -0
  11. django_ninja_aio_crud-2.0.0rc1/ninja_aio/schemas/api.py +24 -0
  12. django_ninja_aio_crud-2.0.0rc1/ninja_aio/schemas/generics.py +5 -0
  13. 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
  14. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/views.py +33 -16
  15. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/pyproject.toml +1 -1
  16. django_ninja_aio_crud-1.0.5/ninja_aio/helpers/__init__.py +0 -3
  17. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/LICENSE +0 -0
  18. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/README.md +0 -0
  19. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/exceptions.py +0 -0
  20. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/parsers.py +0 -0
  21. {django_ninja_aio_crud-1.0.5 → django_ninja_aio_crud-2.0.0rc1}/ninja_aio/renders.py +0 -0
  22. {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: 1.0.5
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.4.5
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"
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "1.0.5"
3
+ __version__ = "2.0.0-rc1"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -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 as exc:
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 as exc:
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):
@@ -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).get_object(request, filters={"pk": obj_pk})
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
- related_qs = await query_handler(related_qs, filters.model_dump())
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
- await rel_util.read_s(request, rel_obj, related_schema)
332
- async for rel_obj in related_qs
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
- filters: dict = None,
183
- getters: dict = None,
309
+ query_data: ObjectQuerySchema = None,
184
310
  with_qs_request=True,
185
- ) -> (
186
- type["ModelSerializer"]
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 instance (by pk/getters) or a queryset if no lookup criteria.
314
+ Retrieve a single object with optimized database queries.
192
315
 
193
- Applies queryset_request (if ModelSerializerMeta), select_related, and
194
- prefetch_related on discovered reverse relations.
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
- filters : dict, optional
202
- Additional filter kwargs.
203
- getters : dict, optional
204
- Field lookups combined with pk lookup.
205
- with_qs_request : bool
206
- Whether to apply model-level queryset_request hook.
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
- Model | QuerySet
211
- Instance if lookup provided; otherwise queryset.
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 instance not found by lookup criteria.
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
- obj_qs = self.model.objects.select_related()
223
- if isinstance(self.model, ModelSerializerMeta) and with_qs_request:
224
- obj_qs = await self.model.queryset_request(request)
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
- obj_qs = obj_qs.prefetch_related(*self.get_reverse_relations())
227
- if filters:
228
- obj_qs = obj_qs.filter(**filters)
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
- if not get_q:
231
- return obj_qs
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 _extract_field_obj(self, field_name: str):
288
- """
289
- Return the underlying Django Field (if any) for a given attribute name.
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 _should_process_nested(self, value: Any, field_obj: Any) -> bool:
299
- """
300
- Determine if a payload entry represents a nested FK / O2O relation dict.
301
- """
302
- if not isinstance(value, dict):
303
- return False
304
- return isinstance(field_obj, (models.ForeignKey, models.OneToOneField))
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
- async def _fetch_related_instance(
307
- self, request, field_obj: models.Field, nested_dict: dict
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
- Resolve the related instance from its primary key inside the nested dict.
311
- """
312
- rel_util = ModelUtil(field_obj.related_model)
313
- rel_pk = nested_dict.get(rel_util.model_pk_name)
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
- async def _rewrite_nested_foreign_keys(self, rel_obj, nested_dict: dict):
317
- """
318
- Rewrite foreign key keys inside a nested dict from <key> to <key>_id.
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, obj_schema)
670
+ return await self.read_s(obj_schema, request, obj)
455
671
 
456
- async def read_s(
672
+ async def _read_s(
457
673
  self,
458
- request: HttpRequest,
459
- obj: type["ModelSerializer"],
460
- obj_schema: Schema,
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
- Serialize an existing instance with the provided read schema.
683
+ Internal serialization method handling both single instances and querysets.
464
684
 
465
685
  Parameters
466
686
  ----------
467
- request : HttpRequest
468
- obj : Model
469
- Target instance.
470
- obj_schema : Schema
471
- Read schema class.
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 payload.
753
+ Serialized model instance as dictionary.
477
754
 
478
755
  Raises
479
756
  ------
480
757
  SerializeError
481
- If obj_schema not provided.
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
- if obj_schema is None:
484
- raise SerializeError({"obj_schema": "must be provided"}, 400)
485
- return await self.parse_output_data(
486
- request, await sync_to_async(obj_schema.from_orm)(obj)
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, obj_schema)
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.objects.select_related().all()
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
- if self._state.adding:
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 self._state.adding:
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 = []
@@ -0,0 +1,5 @@
1
+ from pydantic import RootModel
2
+
3
+
4
+ class GenericMessageSchema(RootModel[dict[str, str]]):
5
+ root: dict[str, str]
@@ -1,36 +1,9 @@
1
1
  from typing import Optional, Type
2
2
 
3
3
  from ninja import Schema
4
- from .models import ModelSerializer
4
+ from ninja_aio.types import ModelSerializerMeta
5
5
  from django.db.models import Model
6
- from pydantic import BaseModel, RootModel, ConfigDict, model_validator
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: Type[ModelSerializer] | Type[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 issubclass(model, ModelSerializer):
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 = ModelUtil(self.model)
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: (int | str, ...)}, "PathSchema"
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.model.objects.select_related()
321
- if isinstance(self.model, ModelSerializerMeta):
322
- qs = await self.model.queryset_request(request)
323
- rels = self.model_util.get_reverse_relations()
324
- if len(rels) > 0:
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
- objs = [
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
- obj = await self.model_util.get_object(request, self._get_pk(pk))
351
- return await self.model_util.read_s(request, obj, self.schema_out)
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.4.5", "joserfc >=1.0.0, <= 1.4.1", "orjson >= 3.10.7, <= 3.11.4"]
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
 
@@ -1,3 +0,0 @@
1
- from .api import ManyToManyAPI
2
-
3
- __all__ = ["ManyToManyAPI"]