django-ninja-aio-crud 2.0.0rc1__py3-none-any.whl → 2.0.0rc3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.0.0rc1
3
+ Version: 2.0.0rc3
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10
@@ -17,15 +17,17 @@ Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
20
22
  Classifier: Programming Language :: Python :: 3 :: Only
21
23
  Classifier: Framework :: Django
22
24
  Classifier: Framework :: AsyncIO
23
25
  Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
24
26
  Classifier: Topic :: Internet :: WWW/HTTP
25
27
  License-File: LICENSE
26
- Requires-Dist: django-ninja >=1.3.0, <=1.5.0
28
+ Requires-Dist: django-ninja >=1.3.0, <=1.5.1
27
29
  Requires-Dist: joserfc >=1.0.0, <= 1.4.1
28
- Requires-Dist: orjson >= 3.10.7, <= 3.11.4
30
+ Requires-Dist: orjson >= 3.10.7, <= 3.11.5
29
31
  Requires-Dist: coverage ; extra == "test"
30
32
  Project-URL: Documentation, https://django-ninja-aio.com
31
33
  Project-URL: Repository, https://github.com/caspel26/django-ninja-aio-crud
@@ -1,4 +1,4 @@
1
- ninja_aio/__init__.py,sha256=jrKqucbfHt32QKtv_Z_sLBKN5ByouVattkfgikaSrAo,123
1
+ ninja_aio/__init__.py,sha256=JcpcShlXZ4BnQ_O8zXApC5ZgzwxHrruInN3QK3tZgps,123
2
2
  ninja_aio/api.py,sha256=SS1TYUiFkdYjfJLVy6GI90GOzvIHzPEeL-UcqWFRHkM,1684
3
3
  ninja_aio/auth.py,sha256=8jaEp7oEJvUUB9EuyE2fOYk-khyAaekT3i80E7AbgOA,5101
4
4
  ninja_aio/decorators.py,sha256=gswkwl1zWSpW8VxGCe8MlgXcHMg6Y7V1f2ertey9Tjo,5522
@@ -9,13 +9,13 @@ ninja_aio/renders.py,sha256=5TdSQI8e4x3Gb2tAw1AaxrbU-asVjf2chWMr8x2Tt80,1485
9
9
  ninja_aio/types.py,sha256=TJSGlA7bt4g9fvPhJ7gzH5tKbLagPmZUzfgttEOp4xs,468
10
10
  ninja_aio/views.py,sha256=u6WVXvErF3DAOTONTH_EhSg-5qhFBhtR4L2lNrt00qo,16331
11
11
  ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- ninja_aio/helpers/api.py,sha256=lkh8eA9OnFlFPpxhnVP_wMHWPKfueVB4AnsXxgzp-HI,17426
12
+ ninja_aio/helpers/api.py,sha256=F6SlvNCEEAm-fzqpTB5r0O9gqhbP0dvyieNcG1JlKCw,20181
13
13
  ninja_aio/helpers/query.py,sha256=tE8RjXvSig-WB_0LRQ0LqoE4G_HMHsu0Na5QzTNIm6U,4262
14
14
  ninja_aio/schemas/__init__.py,sha256=iLBwHg0pmL9k_UkIui5Q8QIl_gO4fgxSv2JHxDzqnSI,549
15
15
  ninja_aio/schemas/api.py,sha256=-VwXhBRhmMsZLIAmWJ-P7tB5klxXS75eukjabeKKYsc,360
16
16
  ninja_aio/schemas/generics.py,sha256=frjJsKJMAdM_NdNKv-9ddZNGxYy5PNzjIRGtuycgr-w,112
17
17
  ninja_aio/schemas/helpers.py,sha256=al1G5-CdB9CF3eIPdZcUSE7vKRJiTU5g1ltoPhI2Onw,2622
