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.
- django_ninja_aio_crud-2.4.0.dist-info/METADATA +382 -0
- django_ninja_aio_crud-2.4.0.dist-info/RECORD +29 -0
- ninja_aio/__init__.py +1 -1
- ninja_aio/api.py +24 -2
- ninja_aio/auth.py +186 -4
- ninja_aio/decorators/__init__.py +23 -0
- ninja_aio/decorators/operations.py +9 -0
- ninja_aio/decorators/views.py +219 -0
- ninja_aio/exceptions.py +36 -1
- ninja_aio/factory/__init__.py +3 -0
- ninja_aio/factory/operations.py +296 -0
- ninja_aio/helpers/__init__.py +0 -0
- ninja_aio/helpers/api.py +506 -0
- ninja_aio/helpers/query.py +108 -0
- ninja_aio/models/__init__.py +4 -0
- ninja_aio/models/serializers.py +738 -0
- ninja_aio/models/utils.py +894 -0
- ninja_aio/renders.py +26 -26
- ninja_aio/schemas/__init__.py +23 -0
- ninja_aio/{schemas.py → schemas/api.py} +0 -5
- ninja_aio/schemas/generics.py +5 -0
- ninja_aio/schemas/helpers.py +170 -0
- ninja_aio/types.py +3 -1
- ninja_aio/views/__init__.py +3 -0
- ninja_aio/views/api.py +582 -0
- ninja_aio/views/mixins.py +275 -0
- django_ninja_aio_crud-0.10.2.dist-info/METADATA +0 -526
- django_ninja_aio_crud-0.10.2.dist-info/RECORD +0 -14
- ninja_aio/models.py +0 -549
- ninja_aio/views.py +0 -522
- {django_ninja_aio_crud-0.10.2.dist-info → django_ninja_aio_crud-2.4.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-0.10.2.dist-info → django_ninja_aio_crud-2.4.0.dist-info}/licenses/LICENSE +0 -0
ninja_aio/renders.py
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
from ipaddress import IPv4Address, IPv6Address
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
import orjson
|
|
5
6
|
from django.http import HttpRequest
|
|
7
|
+
from django.conf import settings
|
|
6
8
|
from ninja.renderers import BaseRenderer
|
|
9
|
+
from pydantic import AnyUrl
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class ORJSONRenderer(BaseRenderer):
|
|
10
13
|
media_type = "application/json"
|
|
14
|
+
option = getattr(settings, "NINJA_AIO_ORJSON_RENDERER_OPTION", None)
|
|
11
15
|
|
|
12
16
|
def render(self, request: HttpRequest, data: dict, *, response_status):
|
|
13
17
|
try:
|
|
@@ -15,9 +19,13 @@ class ORJSONRenderer(BaseRenderer):
|
|
|
15
19
|
for k, v in old_d.items():
|
|
16
20
|
if isinstance(v, list):
|
|
17
21
|
data |= {k: self.render_list(v)}
|
|
18
|
-
return
|
|
22
|
+
return self.dumps(self.render_dict(data))
|
|
19
23
|
except AttributeError:
|
|
20
|
-
return
|
|
24
|
+
return self.dumps(data)
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def dumps(cls, data: dict) -> bytes:
|
|
28
|
+
return orjson.dumps(data, option=cls.option)
|
|
21
29
|
|
|
22
30
|
@classmethod
|
|
23
31
|
def render_list(cls, data: list[dict]) -> list[dict]:
|
|
@@ -28,27 +36,19 @@ class ORJSONRenderer(BaseRenderer):
|
|
|
28
36
|
return cls.parse_data(data)
|
|
29
37
|
|
|
30
38
|
@classmethod
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for index_rel, f_rel in enumerate(v):
|
|
48
|
-
for k_rel, v_rel in f_rel.items():
|
|
49
|
-
if isinstance(v_rel, bytes):
|
|
50
|
-
v[index_rel] |= {k_rel: base64.b64encode(v_rel).decode()}
|
|
51
|
-
if isinstance(v_rel, (IPv4Address, IPv6Address)):
|
|
52
|
-
v[index_rel] |= {k_rel: str(v_rel)}
|
|
53
|
-
data |= {k: v}
|
|
54
|
-
return data
|
|
39
|
+
def transform(cls, value):
|
|
40
|
+
if isinstance(value, bytes):
|
|
41
|
+
return base64.b64encode(value).decode()
|
|
42
|
+
if isinstance(value, (IPv4Address, IPv6Address, AnyUrl)):
|
|
43
|
+
return str(value)
|
|
44
|
+
if isinstance(value, dict):
|
|
45
|
+
return {k: cls.transform(v) for k, v in value.items()}
|
|
46
|
+
if isinstance(value, list):
|
|
47
|
+
return [cls.transform(item) for item in value]
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def parse_data(cls, data: dict | Any):
|
|
52
|
+
if not isinstance(data, dict):
|
|
53
|
+
return cls.transform(data)
|
|
54
|
+
return {k: cls.transform(v) for k, v in data.items()}
|
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from typing import List, Optional, Type
|
|
2
|
+
|
|
3
|
+
from ninja import Schema
|
|
4
|
+
from ninja_aio.types import ModelSerializerMeta
|
|
5
|
+
from django.db.models import Model
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class M2MRelationSchema(BaseModel):
|
|
10
|
+
"""
|
|
11
|
+
Configuration schema for declaring and controlling a Many-to-Many (M2M) relation in the API.
|
|
12
|
+
|
|
13
|
+
This schema is used to describe an M2M relationship between a primary resource and its related
|
|
14
|
+
objects, and to automatically provision CRUD-like endpoints for managing that relation
|
|
15
|
+
(add, remove, and get). It supports both direct Django model classes and model serializers,
|
|
16
|
+
and can optionally expose a custom schema for the related output.
|
|
17
|
+
|
|
18
|
+
model (ModelSerializerMeta | Type[Model]):
|
|
19
|
+
The target related entity, provided either as a ModelSerializer (preferred) or a Django model.
|
|
20
|
+
If a plain model is supplied, you must also provide `related_schema`.
|
|
21
|
+
related_name (str):
|
|
22
|
+
The name of the M2M field on the Django model that links to the related objects.
|
|
23
|
+
add (bool):
|
|
24
|
+
Whether to enable an endpoint for adding related objects. Defaults to True.
|
|
25
|
+
remove (bool):
|
|
26
|
+
Whether to enable an endpoint for removing related objects. Defaults to True.
|
|
27
|
+
get (bool):
|
|
28
|
+
Whether to enable an endpoint for listing/retrieving related objects. Defaults to True.
|
|
29
|
+
path (str | None):
|
|
30
|
+
Optional custom URL path segment for the relation endpoints. If empty or None, a path
|
|
31
|
+
is auto-generated based on `related_name`.
|
|
32
|
+
auth (list | None):
|
|
33
|
+
Optional list of authentication backends to protect the relation endpoints.
|
|
34
|
+
filters (dict[str, tuple] | None):
|
|
35
|
+
Optional mapping of queryable filter fields for the GET endpoint, defined as:
|
|
36
|
+
field_name -> (type, default). Example: {"country": ("str", "")}.
|
|
37
|
+
related_schema (Type[Schema] | None):
|
|
38
|
+
Optional explicit schema to represent related objects in responses.
|
|
39
|
+
If `model` is a ModelSerializerMeta, this is auto-derived via `model.generate_related_s()`.
|
|
40
|
+
If `model` is a plain Django model, this must be provided.
|
|
41
|
+
append_slash (bool):
|
|
42
|
+
Whether to append a trailing slash to the generated GET endpoint path. Defaults to False for backward compatibility.
|
|
43
|
+
|
|
44
|
+
Validation:
|
|
45
|
+
- If `model` is not a ModelSerializerMeta, `related_schema` is required.
|
|
46
|
+
- When `model` is a ModelSerializerMeta and `related_schema` is not provided, it will be
|
|
47
|
+
automatically generated.
|
|
48
|
+
|
|
49
|
+
Usage example:
|
|
50
|
+
filters={"country": ("str", "")},
|
|
51
|
+
auth=[AuthBackend],
|
|
52
|
+
add=True,
|
|
53
|
+
remove=True,
|
|
54
|
+
get=True,
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
model: ModelSerializerMeta | Type[Model]
|
|
58
|
+
related_name: str
|
|
59
|
+
add: bool = True
|
|
60
|
+
remove: bool = True
|
|
61
|
+
get: bool = True
|
|
62
|
+
path: Optional[str] = ""
|
|
63
|
+
auth: Optional[list] = None
|
|
64
|
+
filters: Optional[dict[str, tuple]] = None
|
|
65
|
+
related_schema: Optional[Type[Schema]] = None
|
|
66
|
+
append_slash: bool = False
|
|
67
|
+
|
|
68
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
69
|
+
|
|
70
|
+
@model_validator(mode="before")
|
|
71
|
+
@classmethod
|
|
72
|
+
def validate_related_schema(cls, data):
|
|
73
|
+
related_schema = data.get("related_schema")
|
|
74
|
+
if related_schema is not None:
|
|
75
|
+
return data
|
|
76
|
+
model = data.get("model")
|
|
77
|
+
if not isinstance(model, ModelSerializerMeta):
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"related_schema must be provided if model is not a ModelSerializer",
|
|
80
|
+
)
|
|
81
|
+
data["related_schema"] = model.generate_related_s()
|
|
82
|
+
return data
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ModelQuerySetSchema(BaseModel):
|
|
86
|
+
select_related: Optional[list[str]] = []
|
|
87
|
+
prefetch_related: Optional[list[str]] = []
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ModelQuerySetExtraSchema(ModelQuerySetSchema):
|
|
91
|
+
"""
|
|
92
|
+
Schema defining extra query parameters for model queryset operations in API endpoints.
|
|
93
|
+
Attributes:
|
|
94
|
+
scope (str): The scope defining the level of access for the queryset operation.
|
|
95
|
+
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
96
|
+
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
97
|
+
"""
|
|
98
|
+
scope: str
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ObjectQuerySchema(ModelQuerySetSchema):
|
|
102
|
+
"""
|
|
103
|
+
Schema defining query parameters for single object retrieval in API endpoints.
|
|
104
|
+
Attributes:
|
|
105
|
+
getters (Optional[dict]): A dictionary of getters to apply to the query.
|
|
106
|
+
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
107
|
+
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
108
|
+
"""
|
|
109
|
+
getters: Optional[dict] = {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ObjectsQuerySchema(ModelQuerySetSchema):
|
|
113
|
+
"""
|
|
114
|
+
Schema defining query parameters for multiple object retrieval in API endpoints.
|
|
115
|
+
Attributes:
|
|
116
|
+
filters (Optional[dict]): A dictionary of filters to apply to the query.
|
|
117
|
+
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
118
|
+
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
119
|
+
"""
|
|
120
|
+
filters: Optional[dict] = {}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class QuerySchema(ModelQuerySetSchema):
|
|
124
|
+
"""
|
|
125
|
+
Schema defining query parameters for API endpoints.
|
|
126
|
+
Attributes:
|
|
127
|
+
filters (Optional[dict]): A dictionary of filters to apply to the query.
|
|
128
|
+
getters (Optional[dict]): A dictionary of getters to apply to the query.
|
|
129
|
+
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
130
|
+
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
131
|
+
"""
|
|
132
|
+
filters: Optional[dict] = {}
|
|
133
|
+
getters: Optional[dict] = {}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class QueryUtilBaseScopesSchema(BaseModel):
|
|
137
|
+
"""
|
|
138
|
+
Schema defining base scopes for query utilities.
|
|
139
|
+
Attributes:
|
|
140
|
+
READ (str): Scope for read operations.
|
|
141
|
+
QUERYSET_REQUEST (str): Scope for queryset request operations.
|
|
142
|
+
"""
|
|
143
|
+
READ: str = "read"
|
|
144
|
+
QUERYSET_REQUEST: str = "queryset_request"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class DecoratorsSchema(Schema):
|
|
148
|
+
"""
|
|
149
|
+
Schema defining optional decorator lists for CRUD operations.
|
|
150
|
+
|
|
151
|
+
Attributes:
|
|
152
|
+
list (Optional[List]): Decorators applied to the list endpoint.
|
|
153
|
+
retrieve (Optional[List]): Decorators applied to the retrieve endpoint.
|
|
154
|
+
create (Optional[List]): Decorators applied to the create endpoint.
|
|
155
|
+
update (Optional[List]): Decorators applied to the update endpoint.
|
|
156
|
+
delete (Optional[List]): Decorators applied to the delete endpoint.
|
|
157
|
+
|
|
158
|
+
Notes:
|
|
159
|
+
- Each attribute holds an ordered collection of decorators (callables or decorator references)
|
|
160
|
+
to be applied to the corresponding endpoint.
|
|
161
|
+
- Defaults are empty lists, meaning no decorators are applied unless explicitly provided.
|
|
162
|
+
- Using mutable defaults (empty lists) at the class level may lead to shared state between instances.
|
|
163
|
+
Consider initializing these in __init__ or using default_factory (if using pydantic/dataclasses)
|
|
164
|
+
to avoid unintended side effects.
|
|
165
|
+
"""
|
|
166
|
+
list: Optional[List] = []
|
|
167
|
+
retrieve: Optional[List] = []
|
|
168
|
+
create: Optional[List] = []
|
|
169
|
+
update: Optional[List] = []
|
|
170
|
+
delete: Optional[List] = []
|
ninja_aio/types.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
2
|
|
|
3
|
+
from joserfc import jwk
|
|
3
4
|
from django.db.models import Model
|
|
5
|
+
from typing import TypeAlias
|
|
4
6
|
|
|
5
7
|
S_TYPES = Literal["read", "create", "update"]
|
|
6
8
|
F_TYPES = Literal["fields", "customs", "optionals", "excludes"]
|
|
7
9
|
SCHEMA_TYPES = Literal["In", "Out", "Patch", "Related"]
|
|
8
10
|
VIEW_TYPES = Literal["list", "retrieve", "create", "update", "delete", "all"]
|
|
9
|
-
|
|
11
|
+
JwtKeys: TypeAlias = jwk.RSAKey | jwk.ECKey | jwk.OctKey
|
|
10
12
|
|
|
11
13
|
class ModelSerializerType(type):
|
|
12
14
|
def __repr__(self):
|