django-ninja-aio-crud 0.9.2__tar.gz → 0.10.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 0.9.2
3
+ Version: 0.10.1
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "0.9.2"
3
+ __version__ = "0.10.1"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -1,4 +1,4 @@
1
- from joserfc import jwt, jwk
1
+ from joserfc import jwt, jwk, errors
2
2
  from django.http.request import HttpRequest
3
3
  from ninja.security.http import HttpBearer
4
4
 
@@ -25,11 +25,19 @@ class AsyncJwtBearer(HttpBearer):
25
25
  pass
26
26
 
27
27
  async def authenticate(self, request: HttpRequest, token: str):
28
+ """
29
+ Authenticate the request and return the user if authentication is successful.
30
+ If authentication fails, returns false.
31
+ """
28
32
  try:
29
33
  self.dcd = jwt.decode(token, self.jwt_public, algorithms=self.algorithms)
30
34
  except ValueError as exc:
31
- raise AuthError(", ".join(exc.args), 401)
35
+ # raise AuthError(", ".join(exc.args), 401)
36
+ return False
32
37
 
33
- self.validate_claims(self.dcd.claims)
38
+ try:
39
+ self.validate_claims(self.dcd.claims)
40
+ except errors.JoseError as exc:
41
+ return False
34
42
 
35
43
  return await self.auth_handler(request)
@@ -60,6 +60,10 @@ class ModelUtil:
60
60
  filters: dict = None,
61
61
  getters: dict = None,
62
62
  with_qs_request=True,
63
+ ) -> (
64
+ type["ModelSerializer"]
65
+ | models.Model
66
+ | models.QuerySet[type["ModelSerializer"] | models.Model]
63
67
  ):
64
68
  get_q = {self.model_pk_name: pk} if pk is not None else {}
65
69
  if getters:
@@ -73,6 +77,9 @@ class ModelUtil:
73
77
  if filters:
74
78
  obj_qs = obj_qs.filter(**filters)
75
79
 
80
+ if not get_q:
81
+ return obj_qs
82
+
76
83
  try:
77
84
  obj = await obj_qs.aget(**get_q)
78
85
  except ObjectDoesNotExist:
@@ -0,0 +1,29 @@
1
+ from ninja import Schema
2
+ from pydantic import RootModel
3
+
4
+
5
+ class GenericMessageSchema(RootModel[dict[str, str]]):
6
+ root: dict[str, str]
7
+
8
+
9
+ class M2MDetailSchema(Schema):
10
+ count: int
11
+ details: list[str]
12
+
13
+
14
+ class M2MSchemaOut(Schema):
15
+ errors: M2MDetailSchema
16
+ results: M2MDetailSchema
17
+
18
+
19
+ class M2MAddSchemaIn(Schema):
20
+ add: list = []
21
+
22
+
23
+ class M2MRemoveSchemaIn(Schema):
24
+ remove: list = []
25
+
26
+
27
+ class M2MSchemaIn(Schema):
28
+ add: list = []
29
+ remove: list = []
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from typing import List
2
3
 
3
4
  from ninja import NinjaAPI, Router, Schema, Path, Query
@@ -8,7 +9,13 @@ from django.db.models import Model, QuerySet
8
9
  from pydantic import create_model
9
10
 
10
11
  from .models import ModelSerializer, ModelUtil
11
- from .schemas import GenericMessageSchema
12
+ from .schemas import (
13
+ GenericMessageSchema,
14
+ M2MSchemaOut,
15
+ M2MSchemaIn,
16
+ M2MAddSchemaIn,
17
+ M2MRemoveSchemaIn,
18
+ )
12
19
  from .types import ModelSerializerMeta, VIEW_TYPES
13
20
 
14
21
  ERROR_CODES = frozenset({400, 401, 404, 428})
@@ -93,6 +100,11 @@ class APIViewSet:
93
100
  - **retrieve_docs** (`str`): Documentation for the retrieve view.
94
101
  - **update_docs** (`str`): Documentation for the update view.
95
102
  - **delete_docs** (`str`): Documentation for the delete view.
103
+ - **m2m_relations** (`tuple[ModelSerializer | Model, str]`): Many-to-many relations to manage.
104
+ - **m2m_add** (`bool`): Enable add operation for M2M relations.
105
+ - **m2m_remove** (`bool`): Enable remove operation for M2M relations.
106
+ - **m2m_get** (`bool`): Enable get operation for M2M relations.
107
+ - **m2m_auth** (`list | None`): Authentication for M2M views.
96
108
 
97
109
  ## Notes:
98
110
  If the model is a ModelSerializer instance, schemas are generated
@@ -137,6 +149,11 @@ class APIViewSet:
137
149
  retrieve_docs = "Retrieve a specific object by its primary key."
138
150
  update_docs = "Update an object by its primary key."
139
151
  delete_docs = "Delete an object by its primary key."
152
+ m2m_relations: tuple[ModelSerializer | Model, str] = []
153
+ m2m_add = True
154
+ m2m_remove = True
155
+ m2m_get = True
156
+ m2m_auth: list | None = NOT_SET
140
157
 
141
158
  def __init__(self) -> None:
142
159
  self.error_codes = ERROR_CODES
@@ -347,21 +364,138 @@ class APIViewSet:
347
364
  pass
