django-ninja-aio-crud 2.1.0__py3-none-any.whl → 2.2.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.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
- Requires-Python: >=3.10
6
+ Requires-Python: >=3.10, <=3.14
7
7
  Description-Content-Type: text/markdown
8
8
  Classifier: Operating System :: OS Independent
9
9
  Classifier: Topic :: Internet
@@ -1,12 +1,12 @@
1
- ninja_aio/__init__.py,sha256=YGWehZQ4d5yVFCd1ax-s61BSkND60Yh383YmUJq-LtY,119
2
- ninja_aio/api.py,sha256=SS1TYUiFkdYjfJLVy6GI90GOzvIHzPEeL-UcqWFRHkM,1684
3
- ninja_aio/auth.py,sha256=w9TXDQwJSZzCbncsSwemINTUe1IPZGHnLKiqWTeOoFA,9163
1
+ ninja_aio/__init__.py,sha256=Jgj89rpJ3n4pkGfoSYm9NYrwBjGzwhiOeU5c6mr-J8Q,119
2
+ ninja_aio/api.py,sha256=tuC7vdvn7s1GkCnSFy9Kn1zv0glZfYptRQVvo8ZRtGQ,2429
3
+ ninja_aio/auth.py,sha256=4sWdFPjKiQgUL1d_CSGDblVjnY5ptP6LQha6XXdluJA,9157
4
4
  ninja_aio/decorators.py,sha256=BHoFIiqdIVMFqSxGh-F6WeZFo1xZK4ieDw3dzKfxZIM,8147
5
5
  ninja_aio/exceptions.py,sha256=1-iRbrloIyi0CR6Tcrn5YR4_LloA7PPohKIBaxXJ0-8,2596
6
6
  ninja_aio/models.py,sha256=aJlo5a64O4o-fB8QESLMUJpoA5kcjRJxPBiAIMxg46k,47652
7
7
  ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
8
8
  ninja_aio/renders.py,sha256=VtmSliRJyZ6gjyoib8AXMVUBYF1jPNsiceCHujI_mAs,1699
9
- ninja_aio/types.py,sha256=_QV0DySZhCV1otuh1zhrlHgCaFnr5fd64GQrcA-qKoc,564
9
+ ninja_aio/types.py,sha256=nFqWEopm7eoEaHRzbi6EyA9WZ5Cneyd602ilFKypeQI,577
10
10
  ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  ninja_aio/helpers/api.py,sha256=BTe7OL-X7YgWYeXmka8TmN4-gA43FVZhtH7q0dRjYX0,20238
12
12
  ninja_aio/helpers/query.py,sha256=tE8RjXvSig-WB_0LRQ0LqoE4G_HMHsu0Na5QzTNIm6U,4262
@@ -15,9 +15,9 @@ ninja_aio/schemas/api.py,sha256=-VwXhBRhmMsZLIAmWJ-P7tB5klxXS75eukjabeKKYsc,360
15
15
  ninja_aio/schemas/generics.py,sha256=frjJsKJMAdM_NdNKv-9ddZNGxYy5PNzjIRGtuycgr-w,112
16
16
  ninja_aio/schemas/helpers.py,sha256=rmE0D15lJg95Unv8PU44Hbf0VDTcErMCZZFG3D_znTo,2823
17
17
  ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
18
- ninja_aio/views/api.py,sha256=ispbr5w6WCUdbXmn2LY94zDQ0qb2nDNeTLLNAUBFoDc,17736
18
+ ninja_aio/views/api.py,sha256=O_QQBxk-MltU-bPjxOSu5fFpHaDNP5J3Wk6E5rSBgi4,19744
19
19
  ninja_aio/views/mixins.py,sha256=Jh6BG8Cs823nurVlODlzCquTxKrLH7Pmo5udPqUGZek,11378
20
- django_ninja_aio_crud-2.1.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
21
- django_ninja_aio_crud-2.1.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
22
- django_ninja_aio_crud-2.1.0.dist-info/METADATA,sha256=91kIanSvlN_aeRmhZuXZR2pIBXEzUUOuYLJkijzN7QI,8672
23
- django_ninja_aio_crud-2.1.0.dist-info/RECORD,,
20
+ django_ninja_aio_crud-2.2.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
21
+ django_ninja_aio_crud-2.2.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
22
+ django_ninja_aio_crud-2.2.0.dist-info/METADATA,sha256=lJiJIHnNMaKXAM-aOwuKlcAmaJt8oV_wtiDGSFvjjJE,8680
23
+ django_ninja_aio_crud-2.2.0.dist-info/RECORD,,
ninja_aio/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.1.0"
3
+ __version__ = "2.2.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
ninja_aio/api.py CHANGED
@@ -5,10 +5,13 @@ from ninja.throttling import BaseThrottle
5
5
  from ninja import NinjaAPI