18
- django_ninja_aio_crud-2.0.0rc1.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
19
- django_ninja_aio_crud-2.0.0rc1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
20
- django_ninja_aio_crud-2.0.0rc1.dist-info/METADATA,sha256=3sHPpIDJh65PwwiM4fN-ZuSDPif--X8aivMrquJiCyg,8573
21
- django_ninja_aio_crud-2.0.0rc1.dist-info/RECORD,,
18
+ django_ninja_aio_crud-2.0.0rc3.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
19
+ django_ninja_aio_crud-2.0.0rc3.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
20
+ django_ninja_aio_crud-2.0.0rc3.dist-info/METADATA,sha256=_srbK-mfXF3zuPPmP1YK2zssec21f2W4CnowtGgHgR4,8675
21
+ django_ninja_aio_crud-2.0.0rc3.dist-info/RECORD,,
ninja_aio/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.0.0-rc1"
3
+ __version__ = "2.0.0-rc3"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
ninja_aio/helpers/api.py CHANGED
@@ -38,27 +38,34 @@ class ManyToManyAPI:
38
38
  Core behaviors:
39
39
  - Dynamically generates per-relation filter schemas for query parameters.
40
40
  - Supports custom per-relation query filtering handlers on the parent view set
41
- via a `{related_name}_query_params_handler` coroutine.
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.
42
45
  - Validates requested add/remove primary keys, producing granular success and
43
- error feedback.
46
+ error feedback.
44
47
  - Performs add/remove operations concurrently using asyncio.gather when both
45
- types of operations are requested in the same call.
48
+ types of operations are requested in the same call.
46
49
 
47
50
  Attributes established at initialization:
48
51
  relations: list of M2MRelationSchema defining each relation.
49
52
  view_set: The parent APIViewSet instance from which router, pagination, model util,
50
- and path schema are derived.
53
+ and path schema are derived.
51
54
  router: Ninja router used to register generated endpoints.
52
55
  pagination_class: Pagination class used for GET related endpoints.
53
56
  path_schema: Pydantic schema used to validate path parameters (e.g., primary key).
54
57
  related_model_util: A ModelUtil instance cloned from the parent view set to access
55
- base object retrieval helpers.
58
+ base object retrieval helpers.
56
59
  relations_filters_schemas: Mapping of related_name -> generated Pydantic filter schema.
57
60
 
58
61
  Generated endpoint naming conventions:
59
62
  GET -> get_{base_model_name}_{relation_path}
60
63
  POST -> manage_{base_model_name}_{relation_path}
61
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
+
62
69
  All responses standardize success and error reporting for POST as:
63
70
  {
64
71
  "results": {"count": int, "details": [str, ...]},
@@ -66,25 +73,29 @@ class ManyToManyAPI:
66
73
  }
67
74
 
68
75
  Concurrency note:
69
- Add and remove operations are executed concurrently when both lists are non-empty,
70
- minimizing round-trip latency for bulk mutations.
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.
71
79
 
72
80
  Error semantics:
73
81
  - Missing related objects: reported individually.
74
82
  - Invalid operation context (e.g., removing objects not currently related or adding
75
- objects already related) reported per primary key.
83
+ objects already related) reported per primary key.
76
84
  - Successful operations yield a corresponding success detail string per PK.
77
85
 
78
86
  Security / auth:
79
87
  - Each relation may optionally override auth via its schema; otherwise falls back
80
- to a default configured on the instance (self.default_auth).
88
+ to a default configured on the instance (self.default_auth).
81
89
 
82
90
  Pagination:
83
91
  - Applied only to GET related endpoints via @paginate(self.pagination_class).
84
92
 
85
93
  Extensibility:
86
94
  - Provide custom query param handling by defining an async method on the parent
87
- view set: `<related_name>_query_params_handler(self, queryset, filters_dict)`.
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.
88
99
  - Customize relation filtering schema via each relation's `filters` definition.
89
100
 
90
101
  -----------------------------------------------------------------------
@@ -114,9 +125,9 @@ class ManyToManyAPI:
114
125
 
115
126
  -----------------------------------------------------------------------
116
127
 
