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.
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/PKG-INFO +1 -1
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/ninja_aio/auth.py +11 -3
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/ninja_aio/models.py +7 -0
- django_ninja_aio_crud-0.10.1/ninja_aio/schemas.py +29 -0
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/ninja_aio/views.py +153 -19
- django_ninja_aio_crud-0.9.2/ninja_aio/schemas.py +0 -5
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/LICENSE +0 -0
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/README.md +0 -0
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-0.9.2 → django_ninja_aio_crud-0.10.1}/pyproject.toml +0 -0
|
@@ -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
|
-
|
|
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
|
|
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
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|