6
6
  from ninja.openapi.docs import DocsBase, Swagger
7
7
  from ninja.constants import NOT_SET, NOT_SET_TYPE
8
+ from django.db import models
8
9
 
9
10
  from .parsers import ORJSONParser
10
11
  from .renders import ORJSONRenderer
11
12
  from .exceptions import set_api_exception_handlers
13
+ from .views import APIView, APIViewSet
14
+ from .models import ModelSerializer
12
15
 
13
16
 
14
17
  class NinjaAIO(NinjaAPI):
@@ -49,3 +52,24 @@ class NinjaAIO(NinjaAPI):
49
52
  def set_default_exception_handlers(self):
50
53
  set_api_exception_handlers(self)
51
54
  super().set_default_exception_handlers()
55
+
56
+ def view(self, prefix: str, tags: list[str] = None) -> Any:
57
+ def wrapper(view: type[APIView]):
58
+ instance = view(api=self, prefix=prefix, tags=tags)
59
+ instance.add_views_to_route()
60
+ return instance
61
+
62
+ return wrapper
63
+
64
+ def viewset(
65
+ self,
66
+ model: models.Model | ModelSerializer,
67
+ prefix: str = None,
68
+ tags: list[str] = None,
69
+ ) -> None:
70
+ def wrapper(viewset: type[APIViewSet]):
71
+ instance = viewset(api=self, model=model, prefix=prefix, tags=tags)
72
+ instance.add_views_to_route()
73
+ return instance
74
+
75
+ return wrapper
ninja_aio/auth.py CHANGED
@@ -124,7 +124,7 @@ def validate_key(key: Optional[JwtKeys], setting_name: str) -> JwtKeys:
124
124
  key = getattr(settings, setting_name, None)
125
125
  if key is None:
126
126
  raise ValueError(f"{setting_name} is required")
127
- if not isinstance(key, (jwk.RSAKey, jwk.ECKey)):
127
+ if not isinstance(key, (jwk.RSAKey, jwk.ECKey, jwk.OctKey)):
128
128
  raise ValueError(
129
129
  f"{setting_name} must be an instance of jwk.RSAKey or jwk.ECKey"
130
130
  )
@@ -143,7 +143,7 @@ def validate_mandatory_claims(claims: dict) -> dict:
143
143
 
144
144
 
145
145
  def encode_jwt(
146
- claims: dict, duration: int, private_key: jwk.RSAKey = None, algorithm: str = None
146
+ claims: dict, duration: int, private_key: JwtKeys = None, algorithm: str = None
147
147
  ) -> str:
148
148
  """
149
149
  Encode and sign a JWT.
@@ -192,7 +192,7 @@ def encode_jwt(
192
192
 
193
193
  def decode_jwt(
194
194
  token: str,
195
- public_key: jwk.RSAKey | jwk.ECKey = None,
195
+ public_key: JwtKeys = None,
196
196
  algorithms: list[str] = None,
197
197
  ) -> jwt.Token:
198
198
  """
ninja_aio/types.py CHANGED
@@ -8,7 +8,7 @@ S_TYPES = Literal["read", "create", "update"]
8
8
  F_TYPES = Literal["fields", "customs", "optionals", "excludes"]
9
9
  SCHEMA_TYPES = Literal["In", "Out", "Patch", "Related"]
10
10
  VIEW_TYPES = Literal["list", "retrieve", "create", "update", "delete", "all"]
11
- JwtKeys: TypeAlias = jwk.RSAKey | jwk.ECKey
11
+ JwtKeys: TypeAlias = jwk.RSAKey | jwk.ECKey | jwk.OctKey
12
12
 
13
13
  class ModelSerializerType(type):
14
14
  def __repr__(self):
ninja_aio/views/api.py CHANGED
@@ -18,19 +18,16 @@ from ninja_aio.helpers.api import ManyToManyAPI
18
18
  from ninja_aio.types import ModelSerializerMeta, VIEW_TYPES
19
19
  from ninja_aio.decorators import unique_view, decorate_view
20
20
 
21
- ERROR_CODES = frozenset({400, 401, 404, 428})
21
+ ERROR_CODES = frozenset({400, 401, 404})
22
22
 
23
23
 