117
- _get_query_handler(related_name)
118
- Retrieve an optional per-relation query handler coroutine from the parent view set.
119
- Naming convention: `<related_name>_query_params_handler`.
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`.
120
131
 
121
132
  Parameters:
122
133
  related_name (str): The relation's attribute name on the base model.
@@ -126,16 +137,32 @@ class ManyToManyAPI:
126
137
 
127
138
  -----------------------------------------------------------------------
128
139
 
129
- _check_m2m_objs(request, objs_pks, model, related_manager, remove=False)
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)
130
154
  Validate requested primary keys for add/remove operations against the current
131
155
  relation state. Performs existence checks and logical consistency (e.g., prevents
132
- adding already-related objects or removing non-related objects).
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.
133
158
 
134
159
  Parameters:
135
160
  request (HttpRequest): Incoming request context (passed to ModelUtil for access control).
136
161
  objs_pks (list): List of primary keys to add or remove.
137
- model (ModelSerializer | Model): Model class or serializer used to resolve objects.
162
+ related_model (ModelSerializer | Model): Model class or serializer used to resolve objects.
138
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).
139
166
  remove (bool): If True, treat operation as removal validation.
140
167
 
141
168
  Returns:
@@ -150,7 +177,7 @@ class ManyToManyAPI:
150
177
 
151
178
  -----------------------------------------------------------------------
152
179
 
153
- _collect_m2m(request, pks, model, related_manager, remove=False)
180
+ _collect_m2m(request, pks, model, related_manager, related_name, instance, remove=False)
154
181
  Wrapper around _check_m2m_objs that short-circuits on empty PK lists.
155
182
 
156
183
  Parameters:
@@ -158,6 +185,8 @@ class ManyToManyAPI:
158
185
  pks (list): Primary keys proposed for mutation.
159
186
  model (ModelSerializer | Model)
160
187
  related_manager (QuerySet)
188
+ related_name (str)
189
+ instance (ModelSerializer | Model)
161
190
  remove (bool): Operation type flag.
162
191
 
163
192
  Returns:
@@ -165,38 +194,30 @@ class ManyToManyAPI:
165
194
 
166
195
  -----------------------------------------------------------------------
167
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
+
168
210
  _build_views(relation)
169
211
  Dynamically define and register the GET and/or POST endpoints for a single M2M
170
212
  relation based on the relation's schema flags (get/add/remove). Builds filter
171
213
  schemas, resolves path fragments, and binds handlers to the router with unique
172
214
  operation IDs.
173
215
 
174
- Parameters:
175
- relation (M2MRelationSchema): Declarative specification for one M2M relation.
176
-
177
- Side effects:
178
- - Registers endpoints on self.router.
179
- - Creates closures (get_related / manage_related) capturing relation context.
180
-
181
- GET endpoint behavior:
182
- - Retrieves base object via related_model_util.
183
- - Fetches all related objects; applies optional query handler and filters.
184
- - Serializes each related object with rel_util.read_s.
185
-
186
- POST endpoint behavior:
187
- - Parses add/remove PK lists.
188
- - Validates objects via _collect_m2m.
189
- - Performs asynchronous add/remove operations using aadd / aremove.
190
- - Aggregates results and errors into standardized response payload.
191
-
192
216
  -----------------------------------------------------------------------
193
217
 
194
218
  _add_views()
195
219
  Iterates over all declared relations and invokes _build_views to attach endpoints.
196
220
 
197
- Side effects:
198
- - Populates router with all required M2M endpoints.
199
-
200
221
  -----------------------------------------------------------------------
201
222
 
202
223
  Usage Example (conceptual):
@@ -241,30 +262,45 @@ class ManyToManyAPI:
241
262
  for data in self.relations
242
263
  }
243
264
 
244
- def _get_query_handler(self, related_name: str) -> Coroutine | None:
265
+ def _get_query_params_handler(self, related_name: str) -> Coroutine | None:
245
266
  return getattr(self.view_set, f"{related_name}_query_params_handler", None)
246
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
+
247
271
  async def _check_m2m_objs(
248
272
  self,
249
273
  request: HttpRequest,
250
274
  objs_pks: list,
251
- model: ModelSerializer | Model,
275
+ related_model: ModelSerializer | Model,
252
276
  related_manager: QuerySet,
277
+ related_name: str,
278
+ instance: ModelSerializer | Model,
253
279
  remove: bool = False,
254
280
  ):
255
281
  """
256
282
  Validate requested add/remove pk list for M2M operations.
257
283
  Returns (errors, details, objects_to_process).
284
+ Uses per-PK query handler if available, else falls back to ModelUtil lookup by pk.
258
285
  """
259
286
  errors, objs_detail, objs = [], [], []
260
287
  rel_objs = [rel_obj async for rel_obj in related_manager.select_related().all()]
261
- rel_model_name = model._meta.verbose_name.capitalize()
288
+ rel_model_name = related_model._meta.verbose_name.capitalize()
262
289
  for obj_pk in objs_pks:
263
- rel_obj = await (
264
- await ModelUtil(model).get_objects(
265
- request, query_data=ObjectsQuerySchema(filters={"pk": obj_pk})
266
- )
267
- ).afirst()
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()
268
304
  if rel_obj is None:
269
305
  errors.append(f"{rel_model_name} with pk {obj_pk} not found.")
270
306
  continue
@@ -283,14 +319,22 @@ class ManyToManyAPI:
283
319
  self,
284
320
  request: HttpRequest,
285
321
  pks: list,
286
- model: ModelSerializer | Model,
322
+ reletad_model: ModelSerializer | Model,
287
323
  related_manager: QuerySet,
288
- remove=False,
324
+ related_name: str,
325
+ instance: ModelSerializer | Model,
326
+ remove: bool = False,
289
327
  ):
290
328
  if not pks:
291
329
  return ([], [], [])
292
330
  return await self._check_m2m_objs(
293
- request, pks, model, related_manager, remove=remove
331
+ request,
332
+ pks,
333
+ reletad_model,
334
+ related_manager,
335
+ related_name,
336
+ instance,
337
+ remove=remove,
294
338
  )
295
339
 
296
340
  def _register_get_relation_view(
@@ -326,16 +370,14 @@ class ManyToManyAPI:
326
370
  related_manager = getattr(obj, related_name)
327
371
  related_qs = related_manager.all()
328
372
 
329
- query_handler = self._get_query_handler(related_name)
373
+ query_handler = self._get_query_params_handler(related_name)
330
374
  if filters is not None and query_handler:
331
375
  if asyncio.iscoroutinefunction(query_handler):
332
376
  related_qs = await query_handler(related_qs, filters.model_dump())
333
377
  else:
334
378
  related_qs = query_handler(related_qs, filters.model_dump())
335
379
 
336
- return await rel_util.list_read_s(
337
- related_schema, request, related_qs
338
- )
380
+ return await rel_util.list_read_s(related_schema, request, related_qs)
339
381
 
340
382
  def _resolve_action_schema(self, add: bool, remove: bool):
341
383
  return self.views_action_map[(add, remove)]
@@ -343,7 +385,7 @@ class ManyToManyAPI:
343
385
  def _register_manage_relation_view(
344
386
  self,
345
387
  *,
346
- model,
388
+ related_model: ModelSerializer | Model,
347
389
  related_name: str,
348
390
  m2m_auth,
349
391
  rel_util: ModelUtil,
@@ -380,10 +422,21 @@ class ManyToManyAPI:
380
422
  remove_pks = getattr(data, "remove", []) if m2m_remove else []
381
423
 
382
424
  add_errors, add_details, add_objs = await self._collect_m2m(
383
- request, add_pks, model, related_manager
425
+ request,
426
+ add_pks,
427
+ related_model,
428
+ related_manager,
429
+ related_name,
430
+ obj,
384
431
  )
385
432
  remove_errors, remove_details, remove_objs = await self._collect_m2m(
386
- request, remove_pks, model, related_manager, remove=True
433
+ request,
434
+ remove_pks,
435
+ related_model,
436
+ related_manager,
437
+ related_name,
438
+ obj,
439
+ remove=True,
387
440
  )
388
441
 
389
442
  tasks = []
@@ -423,7 +476,7 @@ class ManyToManyAPI:
423
476
 
424
477
  if m2m_add or m2m_remove:
425
478
  self._register_manage_relation_view(
426
- model=model,
479
+ related_model=model,
427
480
  related_name=related_name,
428
481
  m2m_auth=m2m_auth,
429
482
  rel_util=rel_util,