348
365
  """
349
366
 
350
- def _add_views(self):
351
- if "all" in self.disable:
352
- self.views()
353
- return self.router
354
-
355
- for views_type, (schema, view) in self._crud_views.items():
356
- if views_type not in self.disable and (
357
- schema is not None or views_type == "delete"
358
- ):
359
- view()
360
-
361
- self.views()
362
- return self.router
363
-
364
- def add_views_to_route(self):
365
- return self.api.add_router(
366
- f"{self.api_route_path}", self._add_views()
367
- )
367
+ async def _check_m2m_objs(
368
+ self,
369
+ request: HttpRequest,
370
+ objs_pks: list,
371
+ model: ModelSerializer | Model,
372
+ related_manager: QuerySet,
373
+ remove: bool = False,
374
+ ):
375
+ errors, objs_detail, objs = [], [], []
376
+ rel_objs = [rel_obj async for rel_obj in related_manager.select_related().all()]
377
+ rel_model_name = model._meta.verbose_name.capitalize()
378
+ for obj_pk in objs_pks:
379
+ rel_obj = await (
380
+ await ModelUtil(model).get_object(request, filters={"pk": obj_pk})
381
+ ).afirst()
382
+ if rel_obj is None:
383
+ errors.append(f"{rel_model_name} with pk {obj_pk} not found.")
384
+ continue
385
+ if remove ^ (rel_obj in rel_objs):
386
+ errors.append(
387
+ f"{rel_model_name} with id {obj_pk} is {'not ' if remove else ''} in {self.model_util.model_name}"
388
+ )
389
+ continue
390
+ objs.append(rel_obj)
391
+ objs_detail.append(
392
+ f"{rel_model_name} with id {obj_pk} successfully {'removed' if remove else 'added'}"
393
+ )
394
+ return errors, objs_detail, objs
395
+
396
+ def _m2m_views(self):
397
+ for model, related_name in self.m2m_relations:
398
+ rel_util = ModelUtil(model)
399
+ rel_path = rel_util.verbose_name_path_resolver()
400
+ if self.m2m_get:
401
+
402
+ @self.router.get(
403
+ f"{self.path_retrieve}{rel_path}",
404
+ response={
405
+ 200: List[model.generate_related_s(),],
406
+ self.error_codes: GenericMessageSchema,
407
+ },
408
+ auth=self.m2m_auth,
409
+ summary=f"Get {rel_util.model._meta.verbose_name_plural.capitalize()}",
410
+ description=f"Get all related {rel_util.model._meta.verbose_name_plural.capitalize()}",
411
+ )
412
+ @paginate(self.pagination_class)
413
+ async def get_related(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
414
+ obj = await self.model_util.get_object(request, self._get_pk(pk))
415
+ related_manager = getattr(obj, related_name)
416
+ related_qs = related_manager.all()
417
+ related_objs = [
418
+ await rel_util.read_s(
419
+ request, rel_obj, model.generate_related_s()
420
+ )
421
+ async for rel_obj in related_qs
422
+ ]
423
+ return related_objs
424
+
425
+ get_related.__name__ = f"get_{self.model_util.model_name}_{rel_path}"
426
+
427
+ if self.m2m_add or self.m2m_remove:
428
+ summary = f"{'Add or Remove' if self.m2m_add and self.m2m_remove else 'Add' if self.m2m_add else 'Remove'} {rel_util.model._meta.verbose_name_plural.capitalize()}"
429
+ description = f"{'Add or remove' if self.m2m_add and self.m2m_remove else 'Add' if self.m2m_add else 'Remove'} {rel_util.model._meta.verbose_name_plural.capitalize()}"
430
+ schema_in = (
431
+ M2MSchemaIn
432
+ if self.m2m_add and self.m2m_remove
433
+ else M2MAddSchemaIn
434
+ if self.m2m_add
435
+ else M2MRemoveSchemaIn
436
+ )
437
+
438
+ @self.router.post(
439
+ f"{self.path_retrieve}{rel_path}/",
440
+ response={
441
+ 200: M2MSchemaOut,
442
+ self.error_codes: GenericMessageSchema,
443
+ },
444
+ auth=self.m2m_auth,
445
+ summary=summary,
446
+ description=description,
447
+ )
448
+ async def manage_related(
449
+ request: HttpRequest,
450
+ pk: Path[self.path_schema], # type: ignore
451
+ data: schema_in, # type: ignore
452
+ ):
453
+ obj = await self.model_util.get_object(
454
+ request, self._get_pk(pk)
455
+ )
456
+ related_manager: QuerySet = getattr(obj, related_name)
457
+ add_errors, add_details, add_objs = [], [], []
458
+ remove_errors, remove_details, remove_objs = [], [], []
459
+
460
+ if self.m2m_add and hasattr(data, "add"):
461
+ (
462
+ add_errors,
463
+ add_details,
464
+ add_objs,
465
+ ) = await self._check_m2m_objs(
466
+ request, data.add, model, related_manager
467
+ )
468
+ if self.m2m_remove and hasattr(data, "remove"):
469
+ (
470
+ remove_errors,
471
+ remove_details,
472
+ remove_objs,
473
+ ) = await self._check_m2m_objs(
474
+ request,
475
+ data.remove,
476
+ model,
477
+ related_manager,
478
+ remove=True,
479
+ )
480
+
481
+ await asyncio.gather(
482
+ related_manager.aadd(*add_objs),
483
+ related_manager.aremove(*remove_objs),
484
+ )
485
+ results = add_details + remove_details
486
+ errors = add_errors + remove_errors
487
+
488
+ return {
489
+ "results": {
490
+ "count": len(results),
491
+ "details": results,
492
+ },
493
+ "errors": {
494
+ "count": len(errors),
495
+ "details": errors,
496
+ },
497
+ }
498
+
499
+ manage_related.__name__ = (
500
+ f"manage_{self.model_util.model_name}_{rel_path}"
501
+ )
@@ -1,5 +0,0 @@
1
- from pydantic import RootModel
2
-
3
-
4
- class GenericMessageSchema(RootModel[dict[str, str]]):
5
- root: dict[str, str]