24
- class APIView:
25
- api: NinjaAPI
26
- router_tag: str
27
- api_route_path: str
24
+ class API:
25
+ api: NinjaAPI = None
26
+ router_tag: str = ""
27
+ router_tags: list[str] = []
28
+ api_route_path: str = ""
28
29
  auth: list | None = NOT_SET
29
30
 
30
- def __init__(self) -> None:
31
- self.router = Router(tags=[self.router_tag])
32
- self.error_codes = ERROR_CODES
33
-
34
31
  def views(self):
35
32
  """
36
33
  Override this method to add your custom views. For example:
@@ -38,7 +35,7 @@ class APIView:
38
35
  async def some_method(request, *args, **kwargs):
39
36
  pass
40
37
 
41
- You can add multilple views just doing:
38
+ You can add views just doing:
42
39
 
43
40
  @self.router.get(some_path, response=some_schema)
44
41
  async def some_method(request, *args, **kwargs):
@@ -63,20 +60,80 @@ class APIView:
63
60
  async def some_method(request, *args, **kwargs):
64
61
  pass
65
62
  """
63
+ pass
66
64
 
67
65
  def _add_views(self):
68
- self.views()
69
- return self.router
66
+ raise NotImplementedError("_add_views must be implemented in subclasses")
70
67
 
71
68
  def add_views_to_route(self):
72
69
  return self.api.add_router(f"{self.api_route_path}", self._add_views())
73
70
 
74
71
 
75
- class APIViewSet:
72
+ class APIView(API):
73
+ """
74
+ Base class to register custom, non-CRUD endpoints on a Ninja Router.
75
+
76
+ Usage:
77
+ @api.view(prefix="/custom", tags=["Custom"])
78
+ class CustomAPIView(APIView):
79
+ def views(self):
80
+ @self.router.get("/hello", response=SomeSchema)
81
+ async def hello(request):
82
+ return SomeSchema(...)
83
+
84
+ or
85
+
86
+ class CustomAPIView(APIView):
87
+ api = api
88
+ api_route_path = "/custom"
89
+ router_tags = ["Custom"]
90
+
91
+ def views(self):
92
+ @self.router.get("/hello", response=SomeSchema)
93
+ async def hello(request):
94
+ return SomeSchema(...)
95
+
96
+
97
+ CustomAPIView().add_views_to_route()
98
+
99
+ Attributes:
100
+ api: NinjaAPI instance used to mount the router.
101
+ router_tag: Single tag used if router_tags is not provided.
102
+ router_tags: List of tags assigned to the router.
103
+ api_route_path: Base path where the router is mounted.
104
+ auth: Default auth list or NOT_SET for unauthenticated endpoints.
105
+ router: Router instance where views are registered.
106
+ error_codes: Common error codes returned by endpoints.
107
+
108
+ Overridable methods:
109
+ views(): Register your endpoints using self.router.get/post/patch/delete.
110
+ """
111
+
112
+ def __init__(
113
+ self, api: NinjaAPI = None, prefix: str = None, tags: list[str] = None
114
+ ) -> None:
115
+ self.api = api or self.api
116
+ self.api_route_path = prefix or self.api_route_path
117
+ self.router_tags = tags or self.router_tags or [self.router_tag]
118
+ self.router = Router(tags=self.router_tags)
119
+ self.error_codes = ERROR_CODES
120
+
121
+ def _add_views(self):
122
+ self.views()
123
+ return self.router
124
+
125
+
126
+ class APIViewSet(API):
76
127
  """
77
128
  Base viewset generating async CRUD + optional M2M endpoints for a Django model.
78
129
 
79
130
  Usage:
131
+ @api.viewset(model=MyModel)
132
+ class MyModelViewSet(APIViewSet):
133
+ pass
134
+
135
+ or
136
+
80
137
  class MyModelViewSet(APIViewSet):
81
138
  model = MyModel
82
139
  api = api
@@ -109,8 +166,8 @@ class APIViewSet:
109
166
  dict, and must return the (optionally) filtered queryset.
110
167
 
111
168
  Example:
169
+ @api.viewset(model=models.User)
112
170
  class UserViewSet(APIViewSet):
113
- model = models.User
114
171
  m2m_relations = [
115
172
  M2MRelationSchema(
116
173
  model=models.Tag,
@@ -149,7 +206,7 @@ class APIViewSet:
149
206
  <related_name>_query_params_handler(queryset, filters): Async hook for per-M2M filtering.
150
207
 
151
208
  Error responses:
152
- All endpoints may return GenericMessageSchema for codes in ERROR_CODES (400,401,404,428).
209
+ All endpoints may return GenericMessageSchema for codes in ERROR_CODES (400,401,404).
153
210
 
154
211
  Internal:
155
212
  Dynamic path/filter schemas built with pydantic.create_model.
@@ -157,12 +214,9 @@ class APIViewSet:
157
214
  """
