django-ninja-aio-crud 1.0.0__tar.gz → 1.0.2__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.
Files changed (21) hide show
  1. django_ninja_aio_crud-1.0.2/PKG-INFO +338 -0
  2. django_ninja_aio_crud-1.0.2/README.md +304 -0
  3. {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/__init__.py +1 -1
  4. {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/exceptions.py +1 -1
  5. django_ninja_aio_crud-1.0.2/ninja_aio/helpers/__init__.py +3 -0
  6. django_ninja_aio_crud-1.0.2/ninja_aio/helpers/api.py +432 -0
  7. django_ninja_aio_crud-1.0.2/ninja_aio/models.py +1203 -0
  8. django_ninja_aio_crud-1.0.2/ninja_aio/renders.py +47 -0
  9. {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/schemas.py +16 -1
  10. {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/views.py +19 -193
  11. {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/pyproject.toml +1 -1
  12. django_ninja_aio_crud-1.0.0/PKG-INFO +0 -527
  13. django_ninja_aio_crud-1.0.0/README.md +0 -493
  14. django_ninja_aio_crud-1.0.0/ninja_aio/models.py +0 -887
  15. django_ninja_aio_crud-1.0.0/ninja_aio/renders.py +0 -54
  16. {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/LICENSE +0 -0
  17. {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/api.py +0 -0
  18. {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/auth.py +0 -0
  19. {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/decorators.py +0 -0
  20. {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/parsers.py +0 -0
  21. {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/types.py +0 -0
@@ -0,0 +1,338 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-ninja-aio-crud
3
+ Version: 1.0.2
4
+ Summary: Django Ninja AIO CRUD - Rest Framework
5
+ Author: Giuseppe Casillo
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Internet
10
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
11
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Classifier: Topic :: Software Development
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Framework :: Django
22
+ Classifier: Framework :: AsyncIO
23
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ License-File: LICENSE
26
+ Requires-Dist: django-ninja >=1.3.0
27
+ Requires-Dist: joserfc >=1.0.0
28
+ Requires-Dist: orjson >= 3.10.7
29
+ Requires-Dist: coverage ; extra == "test"
30
+ Project-URL: Documentation, https://django-ninja-aio.com
31
+ Project-URL: Repository, https://github.com/caspel26/django-ninja-aio-crud
32
+ Provides-Extra: test
33
+
34
+ # 🥷 django-ninja-aio-crud
35
+
36
+ ![Tests](https://github.com/caspel26/django-ninja-aio-crud/actions/workflows/coverage.yml/badge.svg)
37
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=caspel26_django-ninja-aio-crud&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=caspel26_django-ninja-aio-crud)
38
+ [![codecov](https://codecov.io/gh/caspel26/django-ninja-aio-crud/graph/badge.svg?token=DZ5WDT3S20)](https://codecov.io/gh/caspel26/django-ninja-aio-crud/)
39
+ [![PyPI - Version](https://img.shields.io/pypi/v/django-ninja-aio-crud?color=g&logo=pypi&logoColor=white)](https://pypi.org/project/django-ninja-aio-crud/)
40
+ [![PyPI - License](https://img.shields.io/pypi/l/django-ninja-aio-crud)](LICENSE)
41
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
42
+
43
+ > Lightweight async CRUD layer on top of **[Django Ninja](https://django-ninja.dev/)** with automatic schema generation, filtering, pagination, auth & Many‑to‑Many management.
44
+
45
+ ---
46
+
47
+ ## ✨ Features
48
+
49
+ - Async CRUD ViewSets (create, list, retrieve, update, delete)
50
+ - Automatic Pydantic schemas from `ModelSerializer` (read/create/update)
51
+ - Dynamic query params (runtime schema via `pydantic.create_model`)
52
+ - Per-method authentication (`auth`, `get_auth`, `post_auth`, etc.)
53
+ - Async pagination (customizable)
54
+ - M2M relation endpoints via `M2MRelationSchema` (add/remove/get + filters)
55
+ - Reverse relation serialization
56
+ - Hook methods (`query_params_handler`, `<related>_query_params_handler`, `custom_actions`, lifecycle hooks)
57
+ - ORJSON renderer through `NinjaAIO`
58
+ - Clean, minimal integration
59
+
60
+ ---
61
+
62
+ ## 📦 Installation
63
+
64
+ ```bash
65
+ pip install django-ninja-aio-crud
66
+ ```
67
+
68
+ Add to your project’s dependencies and ensure Django Ninja is installed.
69
+
70
+ ---
71
+
72
+ ## 🚀 Quick Start
73
+
74
+ models.py
75
+ ```python
76
+ from django.db import models
77
+ from ninja_aio.models import ModelSerializer
78
+
79
+ class Book(ModelSerializer):
80
+ title = models.CharField(max_length=120)
81
+ published = models.BooleanField(default=True)
82
+
83
+ class ReadSerializer:
84
+ fields = ["id", "title", "published"]
85
+
86
+ class CreateSerializer:
87
+ fields = ["title", "published"]
88
+
89
+ class UpdateSerializer:
90
+ optionals = [("title", str), ("published", bool)]
91
+ ```
92
+
93
+ views.py
94
+ ```python
95
+ from ninja_aio import NinjaAIO
96
+ from ninja_aio.views import APIViewSet
97
+ from .models import Book
98
+
99
+ api = NinjaAIO()
100
+
101
+ class BookViewSet(APIViewSet):
102
+ model = Book
103
+ api = api
104
+
105
+ BookViewSet().add_views_to_route()
106
+ ```
107
+
108
+ Visit `/docs` → CRUD endpoints ready.
109
+
110
+ ---
111
+
112
+ ## 🔄 Query Filtering
113
+
114
+ ```python
115
+ class BookViewSet(APIViewSet):
116
+ model = Book
117
+ api = api
118
+ query_params = {"published": (bool, None), "title": (str, None)}
119
+
120
+ async def query_params_handler(self, queryset, filters):
121
+ if filters.get("published") is not None:
122
+ queryset = queryset.filter(published=filters["published"])
123
+ if filters.get("title"):
124
+ queryset = queryset.filter(title__icontains=filters["title"])
125
+ return queryset
126
+ ```
127
+
128
+ Request:
129
+ ```
130
+ GET /book/?published=true&title=python
131
+ ```
132
+
133
+ ---
134
+
135
+ ## 🤝 Many-to-Many Example (with filters)
136
+
137
+ ```python
138
+ from ninja_aio.schemas import M2MRelationSchema
139
+
140
+ class Tag(ModelSerializer):
141
+ name = models.CharField(max_length=50)
142
+ class ReadSerializer:
143
+ fields = ["id", "name"]
144
+
145
+ class Article(ModelSerializer):
146
+ title = models.CharField(max_length=120)
147
+ tags = models.ManyToManyField(Tag, related_name="articles")
148
+
149
+ class ReadSerializer:
150
+ fields = ["id", "title", "tags"]
151
+
152
+ class ArticleViewSet(APIViewSet):
153
+ model = Article
154
+ api = api
155
+ m2m_relations = [
156
+ M2MRelationSchema(
157
+ model=Tag,
158
+ related_name="tags",
159
+ filters={"name": (str, "")}
160
+ )
161
+ ]
162
+
163
+ async def tags_query_params_handler(self, queryset, filters):
164
+ n = filters.get("name")
165
+ if n:
166
+ queryset = queryset.filter(name__icontains=n)
167
+ return queryset
168
+
169
+ ArticleViewSet().add_views_to_route()
170
+ ```
171
+
172
+ Endpoints:
173
+ - `GET /article/{pk}/tag?name=dev`
174
+ - `POST /article/{pk}/tag/` body: `{"add":[1,2],"remove":[3]}`
175
+
176
+ ---
177
+
178
+ ## 🔐 Authentication (JWT example)
179
+
180
+ ```python
181
+ from ninja_aio.auth import AsyncJwtBearer
182
+ from joserfc import jwk
183
+ from .models import Book
184
+
185
+ PUBLIC_KEY = "-----BEGIN PUBLIC KEY----- ..."
186
+
187
+ class JWTAuth(AsyncJwtBearer):
188
+ jwt_public = jwk.RSAKey.import_key(PUBLIC_KEY)
189
+ jwt_alg = "RS256"
190
+ claims = {"sub": {"essential": True}}
191
+
192
+ async def auth_handler(self, request):
193
+ book_id = self.dcd.claims.get("sub")
194
+ return await Book.objects.aget(id=book_id)
195
+
196
+ class SecureBookViewSet(APIViewSet):
197
+ model = Book
198
+ api = api
199
+ auth = [JWTAuth()]
200
+ get_auth = None # list/retrieve public
201
+ ```
202
+
203
+ ---
204
+
205
+ ## 📑 Lifecycle Hooks (ModelSerializer)
206
+
207
+ Available on every save/delete:
208
+ - `on_create_before_save`
209
+ - `on_create_after_save`
210
+ - `before_save`
211
+ - `after_save`
212
+ - `on_delete`
213
+ - `custom_actions(payload)` (create/update custom field logic)
214
+ - `post_create()` (after create commit)
215
+
216
+ ---
217
+
218
+ ## 🧩 Adding Custom Endpoints
219
+
220
+ ```python
221
+ class BookViewSet(APIViewSet):
222
+ model = Book
223
+ api = api
224
+
225
+ def views(self):
226
+ @self.router.get("/stats/")
227
+ async def stats(request):
228
+ total = await Book.objects.acount()
229
+ return {"total": total}
230
+ ```
231
+
232
+ ---
233
+
234
+ ## 📄 Pagination
235
+
236
+ Default: `PageNumberPagination`. Override:
237
+
238
+ ```python
239
+ from ninja.pagination import PageNumberPagination
240
+
241
+ class LargePagination(PageNumberPagination):
242
+ page_size = 50
243
+ max_page_size = 200
244
+
245
+ class BookViewSet(APIViewSet):
246
+ model = Book
247
+ api = api
248
+ pagination_class = LargePagination
249
+ ```
250
+
251
+ ---
252
+
253
+ ## 🛠 Project Structure & Docs
254
+
255
+ Documentation (MkDocs + Material):
256
+ ```
257
+ docs/
258
+ getting_started/
259
+ tutorial/
260
+ api/
261
+ views/
262
+ models/
263
+ authentication.md
264
+ pagination.md
265
+ ```
266
+
267
+ Browse full reference:
268
+ - APIViewSet: `docs/api/views/api_view_set.md`
269
+ - APIView: `docs/api/views/api_view.md`
270
+ - ModelSerializer: `docs/api/models/model_serializer.md`
271
+ - Authentication: `docs/api/authentication.md`
272
+ - Pagination: `docs/api/pagination.md`
273
+
274
+ ---
275
+
276
+ ## 🧪 Tests
277
+
278
+ Use Django test runner + async ORM patterns. Example async pattern:
279
+ ```python
280
+ obj = await Book.objects.acreate(title="T1", published=True)
281
+ count = await Book.objects.acount()
282
+ ```
283
+
284
+ ---
285
+
286
+ ## 🚫 Disable Operations
287
+
288
+ ```python
289
+ class ReadOnlyBookViewSet(APIViewSet):
290
+ model = Book
291
+ api = api
292
+ disable = ["update", "delete"]
293
+ ```
294
+
295
+ ---
296
+
297
+ ## 📌 Performance Tips
298
+
299
+ - Use `queryset_request` classmethod to prefetch
300
+ - Index frequently filtered fields
301
+ - Keep pagination enabled
302
+ - Limit slices (`queryset = queryset[:1000]`) for heavy searches
303
+
304
+ ---
305
+
306
+ ## 🤲 Contributing
307
+
308
+ 1. Fork
309
+ 2. Create branch
310
+ 3. Add tests
311
+ 4. Run lint (`ruff check .`)
312
+ 5. Open PR
313
+
314
+ ---
315
+
316
+ ## ⭐ Support
317
+
318
+ Star the repo or donate:
319
+ - [Buy me a coffee](https://buymeacoffee.com/caspel26)
320
+
321
+ ---
322
+
323
+ ## 📜 License
324
+
325
+ MIT License. See [LICENSE](LICENSE).
326
+
327
+ ---
328
+
329
+ ## 🔗 Quick Links
330
+
331
+ | Item | Link |
332
+ |------|------|
333
+ | PyPI | https://pypi.org/project/django-ninja-aio-crud/ |
334
+ | Docs | https://django-ninja-aio.com
335
+ | Issues | https://github.com/caspel26/django-ninja-aio-crud/issues |
336
+
337
+ ---
338
+
@@ -0,0 +1,304 @@
1
+ # 🥷 django-ninja-aio-crud
2
+
3
+ ![Tests](https://github.com/caspel26/django-ninja-aio-crud/actions/workflows/coverage.yml/badge.svg)
4
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=caspel26_django-ninja-aio-crud&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=caspel26_django-ninja-aio-crud)
5
+ [![codecov](https://codecov.io/gh/caspel26/django-ninja-aio-crud/graph/badge.svg?token=DZ5WDT3S20)](https://codecov.io/gh/caspel26/django-ninja-aio-crud/)
6
+ [![PyPI - Version](https://img.shields.io/pypi/v/django-ninja-aio-crud?color=g&logo=pypi&logoColor=white)](https://pypi.org/project/django-ninja-aio-crud/)
7
+ [![PyPI - License](https://img.shields.io/pypi/l/django-ninja-aio-crud)](LICENSE)
8
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
9
+
10
+ > Lightweight async CRUD layer on top of **[Django Ninja](https://django-ninja.dev/)** with automatic schema generation, filtering, pagination, auth & Many‑to‑Many management.
11
+
12
+ ---
13
+
14
+ ## ✨ Features
15
+
16
+ - Async CRUD ViewSets (create, list, retrieve, update, delete)
17
+ - Automatic Pydantic schemas from `ModelSerializer` (read/create/update)
18
+ - Dynamic query params (runtime schema via `pydantic.create_model`)
19
+ - Per-method authentication (`auth`, `get_auth`, `post_auth`, etc.)
20
+ - Async pagination (customizable)
21
+ - M2M relation endpoints via `M2MRelationSchema` (add/remove/get + filters)
22
+ - Reverse relation serialization
23
+ - Hook methods (`query_params_handler`, `<related>_query_params_handler`, `custom_actions`, lifecycle hooks)
24
+ - ORJSON renderer through `NinjaAIO`
25
+ - Clean, minimal integration
26
+
27
+ ---
28
+
29
+ ## 📦 Installation
30
+
31
+ ```bash
32
+ pip install django-ninja-aio-crud
33
+ ```
34
+
35
+ Add to your project’s dependencies and ensure Django Ninja is installed.
36
+
37
+ ---
38
+
39
+ ## 🚀 Quick Start
40
+
41
+ models.py
42
+ ```python
43
+ from django.db import models
44
+ from ninja_aio.models import ModelSerializer
45
+
46
+ class Book(ModelSerializer):
47
+ title = models.CharField(max_length=120)
48
+ published = models.BooleanField(default=True)
49
+
50
+ class ReadSerializer:
51
+ fields = ["id", "title", "published"]
52
+
53
+ class CreateSerializer:
54
+ fields = ["title", "published"]
55
+
56
+ class UpdateSerializer:
57
+ optionals = [("title", str), ("published", bool)]
58
+ ```
59
+
60
+ views.py
61
+ ```python
62
+ from ninja_aio import NinjaAIO
63
+ from ninja_aio.views import APIViewSet
64
+ from .models import Book
65
+
66
+ api = NinjaAIO()
67
+
68
+ class BookViewSet(APIViewSet):
69
+ model = Book
70
+ api = api
71
+
72
+ BookViewSet().add_views_to_route()
73
+ ```
74
+
75
+ Visit `/docs` → CRUD endpoints ready.
76
+
77
+ ---
78
+
79
+ ## 🔄 Query Filtering
80
+
81
+ ```python
82
+ class BookViewSet(APIViewSet):
83
+ model = Book
84
+ api = api
85
+ query_params = {"published": (bool, None), "title": (str, None)}
86
+
87
+ async def query_params_handler(self, queryset, filters):
88
+ if filters.get("published") is not None:
89
+ queryset = queryset.filter(published=filters["published"])
90
+ if filters.get("title"):
91
+ queryset = queryset.filter(title__icontains=filters["title"])
92
+ return queryset
93
+ ```
94
+
95
+ Request:
96
+ ```
97
+ GET /book/?published=true&title=python
98
+ ```
99
+
100
+ ---
101
+
102
+ ## 🤝 Many-to-Many Example (with filters)
103
+
104
+ ```python
105
+ from ninja_aio.schemas import M2MRelationSchema
106
+
107
+ class Tag(ModelSerializer):
108
+ name = models.CharField(max_length=50)
109
+ class ReadSerializer:
110
+ fields = ["id", "name"]
111
+
112
+ class Article(ModelSerializer):
113
+ title = models.CharField(max_length=120)
114
+ tags = models.ManyToManyField(Tag, related_name="articles")
115
+
116
+ class ReadSerializer:
117
+ fields = ["id", "title", "tags"]
118
+
119
+ class ArticleViewSet(APIViewSet):
120
+ model = Article
121
+ api = api
122
+ m2m_relations = [
123
+ M2MRelationSchema(
124
+ model=Tag,
125
+ related_name="tags",
126
+ filters={"name": (str, "")}
127
+ )
128
+ ]
129
+
130
+ async def tags_query_params_handler(self, queryset, filters):
131
+ n = filters.get("name")
132
+ if n:
133
+ queryset = queryset.filter(name__icontains=n)
134
+ return queryset
135
+
136
+ ArticleViewSet().add_views_to_route()
137
+ ```
138
+
139
+ Endpoints:
140
+ - `GET /article/{pk}/tag?name=dev`
141
+ - `POST /article/{pk}/tag/` body: `{"add":[1,2],"remove":[3]}`
142
+
143
+ ---
144
+
145
+ ## 🔐 Authentication (JWT example)
146
+
147
+ ```python
148
+ from ninja_aio.auth import AsyncJwtBearer
149
+ from joserfc import jwk
150
+ from .models import Book
151
+
152
+ PUBLIC_KEY = "-----BEGIN PUBLIC KEY----- ..."
153
+
154
+ class JWTAuth(AsyncJwtBearer):
155
+ jwt_public = jwk.RSAKey.import_key(PUBLIC_KEY)
156
+ jwt_alg = "RS256"
157
+ claims = {"sub": {"essential": True}}
158
+
159
+ async def auth_handler(self, request):
160
+ book_id = self.dcd.claims.get("sub")
161
+ return await Book.objects.aget(id=book_id)
162
+
163
+ class SecureBookViewSet(APIViewSet):
164
+ model = Book
165
+ api = api
166
+ auth = [JWTAuth()]
167
+ get_auth = None # list/retrieve public
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 📑 Lifecycle Hooks (ModelSerializer)
173
+
174
+ Available on every save/delete:
175
+ - `on_create_before_save`
176
+ - `on_create_after_save`
177
+ - `before_save`
178
+ - `after_save`
179
+ - `on_delete`
180
+ - `custom_actions(payload)` (create/update custom field logic)
181
+ - `post_create()` (after create commit)
182
+
183
+ ---
184
+
185
+ ## 🧩 Adding Custom Endpoints
186
+
187
+ ```python
188
+ class BookViewSet(APIViewSet):
189
+ model = Book
190
+ api = api
191
+
192
+ def views(self):
193
+ @self.router.get("/stats/")
194
+ async def stats(request):
195
+ total = await Book.objects.acount()
196
+ return {"total": total}
197
+ ```
198
+
199
+ ---
200
+
201
+ ## 📄 Pagination
202
+
203
+ Default: `PageNumberPagination`. Override:
204
+
205
+ ```python
206
+ from ninja.pagination import PageNumberPagination
207
+
208
+ class LargePagination(PageNumberPagination):
209
+ page_size = 50
210
+ max_page_size = 200
211
+
212
+ class BookViewSet(APIViewSet):
213
+ model = Book
214
+ api = api
215
+ pagination_class = LargePagination
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 🛠 Project Structure & Docs
221
+
222
+ Documentation (MkDocs + Material):
223
+ ```
224
+ docs/
225
+ getting_started/
226
+ tutorial/
227
+ api/
228
+ views/
229
+ models/
230
+ authentication.md
231
+ pagination.md
232
+ ```
233
+
234
+ Browse full reference:
235
+ - APIViewSet: `docs/api/views/api_view_set.md`
236
+ - APIView: `docs/api/views/api_view.md`
237
+ - ModelSerializer: `docs/api/models/model_serializer.md`
238
+ - Authentication: `docs/api/authentication.md`
239
+ - Pagination: `docs/api/pagination.md`
240
+
241
+ ---
242
+
243
+ ## 🧪 Tests
244
+
245
+ Use Django test runner + async ORM patterns. Example async pattern:
246
+ ```python
247
+ obj = await Book.objects.acreate(title="T1", published=True)
248
+ count = await Book.objects.acount()
249
+ ```
250
+
251
+ ---
252
+
253
+ ## 🚫 Disable Operations
254
+
255
+ ```python
256
+ class ReadOnlyBookViewSet(APIViewSet):
257
+ model = Book
258
+ api = api
259
+ disable = ["update", "delete"]
260
+ ```
261
+
262
+ ---
263
+
264
+ ## 📌 Performance Tips
265
+
266
+ - Use `queryset_request` classmethod to prefetch
267
+ - Index frequently filtered fields
268
+ - Keep pagination enabled
269
+ - Limit slices (`queryset = queryset[:1000]`) for heavy searches
270
+
271
+ ---
272
+
273
+ ## 🤲 Contributing
274
+
275
+ 1. Fork
276
+ 2. Create branch
277
+ 3. Add tests
278
+ 4. Run lint (`ruff check .`)
279
+ 5. Open PR
280
+
281
+ ---
282
+
283
+ ## ⭐ Support
284
+
285
+ Star the repo or donate:
286
+ - [Buy me a coffee](https://buymeacoffee.com/caspel26)
287
+
288
+ ---
289
+
290
+ ## 📜 License
291
+
292
+ MIT License. See [LICENSE](LICENSE).
293
+
294
+ ---
295
+
296
+ ## 🔗 Quick Links
297
+
298
+ | Item | Link |
299
+ |------|------|
300
+ | PyPI | https://pypi.org/project/django-ninja-aio-crud/ |
301
+ | Docs | https://django-ninja-aio.com
302
+ | Issues | https://github.com/caspel26/django-ninja-aio-crud/issues |
303
+
304
+ ---
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "1.0.2"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -42,7 +42,7 @@ class NotFoundError(BaseException):
42
42
 
43
43
  def __init__(self, model: Model, details=None):
44
44
  super().__init__(
45
- error={model._meta.verbose_name: self.error},
45
+ error={model._meta.verbose_name.replace(" ", "_"): self.error},
46
46
  status_code=self.status_code,
47
47
  details=details,
48
48
  )
@@ -0,0 +1,3 @@
1
+ from .api import ManyToManyAPI
2
+
3
+ __all__ = ["ManyToManyAPI"]