django-ninja-aio-crud 0.1.2__py3-none-any.whl → 0.1.4__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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-ninja-aio-crud
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10
@@ -34,24 +34,32 @@ Project-URL: Repository, https://github.com/caspel26/django-ninja-aio-crud
34
34
  ## 📝 Instructions
35
35
 
36
36
  ### 📚 Prerequisites
37
+
37
38
  - Install Python from the [official website](https://www.python.org/) (latest version) and ensure it is added to the system Path and environment variables.
38
39
 
39
40
  ### 💻 Setup your environment
41
+
40
42
  - Create a virtual environment
41
43
  ```bash
42
44
  python -m venv .venv
43
45
  ```
46
+
44
47
  ### ✅ Activate it
48
+
45
49
  - If you are from linux activate it with
50
+
46
51
  ```bash
47
52
  . .venv/bin/activate
48
53
  ```
54
+
49
55
  - If you are from windows activate it with
56
+
50
57
  ```bash
51
58
  . .venv/Scripts/activate
52
59
  ```
53
60
 
54
61
  ### 📥 Install package
62
+
55
63
  ```bash
56
64
  pip install django-ninja-aio-crud
57
65
  ```
@@ -63,14 +71,17 @@ pip install django-ninja-aio-crud
63
71
  > and why not ... [Buy me a coffee](https://buymeacoffee.com/caspel26)
64
72
 
65
73
  ### ModelSerializer
74
+
66
75
  - You can serialize your models using ModelSerializer and made them inherit from it. In your models.py import ModelSerializer
67
76
  ```Python
77
+ # models.py
78
+ from django.db import models
68
79
  from ninja_aio.models import ModelSerializer
69
80
 
70
81
 
71
82
  class Foo(ModelSerializer):
72
- name = mdoels.CharField()
73
- bar = models.CharField()
83
+ name = models.CharField(max_length=30)
84
+ bar = models.CharField(max_length=30)
74
85
 
75
86
  class ReadSerializer:
76
87
  fields = ["id", "name", "bar"]
@@ -81,14 +92,18 @@ class Foo(ModelSerializer):
81
92
  class UpdateSerializer:
82
93
  fields = ["name", "bar"]
83
94
  ```
95
+
84
96
  - ReadSerializer, CreateSerializer, UpdateSerializer are used to define which fields would be included in runtime schemas creation. You can also specify custom fields and handle their function by overriding custom_actions ModelSerializer's method(custom fields are only available for Create and Update serializers).
97
+
85
98
  ```Python
99
+ # models.py
100
+ from django.db import models
86
101
  from ninja_aio.models import ModelSerializer
87
102
 
88
103
 
89
104
  class Foo(ModelSerializer):
90
- name = mdoels.CharField()
91
- bar = models.CharField()
105
+ name = models.CharField(max_length=30)
106
+ bar = models.CharField(max_length=30)
92
107
  active = models.BooleanField(default=False)
93
108
 
94
109
  class ReadSerializer:
@@ -112,12 +127,15 @@ class Foo(ModelSerializer):
112
127
  self.active = True
113
128
  await self.asave()
114
129
  ```
115
- - post create method is a custom method that comes out to handle actions which will be excuted after that the object is created. It can be used, indeed, for example to handle custom fields' actions.
116
130
 
131
+ - post create method is a custom method that comes out to handle actions which will be excuted after that the object is created. It can be used, indeed, for example to handle custom fields' actions.
117
132
 
118
133
  ### APIViewSet
134
+
119
135
  - View class used to automatically generate CRUD views. in your views.py import APIViewSet and define your api using NinjaAPI class. As Parser and Render of the API you must use ninja_aio built-in classes which will serialize data using orjson.
136
+
120
137
  ```Python
138
+ # views.py
121
139
  from ninja import NinjAPI
122
140
  from ninja_aio.views import APIViewSet
123
141
  from ninja_aio.parsers import ORJSONParser
@@ -135,8 +153,11 @@ class FooAPI(APIViewSet):
135
153
 
136
154
  FooAPI().add_views_to_route()
137
155
  ```
156
+
138
157
  - and that's it, your model CRUD will be automatically created. You can also add custom views to CRUD overriding the built-in method "views".
158
+
139
159
  ```Python
160
+ # views.py
140
161
  from ninja import NinjAPI, Schema
141
162
  from ninja_aio.views import APIViewSet
142
163
  from ninja_aio.parsers import ORJSONParser
@@ -161,7 +182,7 @@ class FooAPI(APIViewSet):
161
182
  api = api
162
183
 
163
184
  def views(self):
164
- @self.router.post("numbers-sum/", response={200: ExampleSchemaOut)
185
+ @self.router.post("numbers-sum/", response={200: ExampleSchemaOut})
165
186
  async def sum(request: HttpRequest, data: ExampleSchemaIn):
166
187
  return 200, {sum: data.n1 + data.n2}
167
188
 
@@ -170,8 +191,11 @@ FooAPI().add_views_to_route()
170
191
  ```
171
192
 
172
193
  ### APIView
194
+
173
195
  - View class to code generic views class based. In your views.py import APIView class.
196
+
174
197
  ```Python
198
+ # views.py
175
199
  from ninja import NinjAPI, Schema
176
200
  from ninja_aio.views import APIView
177
201
  from ninja_aio.parsers import ORJSONParser
@@ -195,7 +219,7 @@ class SumView(APIView):
195
219
  router_tag = "Sum"
196
220
 
197
221
  def views(self):
198
- @self.router.post(self.api_router_path, response={200: ExampleSchemaOut)
222
+ @self.router.post("/", response={200: ExampleSchemaOut})
199
223
  async def sum(request: HttpRequest, data: ExampleSchemaIn):
200
224
  return 200, {sum: data.n1 + data.n2}
201
225
 
@@ -203,9 +227,91 @@ class SumView(APIView):
203
227
  SumView().add_views_to_route()
204
228
  ```
205
229
 
230
+ ### Relations
231
+ - You can also set ForeignKey and OneToOne relations into serialization(reverse relations are supported too). Django ninja aio crud will serialize every of these relation automatically.
232
+
233
+ > [!WARNING]
234
+ > Only ForeignKey and OneToOne relations are supported for serialization, ManyToMany relations are not supported yet.
235
+
236
+ - Define models:
237
+
238
+ ```Python
239
+ # models.py
240
+ class Bar(ModelSerializer):
241
+ name = models.CharField(max_length=30)
242
+ description = models.TextField(max_length=30)
243
+
244
+ # ReadSerializer with reverse OneToMany relation (foos)
245
+ class ReadSerializer:
246
+ fields = ["id", "name", "description", "foos"]
247
+
248
+ class CreateSerializer:
249
+ fields = ["name", "description"]
250
+
251
+ class UpdateSerializer:
252
+ fields = ["name", "description"]
253
+
254
+
255
+ class Foo(ModelSerializer):
256
+ name = models.CharField(max_length=30)
257
+ bar = models.ForeignKey(Bar, on_delete=models.CASCADE, related_name="foos")
258
+
259
+ class ReadSerializer:
260
+ fields = ["id", "name", "bar"]
261
+
262
+ class CreateSerializer:
263
+ fields = ["name", "bar"]
264
+
265
+ class UpdateSerializer:
266
+ fields = ["name"]
267
+ ```
268
+
269
+ - Define views:
270
+
271
+ ```Python
272
+ # views.py
273
+ from ninja import NinjAPI
274
+ from ninja_aio.views import APIViewSet
275
+ from ninja_aio.parsers import ORJSONParser
276
+ from ninja_aio.renders import ORJSONRender
277
+
278
+ from .models import Foo, Bar
279
+
280
+ api = NinjaAPI(renderer=ORJSONRenderer(), parser=ORJSONParser())
281
+
282
+
283
+ class FooAPI(APIViewSet):
284
+ model = Foo
285
+ api = api
286
+
287
+
288
+ class BarAPI(APIViewSet):
289
+ model = Bar
290
+ api = api
291
+
292
+
293
+ FooAPI().add_views_to_route()
294
+ BarAPI().add_views_to_route()
295
+ ```
296
+
297
+ - Now run your server and go to /docs url:
298
+
299
+ ### Docs
300
+
301
+ - Foo Schemas
302
+
303
+ ![Swagger UI](docs/images/foo-swagger.png)
304
+
305
+ - Bar Schemas with reverse relation
306
+
307
+ ![Swagger UI](docs/images/bar-swagger.png)
308
+
206
309
  ## 🔒 Authentication
310
+
207
311
  ### Jwt
312
+
208
313
  - AsyncJWTBearer built-in class is an authenticator class which use joserfc module. It cames out with authenticate method which validate given claims. Override auth handler method to write your own authentication method. Default algorithms used is RS256. a jwt Token istance is set as class atribute so you can use it by self.dcd.
314
+
209
315
  ```Python
210
316
  from ninja_aio.auth import AsyncJWTBearer
211
317
  from django.conf import settings
@@ -225,8 +331,11 @@ class CustomJWTBearer(AsyncJWTBearer):
225
331
  return None
226
332
  return request.user
227
333
  ```
334
+
228
335
  - Then add it to views.
336
+
229
337
  ```Python
338
+ # views.py
230
339
  from ninja import NinjAPI, Schema
231
340
  from ninja_aio.views import APIViewSet, APIView
232
341
  from ninja_aio.parsers import ORJSONParser
@@ -259,7 +368,7 @@ class SumView(APIView):
259
368
  auths = CustomJWTBearer()
260
369
 
261
370
  def views(self):
262
- @self.router.post(self.api_router_path, response={200: ExampleSchemaOut}, auth=self.auths)
371
+ @self.router.post("/", response={200: ExampleSchemaOut}, auth=self.auths)
263
372
  async def sum(request: HttpRequest, data: ExampleSchemaIn):
264
373
  return 200, {sum: data.n1 + data.n2}
265
374
 
@@ -268,6 +377,20 @@ FooAPI().add_views_to_route()
268
377
  SumView().add_views_to_route()
269
378
  ```
270
379
 
380
+ ## 📝 Pagination
381
+
382
+ - By default APIViewSet list view uses Django Ninja built-in AsyncPagination class "PageNumberPagination". You can customize and assign it to APIViewSet class. To make your custom pagination consult **<a href="https://django-ninja.dev/guides/response/pagination/#async-pagination">Django Ninja pagination documentation</a>**.
383
+
384
+ ```Python
385
+ # views.py
386
+
387
+ class FooAPI(APIViewSet):
388
+ model = Foo
389
+ api = api
390
+ pagination_class = CustomPaginationClass
391
+
392
+ ```
393
+
271
394
  ## 📌 Notes
272
395
  - Feel free to contribute and improve the program. 🛠️
273
396
 
@@ -0,0 +1,11 @@
1
+ ninja_aio/__init__.py,sha256=ezfuNggZR9pW5CRIDO17RBPn9EANhL9Rz5SZVc2_7OM,70
2
+ ninja_aio/auth.py,sha256=hGgiblvffpHmmakjaxdNT3G0tq39-Bvc5oLNqjn4qd8,1300
3
+ ninja_aio/exceptions.py,sha256=PPNr1CdC7M7Kx1MtiBGuR4hATYQqEuvxCRU6uSG7rgM,543
4
+ ninja_aio/models.py,sha256=m_agSUXIEbt_qOHQhc1G2t-W5-jTofquZDpiBf2y8so,10293
5
+ ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
6
+ ninja_aio/renders.py,sha256=wLQisbIsMJlGYJMO4Ud-qiaVCvz4yxiZ4mYommixAeQ,1453
7
+ ninja_aio/schemas.py,sha256=EgRkfhnzZqwGvdBmqlZixMtMcoD1ZxV_qzJ3fmaAy20,113
8
+ ninja_aio/views.py,sha256=j72V0GHlTNdFDsO2f8ZMy_0aS7Hb4CKWN1t_st1ohDM,6319
9
+ django_ninja_aio_crud-0.1.4.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
10
+ django_ninja_aio_crud-0.1.4.dist-info/METADATA,sha256=ejUKHqdUYxzqDHQOidGtEnOCe2shkLnLCH2ok7t9hac,10440
11
+ django_ninja_aio_crud-0.1.4.dist-info/RECORD,,
ninja_aio/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """ Django Ninja AIO CRUD - Rest Framework """
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.4"
ninja_aio/models.py CHANGED
@@ -87,20 +87,24 @@ class ModelSerializer(models.Model):
87
87
  for f in cls.ReadSerializer.fields:
88
88
  field_obj = getattr(cls, f)
89
89
  if isinstance(field_obj, ReverseManyToOneDescriptor):
90
- reverse_rels.append(field_obj.field.__dict__.get("_related_name"))
90
+ reverse_rels.append(field_obj.field._related_name)
91
91
  if isinstance(field_obj, ReverseOneToOneDescriptor):
92
- reverse_rels.append(
93
- list(field_obj.__dict__.values())[0].__dict__.get("related_name")
94
- )
92
+ reverse_rels.append(list(field_obj[0].related_name))
95
93
  return reverse_rels
96
94
 
97
95
  @classmethod
98
96
  def get_reverse_relation_schema(
99
97
  cls, obj: type["ModelSerializer"], rel_type: type[REL_TYPES], field: str
100
98
  ):
101
- for index, rel_f in enumerate(obj.ReadSerializer.fields):
102
- if rel_f == cls._meta.model_name:
103
- obj.ReadSerializer.fields.pop(index)
99
+ cls_f = []
100
+ for rel_f in obj.ReadSerializer.fields:
101
+ rel_f_obj = getattr(obj, rel_f).field
102
+ if (
103
+ isinstance(rel_f_obj, (models.ForeignKey, models.OneToOneField))
104
+ and rel_f_obj.related_model == cls
105
+ ):
106
+ cls_f.append(rel_f)
107
+ obj.ReadSerializer.fields.remove(rel_f)
104
108
  break
105
109
  rel_schema = obj.generate_read_s(depth=0)
106
110
  if rel_type == "many":
@@ -110,7 +114,8 @@ class ModelSerializer(models.Model):
110
114
  rel_schema | None,
111
115
  None,
112
116
  )
113
- obj.ReadSerializer.fields.append(cls._meta.model_name)
117
+ if len(cls_f) > 0:
118
+ obj.ReadSerializer.fields.append(*cls_f)
114
119
  return rel_data
115
120
 
116
121
  @classmethod
@@ -120,14 +125,12 @@ class ModelSerializer(models.Model):
120
125
  for f in cls.ReadSerializer.fields:
121
126
  field_obj = getattr(cls, f)
122
127
  if isinstance(field_obj, ReverseManyToOneDescriptor):
123
- rel_obj: ModelSerializer = field_obj.field.__dict__.get("model")
128
+ rel_obj: ModelSerializer = field_obj.field.model
124
129
  rel_data = cls.get_reverse_relation_schema(rel_obj, "many", f)
125
130
  reverse_rels.append(rel_data)
126
131
  continue
127
132
  if isinstance(field_obj, ReverseOneToOneDescriptor):
128
- rel_obj: ModelSerializer = list(field_obj.__dict__.values())[
129
- 0
130
- ].__dict__.get("related_model")
133
+ rel_obj: ModelSerializer = list(field_obj)[0].related_model
131
134
  rel_data = cls.get_reverse_relation_schema(rel_obj, "one", f)
132
135
  reverse_rels.append(rel_data)
133
136
  continue
@@ -149,9 +152,10 @@ class ModelSerializer(models.Model):
149
152
  continue
150
153
  field_obj = getattr(cls, k).field
151
154
  if isinstance(field_obj, models.BinaryField):
152
- if not v.endswith(b"=="):
153
- v = v + b"=="
154
- payload |= {k: base64.b64decode(v)}
155
+ try:
156
+ payload |= {k: base64.b64decode(v)}
157
+ except Exception as exc:
158
+ raise SerializeError({k: ". ".join(exc.args)}, 400)
155
159
  if isinstance(field_obj, models.ForeignKey):
156
160
  try:
157
161
  rel: ModelSerializer = await field_obj.related_model.get_object(
@@ -204,18 +208,6 @@ class ModelSerializer(models.Model):
204
208
  return None
205
209
  return customs
206
210
 
207
- @classmethod
208
- def get_optional_fields(cls, s_type: type[S_TYPES]) -> list[str] | None:
209
- try:
210
- match s_type:
211
- case "create":
212
- optionals = cls.CreateSerializer.optionals
213
- case "update":
214
- optionals = cls.UpdateSerializer.optionals
215
- except AttributeError:
216
- return None
217
- return optionals
218
-
219
211
  @classmethod
220
212
  def generate_read_s(cls, depth: int = 1) -> Schema:
221
213
  fields, reverse_rels = cls.get_schema_out_data()
@@ -234,7 +226,6 @@ class ModelSerializer(models.Model):
234
226
  model=cls,
235
227
  name=f"{cls._meta.model_name}SchemaIn",
236
228
  fields=cls.CreateSerializer.fields,
237
- optional_fields=cls.get_optional_fields("create"),
238
229
  custom_fields=cls.get_custom_fields("create"),
239
230
  )
240
231
 
@@ -244,7 +235,6 @@ class ModelSerializer(models.Model):
244
235
  model=cls,
245
236
  name=f"{cls._meta.model_name}SchemaPatch",
246
237
  fields=cls.UpdateSerializer.fields,
247
- optional_fields=cls.get_optional_fields("update"),
248
238
  custom_fields=cls.get_custom_fields("update"),
249
239
  )