158
215
 
159
216
  model: ModelSerializer | Model
160
- api: NinjaAPI
161
- router_tag: str = ""
162
217
  schema_in: Schema | None = None
163
218
  schema_out: Schema | None = None
164
219
  schema_update: Schema | None = None
165
- auth: list | None = NOT_SET
166
220
  get_auth: list | None = NOT_SET
167
221
  post_auth: list | None = NOT_SET
168
222
  patch_auth: list | None = NOT_SET
@@ -170,7 +224,6 @@ class APIViewSet:
170
224
  pagination_class: type[AsyncPaginationBase] = PageNumberPagination
171
225
  query_params: dict[str, tuple[type, ...]] = {}
172
226
  disable: list[type[VIEW_TYPES]] = []
173
- api_route_path: str = ""
174
227
  list_docs = "List all objects."
175
228
  create_docs = "Create a new object."
176
229
  retrieve_docs = "Retrieve a specific object by its primary key."
@@ -180,8 +233,16 @@ class APIViewSet:
180
233
  m2m_auth: list | None = NOT_SET
181
234
  extra_decorators: DecoratorsSchema = DecoratorsSchema()
182
235
 
183
- def __init__(self) -> None:
236
+ def __init__(
237
+ self,
238
+ api: NinjaAPI = None,
239
+ model: Model | ModelSerializer = None,
240
+ prefix: str = None,
241
+ tags: list[str] = None,
242
+ ) -> None:
243
+ self.api = api or self.api
184
244
  self.error_codes = ERROR_CODES
245
+ self.model = model or self.model
185
246
  self.model_util = (
186
247
  ModelUtil(self.model)
187
248
  if not isinstance(self.model, ModelSerializerMeta)
@@ -191,16 +252,17 @@ class APIViewSet:
191
252
  self.path_schema = self._generate_path_schema()
192
253
  self.filters_schema = self._generate_filters_schema()
193
254
  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
- )
197
- self.router = Router(tags=[self.router_tag])
255
+ self.router_tag = self.router_tag or self.model_verbose_name
256
+ self.router_tags = self.router_tags or tags or [self.router_tag]
257
+ self.router = Router(tags=self.router_tags)
198
258
  self.path = "/"
199
259
  self.get_path = ""
200
260
  self.path_retrieve = f"{{{self.model_util.model_pk_name}}}/"
201
261
  self.get_path_retrieve = f"{{{self.model_util.model_pk_name}}}"
202
262
  self.api_route_path = (
203
- self.api_route_path or self.model_util.verbose_name_path_resolver()
263
+ self.api_route_path
264
+ or prefix
265
+ or self.model_util.verbose_name_path_resolver()
204
266
  )
205
267
  self.m2m_api = (
206
268
  None
@@ -450,18 +512,18 @@ class APIViewSet:
450
512
 
451
513
  return self._set_additional_views()
452
514
 
453
- def add_views_to_route(self):
454
- """
455
- Attach router with registered endpoints to the NinjaAPI instance.
456
- """
457
- return self.api.add_router(f"{self.api_route_path}", self._add_views())
458
-
459
515
 
460
516
  class ReadOnlyViewSet(APIViewSet):
461
517
  """
462
518
  ReadOnly viewset generating async List + Retrieve endpoints for a Django model.
463
519
 
464
520
  Usage:
521
+ @api.viewset(model=MyModel)
522
+ class MyModelReadOnlyViewSet(ReadOnlyViewSet):
523
+ pass
524
+
525
+ or
526
+
465
527
  class MyModelReadOnlyViewSet(ReadOnlyViewSet):
466
528
  model = MyModel
467
529
  api = api
@@ -476,10 +538,15 @@ class WriteOnlyViewSet(APIViewSet):
476
538
  WriteOnly viewset generating async Create + Update + Delete endpoints for a Django model.
477
539
 
478
540
  Usage:
541
+ @api.viewset(model=MyModel)
542
+ class MyModelWriteOnlyViewSet(WriteOnlyViewSet):
543
+ pass
544
+
545
+ or
546
+
479
547
  class MyModelWriteOnlyViewSet(WriteOnlyViewSet):
480
548
  model = MyModel
481
549
  api = api
482
- MyModelWriteOnlyViewSet().add_views_to_route()
483
550
  """
484
551
 
485
552
  disable = ["list", "retrieve"]