django-ninja-aio-crud 1.0.5__py3-none-any.whl → 2.0.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.
- {django_ninja_aio_crud-1.0.5.dist-info → django_ninja_aio_crud-2.0.0.dist-info}/METADATA +5 -3
- django_ninja_aio_crud-2.0.0.dist-info/RECORD +21 -0
- ninja_aio/__init__.py +1 -1
- ninja_aio/api.py +0 -2
- ninja_aio/auth.py +3 -4
- ninja_aio/decorators.py +76 -0
- ninja_aio/helpers/__init__.py +0 -3
- ninja_aio/helpers/api.py +117 -59
- ninja_aio/helpers/query.py +103 -0
- ninja_aio/models.py +504 -168
- ninja_aio/renders.py +8 -2
- ninja_aio/schemas/__init__.py +23 -0
- ninja_aio/schemas/api.py +24 -0
- ninja_aio/schemas/generics.py +5 -0
- ninja_aio/{schemas.py → schemas/helpers.py} +40 -32
- ninja_aio/views.py +48 -24
- django_ninja_aio_crud-1.0.5.dist-info/RECORD +0 -17
- {django_ninja_aio_crud-1.0.5.dist-info → django_ninja_aio_crud-2.0.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-1.0.5.dist-info → django_ninja_aio_crud-2.0.0.dist-info}/licenses/LICENSE +0 -0
ninja_aio/renders.py
CHANGED
|
@@ -4,11 +4,13 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
import orjson
|
|
6
6
|
from django.http import HttpRequest
|
|
7
|
+
from django.conf import settings
|
|
7
8
|
from ninja.renderers import BaseRenderer
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class ORJSONRenderer(BaseRenderer):
|
|
11
12
|
media_type = "application/json"
|
|
13
|
+
option = getattr(settings, "NINJA_AIO_ORJSON_RENDERER_OPTION", None)
|
|
12
14
|
|
|
13
15
|
def render(self, request: HttpRequest, data: dict, *, response_status):
|
|
14
16
|
try:
|
|
@@ -16,9 +18,13 @@ class ORJSONRenderer(BaseRenderer):
|
|
|
16
18
|
for k, v in old_d.items():
|
|
17
19
|
if isinstance(v, list):
|
|
18
20
|
data |= {k: self.render_list(v)}
|
|
19
|
-
return
|
|
21
|
+
return self.dumps(self.render_dict(data))
|
|
20
22
|
except AttributeError:
|
|
21
|
-
return
|
|
23
|
+
return self.dumps(data)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def dumps(cls, data: dict) -> bytes:
|
|
27
|
+
return orjson.dumps(data, option=cls.option)
|
|
22
28
|
|
|
23
29
|
@classmethod
|
|
24
30
|
def render_list(cls, data: list[dict]) -> list[dict]:
|
|
@@ -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
|
+
]
|
ninja_aio/schemas/api.py
ADDED
|
@@ -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 = []
|
|
@@ -1,36 +1,9 @@
|
|
|
1
|
-
from typing import Optional, Type
|
|
1
|
+
from typing import List, Optional, Type
|
|
2
2
|
|
|
3
3
|
from ninja import Schema
|
|
4
|
-
from .
|
|
4
|
+
from ninja_aio.types import ModelSerializerMeta
|
|
5
5
|
from django.db.models import Model
|
|
6
|
-
from pydantic import BaseModel,
|
|
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:
|
|
31
|
+
model: ModelSerializerMeta | Type[Model]
|
|
59
32
|
related_name: str
|
|
60
33
|
add: bool = True
|
|
61
34
|
remove: bool = True
|
|
@@ -74,9 +47,44 @@ class M2MRelationSchema(BaseModel):
|
|
|
74
47
|
if related_schema is not None:
|
|
75
48
|
return data
|
|
76
49
|
model = data.get("model")
|
|
77
|
-
if not
|
|
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"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class DecoratorsSchema(Schema):
|
|
86
|
+
list: Optional[List] = []
|
|
87
|
+
retrieve: Optional[List] = []
|
|
88
|
+
create: Optional[List] = []
|
|
89
|
+
update: Optional[List] = []
|
|
90
|
+
delete: Optional[List] = []
|
ninja_aio/views.py
CHANGED
|
@@ -7,14 +7,16 @@ 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, DecoratorsSchema
|
|
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
|
-
from .decorators import unique_view
|
|
19
|
+
from .decorators import unique_view, decorate_view
|
|
18
20
|
|
|
19
21
|
ERROR_CODES = frozenset({400, 401, 404, 428})
|
|
20
22
|
|
|
@@ -156,6 +158,7 @@ class APIViewSet:
|
|
|
156
158
|
|
|
157
159
|
model: ModelSerializer | Model
|
|
158
160
|
api: NinjaAPI
|
|
161
|
+
router_tag: str = ""
|
|
159
162
|
schema_in: Schema | None = None
|
|
160
163
|
schema_out: Schema | None = None
|
|
161
164
|
schema_update: Schema | None = None
|
|
@@ -175,15 +178,22 @@ class APIViewSet:
|
|
|
175
178
|
delete_docs = "Delete an object by its primary key."
|
|
176
179
|
m2m_relations: list[M2MRelationSchema] = []
|
|
177
180
|
m2m_auth: list | None = NOT_SET
|
|
181
|
+
extra_decorators: DecoratorsSchema = DecoratorsSchema()
|
|
178
182
|
|
|
179
183
|
def __init__(self) -> None:
|
|
180
184
|
self.error_codes = ERROR_CODES
|
|
181
|
-
self.model_util =
|
|
185
|
+
self.model_util = (
|
|
186
|
+
ModelUtil(self.model)
|
|
187
|
+
if not isinstance(self.model, ModelSerializerMeta)
|
|
188
|
+
else self.model.util
|
|
189
|
+
)
|
|
182
190
|
self.schema_out, self.schema_in, self.schema_update = self.get_schemas()
|
|
183
191
|
self.path_schema = self._generate_path_schema()
|
|
184
192
|
self.filters_schema = self._generate_filters_schema()
|
|
185
193
|
self.model_verbose_name = self.model._meta.verbose_name.capitalize()
|
|
186
|
-
self.router_tag =
|
|
194
|
+
self.router_tag = (
|
|
195
|
+
self.model_verbose_name if not self.router_tag else self.router_tag
|
|
196
|
+
)
|
|
187
197
|
self.router = Router(tags=[self.router_tag])
|
|
188
198
|
self.path = "/"
|
|
189
199
|
self.get_path = ""
|
|
@@ -241,7 +251,7 @@ class APIViewSet:
|
|
|
241
251
|
Schema containing only the primary key field for path resolution.
|
|
242
252
|
"""
|
|
243
253
|
return self._generate_schema(
|
|
244
|
-
{self.model_util.model_pk_name:
|
|
254
|
+
{self.model_util.model_pk_name: self.model_util.pk_field_type}, "PathSchema"
|
|
245
255
|
)
|
|
246
256
|
|
|
247
257
|
def _generate_filters_schema(self):
|
|
@@ -256,6 +266,16 @@ class APIViewSet:
|
|
|
256
266
|
"""
|
|
257
267
|
return data.model_dump()[self.model_util.model_pk_name]
|
|
258
268
|
|
|
269
|
+
def _get_query_data(self) -> ModelQuerySetSchema:
|
|
270
|
+
"""
|
|
271
|
+
Return default query data for list/retrieve views.
|
|
272
|
+
"""
|
|
273
|
+
return (
|
|
274
|
+
ModelQuerySetSchema()
|
|
275
|
+
if not isinstance(self.model, ModelSerializerMeta)
|
|
276
|
+
else self.model.query_util.read_config
|
|
277
|
+
)
|
|
278
|
+
|
|
259
279
|
def get_schemas(self):
|
|
260
280
|
"""
|
|
261
281
|
Return (schema_out, schema_in, schema_update), generating them if model is a ModelSerializer.
|
|
@@ -290,7 +310,7 @@ class APIViewSet:
|
|
|
290
310
|
description=self.create_docs,
|
|
291
311
|
response={201: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
292
312
|
)
|
|
293
|
-
@unique_view(self)
|
|
313
|
+
@decorate_view(unique_view(self), *self.extra_decorators.create)
|
|
294
314
|
async def create(request: HttpRequest, data: self.schema_in): # type: ignore
|
|
295
315
|
return 201, await self.model_util.create_s(request, data, self.schema_out)
|
|
296
316
|
|
|
@@ -311,25 +331,23 @@ class APIViewSet:
|
|
|
311
331
|
self.error_codes: GenericMessageSchema,
|
|
312
332
|
},
|
|
313
333
|
)
|
|
314
|
-
@
|
|
315
|
-
|
|
334
|
+
@decorate_view(
|
|
335
|
+
paginate(self.pagination_class),
|
|
336
|
+
unique_view(self, plural=True),
|
|
337
|
+
*self.extra_decorators.list,
|
|
338
|
+
)
|
|
316
339
|
async def list(
|
|
317
340
|
request: HttpRequest,
|
|
318
341
|
filters: Query[self.filters_schema] = None, # type: ignore
|
|
319
342
|
):
|
|
320
|
-
qs = self.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
qs = qs.prefetch_related(*rels)
|
|
343
|
+
qs = await self.model_util.get_objects(
|
|
344
|
+
request,
|
|
345
|
+
query_data=self._get_query_data(),
|
|
346
|
+
is_for_read=True,
|
|
347
|
+
)
|
|
326
348
|
if filters is not None:
|
|
327
349
|
qs = await self.query_params_handler(qs, filters.model_dump())
|
|
328
|
-
|
|
329
|
-
await self.model_util.read_s(request, obj, self.schema_out)
|
|
330
|
-
async for obj in qs.all()
|
|
331
|
-
]
|
|
332
|
-
return objs
|
|
350
|
+
return await self.model_util.list_read_s(self.schema_out, request, qs)
|
|
333
351
|
|
|
334
352
|
return list
|
|
335
353
|
|
|
@@ -345,10 +363,16 @@ class APIViewSet:
|
|
|
345
363
|
description=self.retrieve_docs,
|
|
346
364
|
response={200: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
347
365
|
)
|
|
348
|
-
@unique_view(self)
|
|
366
|
+
@decorate_view(unique_view(self), *self.extra_decorators.retrieve)
|
|
349
367
|
async def retrieve(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
|
|
350
|
-
|
|
351
|
-
return await self.model_util.read_s(
|
|
368
|
+
query_data = self._get_query_data()
|
|
369
|
+
return await self.model_util.read_s(
|
|
370
|
+
self.schema_out,
|
|
371
|
+
request,
|
|
372
|
+
query_data=QuerySchema(
|
|
373
|
+
getters={"pk": self._get_pk(pk)}, **query_data.model_dump()
|
|
374
|
+
),
|
|
375
|
+
)
|
|
352
376
|
|
|
353
377
|
return retrieve
|
|
354
378
|
|
|
@@ -364,7 +388,7 @@ class APIViewSet:
|
|
|
364
388
|
description=self.update_docs,
|
|
365
389
|
response={200: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
366
390
|
)
|
|
367
|
-
@unique_view(self)
|
|
391
|
+
@decorate_view(unique_view(self), *self.extra_decorators.update)
|
|
368
392
|
async def update(
|
|
369
393
|
request: HttpRequest,
|
|
370
394
|
data: self.schema_update, # type: ignore
|
|
@@ -388,7 +412,7 @@ class APIViewSet:
|
|
|
388
412
|
description=self.delete_docs,
|
|
389
413
|
response={204: None, self.error_codes: GenericMessageSchema},
|
|
390
414
|
)
|
|
391
|
-
@unique_view(self)
|
|
415
|
+
@decorate_view(unique_view(self), *self.extra_decorators.delete)
|
|
392
416
|
async def delete(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
|
|
393
417
|
return 204, await self.model_util.delete_s(request, self._get_pk(pk))
|
|
394
418
|
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
ninja_aio/__init__.py,sha256=_Euc356-ScYsaVY9gvZ7xLRgTfnHKXk0pv69eFhjdJg,119
|
|
2
|
-
ninja_aio/api.py,sha256=Fe6l3YCy7MW5TY4-Lbl80CFuK2NT2Y7tHfmqPk6Mqak,1735
|
|
3
|
-
ninja_aio/auth.py,sha256=zUwruKcz7MXuOnWp5k1CCSwEc8s2Lyqqk7Qm9kPbJ3o,5149
|
|
4
|
-
ninja_aio/decorators.py,sha256=LsvHbMxmw_So8NV0ey5NRRvSbfYkOZLeLQ4Fix7rQAY,5519
|
|
5
|
-
ninja_aio/exceptions.py,sha256=1-iRbrloIyi0CR6Tcrn5YR4_LloA7PPohKIBaxXJ0-8,2596
|
|
6
|
-
ninja_aio/models.py,sha256=fsFYjKFZb8MDpE1g3Nte-Dot4x7ofmuxIDi8qtkWu0o,35422
|
|
7
|
-
ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
|
|
8
|
-
ninja_aio/renders.py,sha256=5TdSQI8e4x3Gb2tAw1AaxrbU-asVjf2chWMr8x2Tt80,1485
|
|
9
|
-
ninja_aio/schemas.py,sha256=sYxhovFq6nE_Gdx_yl1ufcuHBAXxpsnBmNytyTSqFLI,2449
|
|
10
|
-
ninja_aio/types.py,sha256=TJSGlA7bt4g9fvPhJ7gzH5tKbLagPmZUzfgttEOp4xs,468
|
|
11
|
-
ninja_aio/views.py,sha256=31pyOzABHwO1jcIVJRHXVrpMu9__FHmdX2vEhX6XrPs,15890
|
|
12
|
-
ninja_aio/helpers/__init__.py,sha256=E45h2prtpCudx_bSKkIDMNK9oxrHPDJrweXWCuLxUOs,59
|
|
13
|
-
ninja_aio/helpers/api.py,sha256=VW5C4hQl2flYhGPgVJ2ZkFK1tGMdNOaxVWJfZfLd7OY,17171
|
|
14
|
-
django_ninja_aio_crud-1.0.5.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
|
|
15
|
-
django_ninja_aio_crud-1.0.5.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
16
|
-
django_ninja_aio_crud-1.0.5.dist-info/METADATA,sha256=V6cJgV_yDqLnEtG1FKbQR0fdmGE6okwPChkwEkyfNzs,8570
|
|
17
|
-
django_ninja_aio_crud-1.0.5.dist-info/RECORD,,
|
|
File without changes
|
{django_ninja_aio_crud-1.0.5.dist-info → django_ninja_aio_crud-2.0.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|