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.
@@ -0,0 +1,506 @@
1
+ import asyncio
2
+ from typing import Coroutine
3
+
4
+ from django.http import HttpRequest
5
+ from ninja import Path, Query
6
+ from ninja.pagination import paginate
7
+ from ninja_aio.decorators import unique_view, decorate_view
8
+ from ninja_aio.models import ModelSerializer, ModelUtil
9
+ from ninja_aio.schemas.helpers import ObjectsQuerySchema
10
+ from ninja_aio.schemas import (
11
+ GenericMessageSchema,
12
+ M2MRelationSchema,
13
+ M2MSchemaIn,
14
+ M2MSchemaOut,
15
+ M2MAddSchemaIn,
16
+ M2MRemoveSchemaIn,
17
+ )
18
+ from django.db.models import QuerySet, Model
19
+
20
+
21
+ class ManyToManyAPI:
22
+ """
23
+ ManyToManyAPI
24
+ -------------
25
+ WARNING (Internal Use Only):
26
+ This helper is currently intended solely for internal purposes. Its API,
27
+ behaviors, and response formats may change without notice. Do not rely on
28
+ it as a stable public interface.
29
+
30
+ Utility class that dynamically attaches asynchronous Many-To-Many (M2M) management
31
+ endpoints (GET / ADD / REMOVE) to a provided APIViewSet router in a Django Ninja
32
+ async CRUD context.
33
+
34
+ It inspects a list of M2MRelationSchema definitions and, for each relation, builds:
35
+ - An optional paginated GET endpoint to list related objects.
36
+ - An optional POST endpoint to add and/or remove related object primary keys.
37
+
38
+ Core behaviors:
39
+ - Dynamically generates per-relation filter schemas for query parameters.
40
+ - Supports custom per-relation query filtering handlers on the parent view set
41
+ via a `{related_name}_query_params_handler` coroutine for GET list filters.
42
+ - Supports custom per-relation object resolution for add/remove validation via
43
+ `{related_name}_query_handler(request, pk, instance)` used during POST to
44
+ resolve a single related object before mutation.
45
+ - Validates requested add/remove primary keys, producing granular success and
46
+ error feedback.
47
+ - Performs add/remove operations concurrently using asyncio.gather when both
48
+ types of operations are requested in the same call.
49
+
50
+ Attributes established at initialization:
51
+ relations: list of M2MRelationSchema defining each relation.
52
+ view_set: The parent APIViewSet instance from which router, pagination, model util,
53
+ and path schema are derived.
54
+ router: Ninja router used to register generated endpoints.
55
+ pagination_class: Pagination class used for GET related endpoints.
56
+ path_schema: Pydantic schema used to validate path parameters (e.g., primary key).
57
+ related_model_util: A ModelUtil instance cloned from the parent view set to access
58
+ base object retrieval helpers.
59
+ relations_filters_schemas: Mapping of related_name -> generated Pydantic filter schema.
60
+
61
+ Generated endpoint naming conventions:
62
+ GET -> get_{base_model_name}_{relation_path}
63
+ POST -> manage_{base_model_name}_{relation_path}
64
+
65
+ Endpoint registration details:
66
+ - GET: registered at `{retrieve_path}{relation_path}` with pagination; accepts Query filters.
67
+ - POST: registered at `{retrieve_path}{relation_path}/` (trailing slash) to manage add/remove.
68
+
69
+ All responses standardize success and error reporting for POST as:
70
+ {
71
+ "results": {"count": int, "details": [str, ...]},
72
+ "errors": {"count": int, "details": [str, ...]}
73
+ }
74
+
75
+ Concurrency note:
76
+ - Add and remove operations are executed concurrently when both lists are non-empty,
77
+ minimizing round-trip latency for bulk mutations.
78
+ - Uses related_manager.aadd(...) and related_manager.aremove(...) inside asyncio.gather.
79
+
80
+ Error semantics:
81
+ - Missing related objects: reported individually.
82
+ - Invalid operation context (e.g., removing objects not currently related or adding
83
+ objects already related) reported per primary key.
84
+ - Successful operations yield a corresponding success detail string per PK.
85
+
86
+ Security / auth:
87
+ - Each relation may optionally override auth via its schema; otherwise falls back
88
+ to a default configured on the instance (self.default_auth).
89
+
90
+ Pagination:
91
+ - Applied only to GET related endpoints via @paginate(self.pagination_class).
92
+
93
+ Extensibility:
94
+ - Provide custom query param handling by defining an async method on the parent
95
+ view set: `<related_name>_query_params_handler(self, queryset, filters_dict)`.
96
+ - Provide custom per-PK resolution for POST validation by defining an async method:
97
+ `<related_name>_query_handler(self, request, pk, instance)` returning a queryset,
98
+ from which .afirst() is used to resolve the single target object.
99
+ - Customize relation filtering schema via each relation's `filters` definition.
100
+
101
+ -----------------------------------------------------------------------
102
+
103
+ __init__(relations, view_set)
104
+ Initialize the M2M API helper by binding core utilities from the provided view set
105
+ and precomputing filter schemas per relation.
106
+
107
+ Parameters:
108
+ relations (list[M2MRelationSchema]): Definitions for each M2M relation to expose.
109
+ view_set (APIViewSet): Parent view set containing model utilities and router.
110
+
111
+ Side effects:
112
+ - Captures router, pagination class, path schema from view_set.
113
+ - Clones ModelUtil for related model operations.
114
+ - Pre-generates filter schemas for each relation (if filters declared).
115
+
116
+ -----------------------------------------------------------------------
117
+
118
+ _generate_m2m_filters_schemas()
119
+ Create a mapping of related_name -> Pydantic schema used for query filtering in
120
+ GET related endpoints. If a relation has no filters specified, an empty schema
121
+ (dict) is used.
122
+
123
+ Returns:
124
+ dict[str, BaseModel]: Generated schemas keyed by related_name.
125
+
126
+ -----------------------------------------------------------------------
127
+
128
+ _get_query_params_handler(related_name)
129
+ Retrieve an optional per-relation query handler from the parent view set for GET list filters.
130
+ Naming convention: `<related_name}_query_params_handler`.
131
+
132
+ Parameters:
133
+ related_name (str): The relation's attribute name on the base model.
134
+
135
+ Returns:
136
+ Coroutine | None: Handler to transform or filter the queryset based on query params.
137
+
138
+ -----------------------------------------------------------------------
139
+
140
+ _get_query_handler(related_name)
141
+ Retrieve an optional per-relation single-object resolution handler from the parent view set
142
+ used during POST add/remove validation. If present, it receives `(request, pk, instance)` and
143
+ should return a queryset from which `.afirst()` will resolve the target object.
144
+
145
+ Parameters:
146
+ related_name (str): The relation's attribute name on the base model.
147
+
148
+ Returns:
149
+ Coroutine | None: Handler to resolve a single related object by pk.
150
+
151
+ -----------------------------------------------------------------------
152
+
153
+ _check_m2m_objs(request, objs_pks, related_model, related_manager, related_name, instance, remove=False)
154
+ Validate requested primary keys for add/remove operations against the current
155
+ relation state. Performs existence checks and logical consistency (e.g., prevents
156
+ adding already-related objects or removing non-related objects). Uses `_get_query_handler`
157
+ when available; otherwise falls back to ModelUtil(...).get_objects(...) to resolve by pk.
158
+
159
+ Parameters:
160
+ request (HttpRequest): Incoming request context (passed to ModelUtil for access control).
161
+ objs_pks (list): List of primary keys to add or remove.
162
+ related_model (ModelSerializer | Model): Model class or serializer used to resolve objects.
163
+ related_manager (QuerySet): Related manager for the base object's M2M field.
164
+ related_name (str): M2M field name on the base object.
165
+ instance (ModelSerializer | Model): Base object instance (owner of the relation).
166
+ remove (bool): If True, treat operation as removal validation.
167
+
168
+ Returns:
169
+ tuple[list[str], list[str], list[Model]]:
170
+ errors -> List of error messages per invalid PK.
171
+ objs_detail -> List of success detail messages per valid PK.
172
+ objs -> List of resolved model instances to process.
173
+
174
+ Error cases:
175
+ - Object not found.
176
+ - Object presence mismatch (attempting wrong operation given relation membership).
177
+
178
+ -----------------------------------------------------------------------
179
+
180
+ _collect_m2m(request, pks, model, related_manager, related_name, instance, remove=False)
181
+ Wrapper around _check_m2m_objs that short-circuits on empty PK lists.
182
+
183
+ Parameters:
184
+ request (HttpRequest)
185
+ pks (list): Primary keys proposed for mutation.
186
+ model (ModelSerializer | Model)
187
+ related_manager (QuerySet)
188
+ related_name (str)
189
+ instance (ModelSerializer | Model)
190
+ remove (bool): Operation type flag.
191
+
192
+ Returns:
193
+ tuple[list[str], list[str], list[Model]]: See _check_m2m_objs.
194
+
195
+ -----------------------------------------------------------------------
196
+
197
+ _register_get_relation_view(...)
198
+ Registers the GET endpoint for listing related objects. Applies optional
199
+ query params handler for filtering. Uses pagination and serializes via `list_read_s`.
200
+
201
+ -----------------------------------------------------------------------
202
+
203
+ _register_manage_relation_view(...)
204
+ Registers the POST endpoint for adding/removing related objects. Validates via
205
+ `_collect_m2m` and executes mutations concurrently using `aadd`/`aremove` with
206
+ `asyncio.gather`. Aggregates per-PK results and errors into a standardized payload.
207
+
208
+ -----------------------------------------------------------------------
209
+
210
+ _build_views(relation)
211
+ Dynamically define and register the GET and/or POST endpoints for a single M2M
212
+ relation based on the relation's schema flags (get/add/remove). Builds filter
213
+ schemas, resolves path fragments, and binds handlers to the router with unique
214
+ operation IDs.
215
+
216
+ -----------------------------------------------------------------------
217
+
218
+ _add_views()
219
+ Iterates over all declared relations and invokes _build_views to attach endpoints.
220
+
221
+ -----------------------------------------------------------------------
222
+
223
+ Usage Example (conceptual):
224
+ api = ManyToManyAPI(relations=[...], view_set=my_view_set)
225
+ api._add_views() # Registers all endpoints automatically during initialization flow.
226
+ """
227
+
228
+ def __init__(
229
+ self,
230
+ relations: list[M2MRelationSchema],
231
+ view_set,
232
+ ):
233
+ # Import here to avoid circular imports
234
+ from ninja_aio.views import APIViewSet
235
+
236
+ self.relations = relations
237
+ self.view_set: APIViewSet = view_set
238
+ self.router = self.view_set.router
239
+ self.pagination_class = self.view_set.pagination_class
240
+ self.path_schema = self.view_set.path_schema
241
+ self.default_auth = self.view_set.m2m_auth
242
+ self.related_model_util = self.view_set.model_util
243
+ self.relations_filters_schemas = self._generate_m2m_filters_schemas()
244
+
245
+ @property
246
+ def views_action_map(self):
247
+ return {
248
+ (True, True): ("Add or Remove", M2MSchemaIn),
249
+ (True, False): ("Add", M2MAddSchemaIn),
250
+ (False, True): ("Remove", M2MRemoveSchemaIn),
251
+ }
252
+
253
+ def _generate_m2m_filters_schemas(self):
254
+ """
255
+ Build per-relation filters schemas for M2M endpoints.
256
+ """
257
+ return {
258
+ data.related_name: self.view_set._generate_schema(
259
+ {} if not data.filters else data.filters,
260
+ f"{self.related_model_util.model_name}{data.related_name.capitalize()}FiltersSchema",
261
+ )
262
+ for data in self.relations
263
+ }
264
+
265
+ def _get_query_params_handler(self, related_name: str) -> Coroutine | None:
266
+ return getattr(self.view_set, f"{related_name}_query_params_handler", None)
267
+
268
+ def _get_query_handler(self, related_name: str) -> Coroutine | None:
269
+ return getattr(self.view_set, f"{related_name}_query_handler", None)
270
+
271
+ async def _check_m2m_objs(
272
+ self,
273
+ request: HttpRequest,
274
+ objs_pks: list,
275
+ related_model: ModelSerializer | Model,
276
+ related_manager: QuerySet,
277
+ related_name: str,
278
+ instance: ModelSerializer | Model,
279
+ remove: bool = False,
280
+ ):
281
+ """
282
+ Validate requested add/remove pk list for M2M operations.
283
+ Returns (errors, details, objects_to_process).
284
+ Uses per-PK query handler if available, else falls back to ModelUtil lookup by pk.
285
+ """
286
+ errors, objs_detail, objs = [], [], []
287
+ rel_objs = [rel_obj async for rel_obj in related_manager.select_related().all()]
288
+ rel_model_name = related_model._meta.verbose_name.capitalize()
289
+ for obj_pk in objs_pks:
290
+ if query_handler := self._get_query_handler(related_name):
291
+ rel_obj = await (
292
+ await query_handler(
293
+ request,
294
+ obj_pk,
295
+ instance,
296
+ )
297
+ ).afirst()
298
+ else:
299
+ rel_obj = await (
300
+ await ModelUtil(related_model).get_objects(
301
+ request, query_data=ObjectsQuerySchema(filters={"pk": obj_pk})
302
+ )
303
+ ).afirst()
304
+ if rel_obj is None:
305
+ errors.append(f"{rel_model_name} with pk {obj_pk} not found.")
306
+ continue
307
+ if remove ^ (rel_obj in rel_objs):
308
+ errors.append(
309
+ f"{rel_model_name} with pk {obj_pk} is {'not ' if remove else ''}in {self.related_model_util.model_name}"
310
+ )
311
+ continue
312
+ objs.append(rel_obj)
313
+ objs_detail.append(
314
+ f"{rel_model_name} with pk {obj_pk} successfully {'removed' if remove else 'added'}"
315
+ )
316
+ return errors, objs_detail, objs
317
+
318
+ async def _collect_m2m(
319
+ self,
320
+ request: HttpRequest,
321
+ pks: list,
322
+ reletad_model: ModelSerializer | Model,
323
+ related_manager: QuerySet,
324
+ related_name: str,
325
+ instance: ModelSerializer | Model,
326
+ remove: bool = False,
327
+ ):
328
+ if not pks:
329
+ return ([], [], [])
330
+ return await self._check_m2m_objs(
331
+ request,
332
+ pks,
333
+ reletad_model,
334
+ related_manager,
335
+ related_name,
336
+ instance,
337
+ remove=remove,
338
+ )
339
+
340
+ def _get_api_path(self, rel_path: str, append_slash: bool = None) -> str:
341
+ append_slash = append_slash if append_slash is not None else True
342
+ path = (
343
+ f"{self.view_set.path_retrieve}{rel_path}/"
344
+ if rel_path.startswith("/")
345
+ else f"{self.view_set.path_retrieve}/{rel_path}/"
346
+ )
347
+ if not append_slash:
348
+ path = path.rstrip("/")
349
+ return path
350
+
351
+ def _register_get_relation_view(
352
+ self,
353
+ *,
354
+ related_name: str,
355
+ m2m_auth,
356
+ rel_util: ModelUtil,
357
+ rel_path: str,
358
+ related_schema,
359
+ filters_schema,
360
+ append_slash: bool,
361
+ ):
362
+ @self.router.get(
363
+ self._get_api_path(rel_path, append_slash=append_slash),
364
+ response={
365
+ 200: list[related_schema],
366
+ self.view_set.error_codes: GenericMessageSchema,
367
+ },
368
+ auth=m2m_auth,
369
+ summary=f"Get {rel_util.model._meta.verbose_name_plural.capitalize()}",
370
+ description=f"Get all related {rel_util.model._meta.verbose_name_plural.capitalize()}",
371
+ )
372
+ @decorate_view(
373
+ unique_view(f"get_{self.related_model_util.model_name}_{rel_path}"),
374
+ paginate(self.pagination_class),
375
+ )
376
+ async def get_related(
377
+ request: HttpRequest,
378
+ pk: Path[self.path_schema], # type: ignore
379
+ filters: Query[filters_schema] = None, # type: ignore
380
+ ):
381
+ obj = await self.related_model_util.get_object(
382
+ request, self.view_set._get_pk(pk)
383
+ )
384
+ related_manager = getattr(obj, related_name)
385
+ related_qs = related_manager.all()
386
+
387
+ query_handler = self._get_query_params_handler(related_name)
388
+ if filters is not None and query_handler:
389
+ if asyncio.iscoroutinefunction(query_handler):
390
+ related_qs = await query_handler(related_qs, filters.model_dump())
391
+ else:
392
+ related_qs = query_handler(related_qs, filters.model_dump())
393
+
394
+ return await rel_util.list_read_s(related_schema, request, related_qs)
395
+
396
+ def _resolve_action_schema(self, add: bool, remove: bool):
397
+ return self.views_action_map[(add, remove)]
398
+
399
+ def _register_manage_relation_view(
400
+ self,
401
+ *,
402
+ related_model: ModelSerializer | Model,
403
+ related_name: str,
404
+ m2m_auth,
405
+ rel_util: ModelUtil,
406
+ rel_path: str,
407
+ m2m_add: bool,
408
+ m2m_remove: bool,
409
+ ):
410
+ action, schema_in = self._resolve_action_schema(m2m_add, m2m_remove)
411
+ plural = rel_util.model._meta.verbose_name_plural.capitalize()
412
+ summary = f"{action} {plural}"
413
+
414
+ @self.router.post(
415
+ self._get_api_path(rel_path),
416
+ response={
417
+ 200: M2MSchemaOut,
418
+ self.view_set.error_codes: GenericMessageSchema,
419
+ },
420
+ auth=m2m_auth,
421
+ summary=summary,
422
+ description=summary,
423
+ )
424
+ @unique_view(f"manage_{self.related_model_util.model_name}_{rel_path}")
425
+ async def manage_related(
426
+ request: HttpRequest,
427
+ pk: Path[self.path_schema], # type: ignore
428
+ data: schema_in, # type: ignore
429
+ ):
430
+ obj = await self.related_model_util.get_object(
431
+ request, self.view_set._get_pk(pk)
432
+ )
433
+ related_manager: QuerySet = getattr(obj, related_name)
434
+
435
+ add_pks = getattr(data, "add", []) if m2m_add else []
436
+ remove_pks = getattr(data, "remove", []) if m2m_remove else []
437
+
438
+ add_errors, add_details, add_objs = await self._collect_m2m(
439
+ request,
440
+ add_pks,
441
+ related_model,
442
+ related_manager,
443
+ related_name,
444
+ obj,
445
+ )
446
+ remove_errors, remove_details, remove_objs = await self._collect_m2m(
447
+ request,
448
+ remove_pks,
449
+ related_model,
450
+ related_manager,
451
+ related_name,
452
+ obj,
453
+ remove=True,
454
+ )
455
+
456
+ tasks = []
457
+ if add_objs:
458
+ tasks.append(related_manager.aadd(*add_objs))
459
+ if remove_objs:
460
+ tasks.append(related_manager.aremove(*remove_objs))
461
+ if tasks:
462
+ await asyncio.gather(*tasks)
463
+
464
+ results = add_details + remove_details
465
+ errors = add_errors + remove_errors
466
+ return {
467
+ "results": {"count": len(results), "details": results},
468
+ "errors": {"count": len(errors), "details": errors},
469
+ }
470
+
471
+ def _build_views(self, relation: M2MRelationSchema):
472
+ model = relation.model
473
+ related_name = relation.related_name
474
+ m2m_auth = relation.auth or self.default_auth
475
+ rel_util = ModelUtil(model)
476
+ rel_path = relation.path or rel_util.verbose_name_path_resolver()
477
+ related_schema = relation.related_schema
478
+ m2m_add, m2m_remove, m2m_get = relation.add, relation.remove, relation.get
479
+ filters_schema = self.relations_filters_schemas.get(related_name)
480
+ append_slash = relation.append_slash
481
+
482
+ if m2m_get:
483
+ self._register_get_relation_view(
484
+ related_name=related_name,
485
+ m2m_auth=m2m_auth,
486
+ rel_util=rel_util,
487
+ rel_path=rel_path,
488
+ related_schema=related_schema,
489
+ filters_schema=filters_schema,
490
+ append_slash=append_slash,
491
+ )
492
+
493
+ if m2m_add or m2m_remove:
494
+ self._register_manage_relation_view(
495
+ related_model=model,
496
+ related_name=related_name,
497
+ m2m_auth=m2m_auth,
498
+ rel_util=rel_util,
499
+ rel_path=rel_path,
500
+ m2m_add=m2m_add,
501
+ m2m_remove=m2m_remove,
502
+ )
503
+
504
+ def _add_views(self):
505
+ for relation in self.relations:
506
+ self._build_views(relation)
@@ -0,0 +1,108 @@
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
+ """Create a simple namespace where each provided scope becomes an attribute."""
12
+ for key, value in scopes.items():
13
+ setattr(self, key, value)
14
+
15
+ def __iter__(self):
16
+ """Iterate over the stored scope values."""
17
+ return iter(self.__dict__.values())
18
+
19
+
20
+ class QueryUtil:
21
+ """
22
+ Helper class to manage queryset optimizations based on predefined scopes.
23
+ Attributes:
24
+ model (ModelSerializerMeta): The model serializer meta to which this utility is attached.
25
+ SCOPES (ScopeNamespace): An enumeration-like object containing available scopes.
26
+ read_config (ModelQuerySetSchema): Configuration for the 'read' scope.
27
+ queryset_request_config (ModelQuerySetSchema): Configuration for the 'queryset_request' scope
28
+ extra_configs (dict): Additional configurations for custom scopes.
29
+ Methods:
30
+ apply_queryset_optimizations(queryset, scope): Applies select_related and prefetch_related
31
+ optimizations to the given queryset based on the specified scope.
32
+
33
+ Example:
34
+ query_util = QueryUtil(MyModelSerializer) or MyModel.query_util
35
+ qs = MyModel.objects.all()
36
+ optimized_qs = query_util.apply_queryset_optimizations(qs, query_util.SCOPES.READ)
37
+
38
+ # Applying optimizations for a custom scope
39
+ class MyModelSerializer(ModelSerializer):
40
+ class QuerySet:
41
+ extras = [
42
+ ModelQuerySetExtraSchema(
43
+ scope="custom_scope",
44
+ select_related=["custom_fk_field"],
45
+ prefetch_related=["custom_m2m_field"],
46
+ )
47
+ ]
48
+ query_util = MyModelSerializer.query_util
49
+ qs = MyModelSerializer.objects.all()
50
+ optimized_qs_custom = query_util.apply_queryset_optimizations(qs, "custom_scope")
51
+ """
52
+
53
+ SCOPES: QueryUtilBaseScopesSchema
54
+
55
+ def __init__(self, model: ModelSerializerMeta):
56
+ """Initialize QueryUtil, resolving base and extra scope configurations for a model."""
57
+ self.model = model
58
+ self._configuration = getattr(self.model, "QuerySet", None)
59
+ self._extra_configuration: list[ModelQuerySetExtraSchema] = getattr(
60
+ self._configuration, "extras", []
61
+ )
62
+ self._BASE_SCOPES = QueryUtilBaseScopesSchema().model_dump()
63
+ self.SCOPES = ScopeNamespace(
64
+ **self._BASE_SCOPES,
65
+ **{extra.scope: extra.scope for extra in self._extra_configuration},
66
+ )
67
+ self.extra_configs = {extra.scope: extra for extra in self._extra_configuration}
68
+ self._configs = {
69
+ **{scope: self._get_config(scope) for scope in self._BASE_SCOPES.values()},
70
+ **self.extra_configs,
71
+ }
72
+ self.read_config: ModelQuerySetSchema = self._configs.get(
73
+ self.SCOPES.READ, ModelQuerySetSchema()
74
+ )
75
+ self.queryset_request_config: ModelQuerySetSchema = self._configs.get(
76
+ self.SCOPES.QUERYSET_REQUEST, ModelQuerySetSchema()
77
+ )
78
+
79
+ def _get_config(self, conf_name: str) -> ModelQuerySetSchema:
80
+ """Helper method to retrieve configuration attributes."""
81
+ return getattr(self._configuration, conf_name, ModelQuerySetSchema())
82
+
83
+ def apply_queryset_optimizations(self, queryset, scope: str):
84
+ """
85
+ Apply select_related and prefetch_related optimizations to the queryset
86
+ according to the specified scope.
87
+
88
+ Args:
89
+ queryset (QuerySet): The Django queryset to optimize.
90
+ scope (str): The scope to apply. Must be in self.SCOPES.
91
+
92
+ Returns:
93
+ QuerySet: The optimized queryset.
94
+
95
+ Raises:
96
+ ValueError: If the given scope is not supported.
97
+ """
98
+ if scope not in self._configs:
99
+ valid_scopes = list(self._configs.keys())
100
+ raise ValueError(
101
+ f"Invalid scope '{scope}' for QueryUtil. Supported scopes: {valid_scopes}"
102
+ )
103
+ config = self._configs.get(scope, ModelQuerySetSchema())
104
+ if config.select_related:
105
+ queryset = queryset.select_related(*config.select_related)
106
+ if config.prefetch_related:
107
+ queryset = queryset.prefetch_related(*config.prefetch_related)
108
+ return queryset
@@ -0,0 +1,4 @@
1
+ from .utils import ModelUtil
2
+ from .serializers import ModelSerializer
3
+
4
+ __all__ = ["ModelUtil", "ModelSerializer"]