250
240
 
ninja_aio/renders.py CHANGED
@@ -8,9 +8,11 @@ from ninja.renderers import BaseRenderer
8
8
  class ORJSONRenderer(BaseRenderer):
9
9
  media_type = "application/json"
10
10
 
11
- def render(self, request: HttpRequest, data: dict | list, *, response_status):
12
- if isinstance(data, list):
13
- return orjson.dumps(self.render_list(data))
11
+ def render(self, request: HttpRequest, data: dict, *, response_status):
12
+ old_d = data
13
+ for k, v in old_d.items():
14
+ if isinstance(v, list):
15
+ data |= {k: self.render_list(v)}
14
16
  return orjson.dumps(self.render_dict(data))
15
17
 
16
18
  @classmethod
@@ -33,11 +35,9 @@ class ORJSONRenderer(BaseRenderer):
33
35
  v |= {k_rel: base64.b64encode(v_rel).decode()}
34
36
  data |= {k: v}
35
37
  if isinstance(v, list):
36
- index_rel = 0
37
- for f_rel in v:
38
+ for index_rel, f_rel in enumerate(v):
38
39
  for k_rel, v_rel in f_rel.items():
39
40
  if isinstance(v_rel, bytes):
40
41
  v[index_rel] |= {k_rel: base64.b64encode(v_rel).decode()}
