django-ninja-aio-crud 2.0.0__tar.gz → 2.0.0rc2__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-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/PKG-INFO +1 -1
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/decorators.py +0 -73
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/helpers/api.py +59 -112
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/renders.py +2 -8
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/schemas/helpers.py +2 -10
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/views.py +9 -16
- django_ninja_aio_crud-2.0.0/.github/dependabot.yml +0 -6
- django_ninja_aio_crud-2.0.0/.github/workflows/coverage.yml +0 -32
- django_ninja_aio_crud-2.0.0/.github/workflows/docs.yml +0 -110
- django_ninja_aio_crud-2.0.0/.github/workflows/publish.yml +0 -23
- django_ninja_aio_crud-2.0.0/.gitignore +0 -295
- django_ninja_aio_crud-2.0.0/.pre-commit-config.yaml +0 -22
- django_ninja_aio_crud-2.0.0/docs/CNAME +0 -1
- django_ninja_aio_crud-2.0.0/docs/api/authentication.md +0 -965
- django_ninja_aio_crud-2.0.0/docs/api/models/model_serializer.md +0 -867
- django_ninja_aio_crud-2.0.0/docs/api/models/model_util.md +0 -1121
- django_ninja_aio_crud-2.0.0/docs/api/pagination.md +0 -771
- django_ninja_aio_crud-2.0.0/docs/api/renderers/orjson_renderer.md +0 -20
- django_ninja_aio_crud-2.0.0/docs/api/views/api_view.md +0 -141
- django_ninja_aio_crud-2.0.0/docs/api/views/api_view_set.md +0 -410
- django_ninja_aio_crud-2.0.0/docs/api/views/decorators.md +0 -65
- django_ninja_aio_crud-2.0.0/docs/contributing.md +0 -41
- django_ninja_aio_crud-2.0.0/docs/extra.css +0 -14
- django_ninja_aio_crud-2.0.0/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- django_ninja_aio_crud-2.0.0/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- django_ninja_aio_crud-2.0.0/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- django_ninja_aio_crud-2.0.0/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- django_ninja_aio_crud-2.0.0/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- django_ninja_aio_crud-2.0.0/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- django_ninja_aio_crud-2.0.0/docs/getting_started/installation.md +0 -10
- django_ninja_aio_crud-2.0.0/docs/getting_started/quick_start.md +0 -135
- django_ninja_aio_crud-2.0.0/docs/images/bar-swagger.png +0 -0
- django_ninja_aio_crud-2.0.0/docs/images/favicon.ico +0 -0
- django_ninja_aio_crud-2.0.0/docs/images/foo-swagger.png +0 -0
- django_ninja_aio_crud-2.0.0/docs/images/logo.png +0 -0
- django_ninja_aio_crud-2.0.0/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- django_ninja_aio_crud-2.0.0/docs/index.md +0 -337
- django_ninja_aio_crud-2.0.0/docs/release_notes.md +0 -4
- django_ninja_aio_crud-2.0.0/docs/requirements.txt +0 -8
- django_ninja_aio_crud-2.0.0/docs/tutorial/authentication.md +0 -865
- django_ninja_aio_crud-2.0.0/docs/tutorial/crud.md +0 -871
- django_ninja_aio_crud-2.0.0/docs/tutorial/filtering.md +0 -928
- django_ninja_aio_crud-2.0.0/docs/tutorial/model.md +0 -724
- django_ninja_aio_crud-2.0.0/examples/ex_1/models.py +0 -35
- django_ninja_aio_crud-2.0.0/examples/ex_1/urls.py +0 -11
- django_ninja_aio_crud-2.0.0/examples/ex_1/views.py +0 -16
- django_ninja_aio_crud-2.0.0/examples/ex_2/auth.py +0 -33
- django_ninja_aio_crud-2.0.0/examples/ex_2/models.py +0 -62
- django_ninja_aio_crud-2.0.0/examples/ex_2/urls.py +0 -11
- django_ninja_aio_crud-2.0.0/examples/ex_2/views.py +0 -26
- django_ninja_aio_crud-2.0.0/main.py +0 -245
- django_ninja_aio_crud-2.0.0/mkdocs.yml +0 -153
- django_ninja_aio_crud-2.0.0/requirements.dev.txt +0 -3
- django_ninja_aio_crud-2.0.0/run-local-coverage.sh +0 -10
- django_ninja_aio_crud-2.0.0/tests/__init__.py +0 -0
- django_ninja_aio_crud-2.0.0/tests/core/__init__.py +0 -0
- django_ninja_aio_crud-2.0.0/tests/core/test_decorators.py +0 -42
- django_ninja_aio_crud-2.0.0/tests/core/test_exceptions_api.py +0 -43
- django_ninja_aio_crud-2.0.0/tests/core/test_renderer_parser.py +0 -50
- django_ninja_aio_crud-2.0.0/tests/generics/__init__.py +0 -0
- django_ninja_aio_crud-2.0.0/tests/generics/literals.py +0 -1
- django_ninja_aio_crud-2.0.0/tests/generics/models.py +0 -408
- django_ninja_aio_crud-2.0.0/tests/generics/request.py +0 -19
- django_ninja_aio_crud-2.0.0/tests/generics/views.py +0 -363
- django_ninja_aio_crud-2.0.0/tests/helpers/__init__.py +0 -0
- django_ninja_aio_crud-2.0.0/tests/helpers/test_many_to_many_api.py +0 -118
- django_ninja_aio_crud-2.0.0/tests/models/__init__.py +0 -0
- django_ninja_aio_crud-2.0.0/tests/models/test_model_util.py +0 -68
- django_ninja_aio_crud-2.0.0/tests/models/test_models_extra.py +0 -313
- django_ninja_aio_crud-2.0.0/tests/test_app/__init__.py +0 -0
- django_ninja_aio_crud-2.0.0/tests/test_app/models.py +0 -173
- django_ninja_aio_crud-2.0.0/tests/test_app/schema.py +0 -73
- django_ninja_aio_crud-2.0.0/tests/test_app/views.py +0 -89
- django_ninja_aio_crud-2.0.0/tests/test_decorators.py +0 -84
- django_ninja_aio_crud-2.0.0/tests/test_exceptions.py +0 -138
- django_ninja_aio_crud-2.0.0/tests/test_query_util.py +0 -95
- django_ninja_aio_crud-2.0.0/tests/test_settings.py +0 -48
- django_ninja_aio_crud-2.0.0/tests/views/__init__.py +0 -0
- django_ninja_aio_crud-2.0.0/tests/views/test_views.py +0 -57
- django_ninja_aio_crud-2.0.0/tests/views/test_viewset.py +0 -349
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/LICENSE +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/README.md +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/models.py +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-2.0.0 → django_ninja_aio_crud-2.0.0rc2}/pyproject.toml +0 -0
|
@@ -143,76 +143,3 @@ def unique_view(self: object | str, plural: bool = False):
|
|
|
143
143
|
return func # Return original function (no wrapper)
|
|
144
144
|
|
|
145
145
|
return decorator
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def decorate_view(*decorators):
|
|
149
|
-
"""
|
|
150
|
-
Compose and apply multiple decorators to a view (sync or async) without adding an extra wrapper.
|
|
151
|
-
|
|
152
|
-
This utility was introduced to support class-based patterns where Django Ninja’s
|
|
153
|
-
built-in `decorate_view` does not fit well. For APIs implemented with vanilla
|
|
154
|
-
Django Ninja (function-based style), you should continue using Django Ninja’s
|
|
155
|
-
native `decorate_view`.
|
|
156
|
-
|
|
157
|
-
Behavior:
|
|
158
|
-
- Applies decorators in the same order as Python’s stacking syntax:
|
|
159
|
-
@d1
|
|
160
|
-
@d2
|
|
161
|
-
is equivalent to: view = d1(d2(view))
|
|
162
|
-
- Supports both synchronous and asynchronous views.
|
|
163
|
-
- Ignores None values, enabling conditional decoration.
|
|
164
|
-
- Does not introduce an additional wrapper; composition depends on each
|
|
165
|
-
decorator for signature/metadata preservation (e.g., using functools.wraps).
|
|
166
|
-
|
|
167
|
-
*decorators: Decorator callables to apply to the target view. Any None values
|
|
168
|
-
are skipped.
|
|
169
|
-
|
|
170
|
-
Callable: A decorator that applies the provided decorators in Python stacking order.
|
|
171
|
-
|
|
172
|
-
Method usage in class-based patterns:
|
|
173
|
-
|
|
174
|
-
Args:
|
|
175
|
-
*decorators: Decorator callables to apply to the target view. Any None
|
|
176
|
-
values are skipped.
|
|
177
|
-
|
|
178
|
-
Returns:
|
|
179
|
-
A decorator that applies the provided decorators in Python stacking order.
|
|
180
|
-
|
|
181
|
-
Examples:
|
|
182
|
-
Basic usage:
|
|
183
|
-
class MyAPIViewSet(APIViewSet):
|
|
184
|
-
api = api
|
|
185
|
-
model = MyModel
|
|
186
|
-
|
|
187
|
-
def views(self):
|
|
188
|
-
@self.router.get('some-endpoint/')
|
|
189
|
-
@decorate_view(authenticate, log_request)
|
|
190
|
-
async def some_view(request):
|
|
191
|
-
...
|
|
192
|
-
|
|
193
|
-
Conditional decoration (skips None):
|
|
194
|
-
class MyAPIViewSet(APIViewSet):
|
|
195
|
-
api = api
|
|
196
|
-
model = MyModel
|
|
197
|
-
cache_dec = cache_page(60) if settings.ENABLE_CACHE else None
|
|
198
|
-
def views(self):
|
|
199
|
-
@self.router.get('data/')
|
|
200
|
-
@decorate_view(self.cache_dec, authenticate)
|
|
201
|
-
async def data_view(request):
|
|
202
|
-
...
|
|
203
|
-
|
|
204
|
-
Notes:
|
|
205
|
-
- Each decorator is applied in the order provided, with the first decorator
|
|
206
|
-
wrapping the result of the second, and so on.
|
|
207
|
-
- Ensure that each decorator is compatible with the view’s sync/async nature.
|
|
208
|
-
"""
|
|
209
|
-
|
|
210
|
-
def _decorator(view):
|
|
211
|
-
wrapped = view
|
|
212
|
-
for dec in reversed(decorators):
|
|
213
|
-
if dec is None:
|
|
214
|
-
continue
|
|
215
|
-
wrapped = dec(wrapped)
|
|
216
|
-
return wrapped
|
|
217
|
-
|
|
218
|
-
return _decorator
|
|
@@ -38,34 +38,27 @@ 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
|
-
|
|
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.
|
|
41
|
+
via a `{related_name}_query_params_handler` coroutine.
|
|
45
42
|
- Validates requested add/remove primary keys, producing granular success and
|
|
46
|
-
|
|
43
|
+
error feedback.
|
|
47
44
|
- Performs add/remove operations concurrently using asyncio.gather when both
|
|
48
|
-
|
|
45
|
+
types of operations are requested in the same call.
|
|
49
46
|
|
|
50
47
|
Attributes established at initialization:
|
|
51
48
|
relations: list of M2MRelationSchema defining each relation.
|
|
52
49
|
view_set: The parent APIViewSet instance from which router, pagination, model util,
|
|
53
|
-
|
|
50
|
+
and path schema are derived.
|
|
54
51
|
router: Ninja router used to register generated endpoints.
|
|
55
52
|
pagination_class: Pagination class used for GET related endpoints.
|
|
56
53
|
path_schema: Pydantic schema used to validate path parameters (e.g., primary key).
|
|
57
54
|
related_model_util: A ModelUtil instance cloned from the parent view set to access
|
|
58
|
-
|
|
55
|
+
base object retrieval helpers.
|
|
59
56
|
relations_filters_schemas: Mapping of related_name -> generated Pydantic filter schema.
|
|
60
57
|
|
|
61
58
|
Generated endpoint naming conventions:
|
|
62
59
|
GET -> get_{base_model_name}_{relation_path}
|
|
63
60
|
POST -> manage_{base_model_name}_{relation_path}
|
|
64
61
|
|
|
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
|
-
|
|
69
62
|
All responses standardize success and error reporting for POST as:
|
|
70
63
|
{
|
|
71
64
|
"results": {"count": int, "details": [str, ...]},
|
|
@@ -73,29 +66,25 @@ class ManyToManyAPI:
|
|
|
73
66
|
}
|
|
74
67
|
|
|
75
68
|
Concurrency note:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- Uses related_manager.aadd(...) and related_manager.aremove(...) inside asyncio.gather.
|
|
69
|
+
Add and remove operations are executed concurrently when both lists are non-empty,
|
|
70
|
+
minimizing round-trip latency for bulk mutations.
|
|
79
71
|
|
|
80
72
|
Error semantics:
|
|
81
73
|
- Missing related objects: reported individually.
|
|
82
74
|
- Invalid operation context (e.g., removing objects not currently related or adding
|
|
83
|
-
|
|
75
|
+
objects already related) reported per primary key.
|
|
84
76
|
- Successful operations yield a corresponding success detail string per PK.
|
|
85
77
|
|
|
86
78
|
Security / auth:
|
|
87
79
|
- Each relation may optionally override auth via its schema; otherwise falls back
|
|
88
|
-
|
|
80
|
+
to a default configured on the instance (self.default_auth).
|
|
89
81
|
|
|
90
82
|
Pagination:
|
|
91
83
|
- Applied only to GET related endpoints via @paginate(self.pagination_class).
|
|
92
84
|
|
|
93
85
|
Extensibility:
|
|
94
86
|
- Provide custom query param handling by defining an async method on the parent
|
|
95
|
-
|
|
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.
|
|
87
|
+
view set: `<related_name>_query_params_handler(self, queryset, filters_dict)`.
|
|
99
88
|
- Customize relation filtering schema via each relation's `filters` definition.
|
|
100
89
|
|
|
101
90
|
-----------------------------------------------------------------------
|
|
@@ -125,44 +114,28 @@ class ManyToManyAPI:
|
|
|
125
114
|
|
|
126
115
|
-----------------------------------------------------------------------
|
|
127
116
|
|
|
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`.
|
|
131
|
-
|
|
132
|
-
Parameters:
|
|
133
|
-
related_name (str): The relation's attribute name on the base model.
|
|
134
|
-
|
|
135
|
-
Returns:
|
|
136
|
-
Coroutine | None: Handler to transform or filter the queryset based on query params.
|
|
137
|
-
|
|
138
|
-
-----------------------------------------------------------------------
|
|
139
|
-
|
|
140
117
|
_get_query_handler(related_name)
|
|
141
|
-
Retrieve an optional per-relation
|
|
142
|
-
|
|
143
|
-
should return a queryset from which `.afirst()` will resolve the target object.
|
|
118
|
+
Retrieve an optional per-relation query handler coroutine from the parent view set.
|
|
119
|
+
Naming convention: `<related_name>_query_params_handler`.
|
|
144
120
|
|
|
145
121
|
Parameters:
|
|
146
122
|
related_name (str): The relation's attribute name on the base model.
|
|
147
123
|
|
|
148
124
|
Returns:
|
|
149
|
-
Coroutine | None: Handler to
|
|
125
|
+
Coroutine | None: Handler to transform or filter the queryset based on query params.
|
|
150
126
|
|
|
151
127
|
-----------------------------------------------------------------------
|
|
152
128
|
|
|
153
|
-
_check_m2m_objs(request, objs_pks,
|
|
129
|
+
_check_m2m_objs(request, objs_pks, model, related_manager, remove=False)
|
|
154
130
|
Validate requested primary keys for add/remove operations against the current
|
|
155
131
|
relation state. Performs existence checks and logical consistency (e.g., prevents
|
|
156
|
-
adding already-related objects or removing non-related objects).
|
|
157
|
-
when available; otherwise falls back to ModelUtil(...).get_objects(...) to resolve by pk.
|
|
132
|
+
adding already-related objects or removing non-related objects).
|
|
158
133
|
|
|
159
134
|
Parameters:
|
|
160
135
|
request (HttpRequest): Incoming request context (passed to ModelUtil for access control).
|
|
161
136
|
objs_pks (list): List of primary keys to add or remove.
|
|
162
|
-
|
|
137
|
+
model (ModelSerializer | Model): Model class or serializer used to resolve objects.
|
|
163
138
|
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).
|
|
166
139
|
remove (bool): If True, treat operation as removal validation.
|
|
167
140
|
|
|
168
141
|
Returns:
|
|
@@ -177,7 +150,7 @@ class ManyToManyAPI:
|
|
|
177
150
|
|
|
178
151
|
-----------------------------------------------------------------------
|
|
179
152
|
|
|
180
|
-
_collect_m2m(request, pks, model, related_manager,
|
|
153
|
+
_collect_m2m(request, pks, model, related_manager, remove=False)
|
|
181
154
|
Wrapper around _check_m2m_objs that short-circuits on empty PK lists.
|
|
182
155
|
|
|
183
156
|
Parameters:
|
|
@@ -185,8 +158,6 @@ class ManyToManyAPI:
|
|
|
185
158
|
pks (list): Primary keys proposed for mutation.
|
|
186
159
|
model (ModelSerializer | Model)
|
|
187
160
|
related_manager (QuerySet)
|
|
188
|
-
related_name (str)
|
|
189
|
-
instance (ModelSerializer | Model)
|
|
190
161
|
remove (bool): Operation type flag.
|
|
191
162
|
|
|
192
163
|
Returns:
|
|
@@ -194,30 +165,38 @@ class ManyToManyAPI:
|
|
|
194
165
|
|
|
195
166
|
-----------------------------------------------------------------------
|
|
196
167
|
|
|
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
|
-
|
|
210
168
|
_build_views(relation)
|
|
211
169
|
Dynamically define and register the GET and/or POST endpoints for a single M2M
|
|
212
170
|
relation based on the relation's schema flags (get/add/remove). Builds filter
|
|
213
171
|
schemas, resolves path fragments, and binds handlers to the router with unique
|
|
214
172
|
operation IDs.
|
|
215
173
|
|
|
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
|
+
|
|
216
192
|
-----------------------------------------------------------------------
|
|
217
193
|
|
|
218
194
|
_add_views()
|
|
219
195
|
Iterates over all declared relations and invokes _build_views to attach endpoints.
|
|
220
196
|
|
|
197
|
+
Side effects:
|
|
198
|
+
- Populates router with all required M2M endpoints.
|
|
199
|
+
|
|
221
200
|
-----------------------------------------------------------------------
|
|
222
201
|
|
|
223
202
|
Usage Example (conceptual):
|
|
@@ -262,56 +241,41 @@ class ManyToManyAPI:
|
|
|
262
241
|
for data in self.relations
|
|
263
242
|
}
|
|
264
243
|
|
|
265
|
-
def _get_query_params_handler(self, related_name: str) -> Coroutine | None:
|
|
266
|
-
return getattr(self.view_set, f"{related_name}_query_params_handler", None)
|
|
267
|
-
|
|
268
244
|
def _get_query_handler(self, related_name: str) -> Coroutine | None:
|
|
269
|
-
return getattr(self.view_set, f"{related_name}
|
|
245
|
+
return getattr(self.view_set, f"{related_name}_query_params_handler", None)
|
|
270
246
|
|
|
271
247
|
async def _check_m2m_objs(
|
|
272
248
|
self,
|
|
273
249
|
request: HttpRequest,
|
|
274
250
|
objs_pks: list,
|
|
275
|
-
|
|
251
|
+
model: ModelSerializer | Model,
|
|
276
252
|
related_manager: QuerySet,
|
|
277
|
-
related_name: str,
|
|
278
|
-
instance: ModelSerializer | Model,
|
|
279
253
|
remove: bool = False,
|
|
280
254
|
):
|
|
281
255
|
"""
|
|
282
256
|
Validate requested add/remove pk list for M2M operations.
|
|
283
257
|
Returns (errors, details, objects_to_process).
|
|
284
|
-
Uses per-PK query handler if available, else falls back to ModelUtil lookup by pk.
|
|
285
258
|
"""
|
|
286
259
|
errors, objs_detail, objs = [], [], []
|
|
287
260
|
rel_objs = [rel_obj async for rel_obj in related_manager.select_related().all()]
|
|
288
|
-
rel_model_name =
|
|
261
|
+
rel_model_name = model._meta.verbose_name.capitalize()
|
|
289
262
|
for obj_pk in objs_pks:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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()
|
|
263
|
+
rel_obj = await (
|
|
264
|
+
await ModelUtil(model).get_objects(
|
|
265
|
+
request, query_data=ObjectsQuerySchema(filters={"pk": obj_pk})
|
|
266
|
+
)
|
|
267
|
+
).afirst()
|
|
304
268
|
if rel_obj is None:
|
|
305
269
|
errors.append(f"{rel_model_name} with pk {obj_pk} not found.")
|
|
306
270
|
continue
|
|
307
271
|
if remove ^ (rel_obj in rel_objs):
|
|
308
272
|
errors.append(
|
|
309
|
-
f"{rel_model_name} with
|
|
273
|
+
f"{rel_model_name} with id {obj_pk} is {'not ' if remove else ''}in {self.related_model_util.model_name}"
|
|
310
274
|
)
|
|
311
275
|
continue
|
|
312
276
|
objs.append(rel_obj)
|
|
313
277
|
objs_detail.append(
|
|
314
|
-
f"{rel_model_name} with
|
|
278
|
+
f"{rel_model_name} with id {obj_pk} successfully {'removed' if remove else 'added'}"
|
|
315
279
|
)
|
|
316
280
|
return errors, objs_detail, objs
|
|
317
281
|
|
|
@@ -319,22 +283,14 @@ class ManyToManyAPI:
|
|
|
319
283
|
self,
|
|
320
284
|
request: HttpRequest,
|
|
321
285
|
pks: list,
|
|
322
|
-
|
|
286
|
+
model: ModelSerializer | Model,
|
|
323
287
|
related_manager: QuerySet,
|
|
324
|
-
|
|
325
|
-
instance: ModelSerializer | Model,
|
|
326
|
-
remove: bool = False,
|
|
288
|
+
remove=False,
|
|
327
289
|
):
|
|
328
290
|
if not pks:
|
|
329
291
|
return ([], [], [])
|
|
330
292
|
return await self._check_m2m_objs(
|
|
331
|
-
request,
|
|
332
|
-
pks,
|
|
333
|
-
reletad_model,
|
|
334
|
-
related_manager,
|
|
335
|
-
related_name,
|
|
336
|
-
instance,
|
|
337
|
-
remove=remove,
|
|
293
|
+
request, pks, model, related_manager, remove=remove
|
|
338
294
|
)
|
|
339
295
|
|
|
340
296
|
def _register_get_relation_view(
|
|
@@ -370,14 +326,16 @@ class ManyToManyAPI:
|
|
|
370
326
|
related_manager = getattr(obj, related_name)
|
|
371
327
|
related_qs = related_manager.all()
|
|
372
328
|
|
|
373
|
-
query_handler = self.
|
|
329
|
+
query_handler = self._get_query_handler(related_name)
|
|
374
330
|
if filters is not None and query_handler:
|
|
375
331
|
if asyncio.iscoroutinefunction(query_handler):
|
|
376
332
|
related_qs = await query_handler(related_qs, filters.model_dump())
|
|
377
333
|
else:
|
|
378
334
|
related_qs = query_handler(related_qs, filters.model_dump())
|
|
379
335
|
|
|
380
|
-
return await rel_util.list_read_s(
|
|
336
|
+
return await rel_util.list_read_s(
|
|
337
|
+
related_schema, request, related_qs
|
|
338
|
+
)
|
|
381
339
|
|
|
382
340
|
def _resolve_action_schema(self, add: bool, remove: bool):
|
|
383
341
|
return self.views_action_map[(add, remove)]
|
|
@@ -385,7 +343,7 @@ class ManyToManyAPI:
|
|
|
385
343
|
def _register_manage_relation_view(
|
|
386
344
|
self,
|
|
387
345
|
*,
|
|
388
|
-
|
|
346
|
+
model,
|
|
389
347
|
related_name: str,
|
|
390
348
|
m2m_auth,
|
|
391
349
|
rel_util: ModelUtil,
|
|
@@ -422,21 +380,10 @@ class ManyToManyAPI:
|
|
|
422
380
|
remove_pks = getattr(data, "remove", []) if m2m_remove else []
|
|
423
381
|
|
|
424
382
|
add_errors, add_details, add_objs = await self._collect_m2m(
|
|
425
|
-
request,
|
|
426
|
-
add_pks,
|
|
427
|
-
related_model,
|
|
428
|
-
related_manager,
|
|
429
|
-
related_name,
|
|
430
|
-
obj,
|
|
383
|
+
request, add_pks, model, related_manager
|
|
431
384
|
)
|
|
432
385
|
remove_errors, remove_details, remove_objs = await self._collect_m2m(
|
|
433
|
-
request,
|
|
434
|
-
remove_pks,
|
|
435
|
-
related_model,
|
|
436
|
-
related_manager,
|
|
437
|
-
related_name,
|
|
438
|
-
obj,
|
|
439
|
-
remove=True,
|
|
386
|
+
request, remove_pks, model, related_manager, remove=True
|
|
440
387
|
)
|
|
441
388
|
|
|
442
389
|
tasks = []
|
|
@@ -476,7 +423,7 @@ class ManyToManyAPI:
|
|
|
476
423
|
|
|
477
424
|
if m2m_add or m2m_remove:
|
|
478
425
|
self._register_manage_relation_view(
|
|
479
|
-
|
|
426
|
+
model=model,
|
|
480
427
|
related_name=related_name,
|
|
481
428
|
m2m_auth=m2m_auth,
|
|
482
429
|
rel_util=rel_util,
|
|
@@ -4,13 +4,11 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
import orjson
|
|
6
6
|
from django.http import HttpRequest
|
|
7
|
-
from django.conf import settings
|
|
8
7
|
from ninja.renderers import BaseRenderer
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class ORJSONRenderer(BaseRenderer):
|
|
12
11
|
media_type = "application/json"
|
|
13
|
-
option = getattr(settings, "NINJA_AIO_ORJSON_RENDERER_OPTION", None)
|
|
14
12
|
|
|
15
13
|
def render(self, request: HttpRequest, data: dict, *, response_status):
|
|
16
14
|
try:
|
|
@@ -18,13 +16,9 @@ class ORJSONRenderer(BaseRenderer):
|
|
|
18
16
|
for k, v in old_d.items():
|
|
19
17
|
if isinstance(v, list):
|
|
20
18
|
data |= {k: self.render_list(v)}
|
|
21
|
-
return
|
|
19
|
+
return orjson.dumps(self.render_dict(data))
|
|
22
20
|
except AttributeError:
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
@classmethod
|
|
26
|
-
def dumps(cls, data: dict) -> bytes:
|
|
27
|
-
return orjson.dumps(data, option=cls.option)
|
|
21
|
+
return orjson.dumps(data)
|
|
28
22
|
|
|
29
23
|
@classmethod
|
|
30
24
|
def render_list(cls, data: list[dict]) -> list[dict]:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Optional, Type
|
|
2
2
|
|
|
3
3
|
from ninja import Schema
|
|
4
4
|
from ninja_aio.types import ModelSerializerMeta
|
|
@@ -79,12 +79,4 @@ class QuerySchema(ModelQuerySetSchema):
|
|
|
79
79
|
|
|
80
80
|
class QueryUtilBaseScopesSchema(BaseModel):
|
|
81
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] = []
|
|
82
|
+
QUERYSET_REQUEST: str = "queryset_request"
|
|
@@ -7,7 +7,7 @@ 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
|
|
10
|
+
from ninja_aio.schemas.helpers import ModelQuerySetSchema, QuerySchema
|
|
11
11
|
|
|
12
12
|
from .models import ModelSerializer, ModelUtil
|
|
13
13
|
from .schemas import (
|
|
@@ -16,7 +16,7 @@ from .schemas import (
|
|
|
16
16
|
)
|
|
17
17
|
from .helpers.api import ManyToManyAPI
|
|
18
18
|
from .types import ModelSerializerMeta, VIEW_TYPES
|
|
19
|
-
from .decorators import unique_view
|
|
19
|
+
from .decorators import unique_view
|
|
20
20
|
|
|
21
21
|
ERROR_CODES = frozenset({400, 401, 404, 428})
|
|
22
22
|
|
|
@@ -158,7 +158,6 @@ class APIViewSet:
|
|
|
158
158
|
|
|
159
159
|
model: ModelSerializer | Model
|
|
160
160
|
api: NinjaAPI
|
|
161
|
-
router_tag: str = ""
|
|
162
161
|
schema_in: Schema | None = None
|
|
163
162
|
schema_out: Schema | None = None
|
|
164
163
|
schema_update: Schema | None = None
|
|
@@ -178,7 +177,6 @@ class APIViewSet:
|
|
|
178
177
|
delete_docs = "Delete an object by its primary key."
|
|
179
178
|
m2m_relations: list[M2MRelationSchema] = []
|
|
180
179
|
m2m_auth: list | None = NOT_SET
|
|
181
|
-
extra_decorators: DecoratorsSchema = DecoratorsSchema()
|
|
182
180
|
|
|
183
181
|
def __init__(self) -> None:
|
|
184
182
|
self.error_codes = ERROR_CODES
|
|
@@ -191,9 +189,7 @@ class APIViewSet:
|
|
|
191
189
|
self.path_schema = self._generate_path_schema()
|
|
192
190
|
self.filters_schema = self._generate_filters_schema()
|
|
193
191
|
self.model_verbose_name = self.model._meta.verbose_name.capitalize()
|
|
194
|
-
self.router_tag =
|
|
195
|
-
self.model_verbose_name if not self.router_tag else self.router_tag
|
|
196
|
-
)
|
|
192
|
+
self.router_tag = self.model_verbose_name
|
|
197
193
|
self.router = Router(tags=[self.router_tag])
|
|
198
194
|
self.path = "/"
|
|
199
195
|
self.get_path = ""
|
|
@@ -310,7 +306,7 @@ class APIViewSet:
|
|
|
310
306
|
description=self.create_docs,
|
|
311
307
|
response={201: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
312
308
|
)
|
|
313
|
-
@
|
|
309
|
+
@unique_view(self)
|
|
314
310
|
async def create(request: HttpRequest, data: self.schema_in): # type: ignore
|
|
315
311
|
return 201, await self.model_util.create_s(request, data, self.schema_out)
|
|
316
312
|
|
|
@@ -331,11 +327,8 @@ class APIViewSet:
|
|
|
331
327
|
self.error_codes: GenericMessageSchema,
|
|
332
328
|
},
|
|
333
329
|
)
|
|
334
|
-
@
|
|
335
|
-
|
|
336
|
-
unique_view(self, plural=True),
|
|
337
|
-
*self.extra_decorators.list,
|
|
338
|
-
)
|
|
330
|
+
@unique_view(self, plural=True)
|
|
331
|
+
@paginate(self.pagination_class)
|
|
339
332
|
async def list(
|
|
340
333
|
request: HttpRequest,
|
|
341
334
|
filters: Query[self.filters_schema] = None, # type: ignore
|
|
@@ -363,7 +356,7 @@ class APIViewSet:
|
|
|
363
356
|
description=self.retrieve_docs,
|
|
364
357
|
response={200: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
365
358
|
)
|
|
366
|
-
@
|
|
359
|
+
@unique_view(self)
|
|
367
360
|
async def retrieve(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
|
|
368
361
|
query_data = self._get_query_data()
|
|
369
362
|
return await self.model_util.read_s(
|
|
@@ -388,7 +381,7 @@ class APIViewSet:
|
|
|
388
381
|
description=self.update_docs,
|
|
389
382
|
response={200: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
390
383
|
)
|
|
391
|
-
@
|
|
384
|
+
@unique_view(self)
|
|
392
385
|
async def update(
|
|
393
386
|
request: HttpRequest,
|
|
394
387
|
data: self.schema_update, # type: ignore
|
|
@@ -412,7 +405,7 @@ class APIViewSet:
|
|
|
412
405
|
description=self.delete_docs,
|
|
413
406
|
response={204: None, self.error_codes: GenericMessageSchema},
|
|
414
407
|
)
|
|
415
|
-
@
|
|
408
|
+
@unique_view(self)
|
|
416
409
|
async def delete(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
|
|
417
410
|
return 204, await self.model_util.delete_s(request, self._get_pk(pk))
|
|
418
411
|
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
name: Coverage
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ main ]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [ main ]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test_coverage:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v6
|
|
15
|
-
- name: Set up Python
|
|
16
|
-
uses: actions/setup-python@v6
|
|
17
|
-
with:
|
|
18
|
-
python-version: '3.10'
|
|
19
|
-
- name: Install Flit
|
|
20
|
-
run: pip install flit
|
|
21
|
-
- name: Install Dependencies
|
|
22
|
-
run: flit install --symlink
|
|
23
|
-
- name: Test
|
|
24
|
-
run: |
|
|
25
|
-
coverage run -m django test --settings=tests.test_settings
|
|
26
|
-
coverage report
|
|
27
|
-
coverage xml
|
|
28
|
-
|
|
29
|
-
- name: Coverage
|
|
30
|
-
uses: codecov/codecov-action@v5.5.1
|
|
31
|
-
with:
|
|
32
|
-
token: ${{ secrets.CODECOV_TOKEN }}
|