41
- index_rel += 1
42
42
  data |= {k: v}
43
43
  return data
ninja_aio/views.py CHANGED
@@ -2,6 +2,7 @@ from typing import List
2
2
 
3
3
  from ninja import NinjaAPI, Router
4
4
  from ninja.constants import NOT_SET
5
+ from ninja.pagination import paginate, AsyncPaginationBase, PageNumberPagination
5
6
  from django.http import HttpRequest
6
7
 
7
8
  from .models import ModelSerializer
@@ -67,6 +68,7 @@ class APIViewSet:
67
68
  model: ModelSerializer
68
69
  api: NinjaAPI
69
70
  auths: list | None = NOT_SET
71
+ pagination_class: type[AsyncPaginationBase] = PageNumberPagination
70
72
 
71
73
  def __init__(self) -> None:
72
74
  self.router = Router(tags=[self.model._meta.model_name.capitalize()])
@@ -97,6 +99,7 @@ class APIViewSet:
97
99
  self.error_codes: GenericMessageSchema,
98
100
  },
99
101
  )
102
+ @paginate(self.pagination_class)
100
103
  async def list(request: HttpRequest):
101
104
  qs = await self.model.queryset_request(request)
102
105
  rels = self.model.get_reverse_relations()
@@ -1,11 +0,0 @@
1
- ninja_aio/__init__.py,sha256=w9rOYq7MufeqhiFOquIoeO1V-eGDrD1pyvwkDaCqtBQ,70
2
- ninja_aio/auth.py,sha256=hGgiblvffpHmmakjaxdNT3G0tq39-Bvc5oLNqjn4qd8,1300
3
- ninja_aio/exceptions.py,sha256=PPNr1CdC7M7Kx1MtiBGuR4hATYQqEuvxCRU6uSG7rgM,543
4
- ninja_aio/models.py,sha256=G6AOtGNtaiQNZ1nG6rxv-xNRzZmSgbrNYwQjXmNeFmo,10710
5
- ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
6
- ninja_aio/renders.py,sha256=uADkEyHQr4yet2h9j89Zjr9AbIElWCiLVk5HoqLSm10,1453
7
- ninja_aio/schemas.py,sha256=EgRkfhnzZqwGvdBmqlZixMtMcoD1ZxV_qzJ3fmaAy20,113
8
- ninja_aio/views.py,sha256=pEtc0ovXvymfKJRtthd8cAps2RetTr9gzn-Xmxs763o,6126
9
- django_ninja_aio_crud-0.1.2.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
10
- django_ninja_aio_crud-0.1.2.dist-info/METADATA,sha256=lztVo5K7MznYges2u6UZczydkOEek5zNjKkSyZ9gc64,8031
11
- django_ninja_aio_crud-0.1.2.dist-info/RECORD,,