django-ninja-aio-crud 0.1.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.
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.1
2
+ Name: django-ninja-aio-crud
3
+ Version: 0.1.0
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
+ Requires-Dist: django-ninja >=1.3.0
26
+ Requires-Dist: joserfc >=1.0.0
27
+ Requires-Dist: orjson >= 3.10.7
28
+ Project-URL: Repository, https://github.com/caspel26/django-ninja-aio-crud
29
+
30
+ # 🥷 django-ninja-aio-crud
31
+ > [!NOTE]
32
+ > Django ninja aio crud framework is based on **<a href="https://django-ninja.dev/">Django Ninja framework</a>**. It comes out with built-in views and models which are able to make automatic async CRUD operations and codes views class based making the developers' life easier and the code cleaner.
33
+
34
+ ## 📝 Instructions
35
+
36
+ ### 📚 Prerequisites
37
+ - 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
+ ### 💻 Setup your environment
40
+ - Create a virtual environment
41
+ ```bash
42
+ python -m venv .venv
43
+ ```
44
+ ### ✅ Activate it
45
+ - If you are from linux activate it with
46
+ ```bash
47
+ . .venv/bin/activate
48
+ ```
49
+ - If you are from windows activate it with
50
+ ```bash
51
+ . .venv/Scripts/activate
52
+ ```
53
+
54
+ ### 📥 Install requirements
55
+ ```bash
56
+ pip install django-ninja-aio
57
+ ```
58
+
59
+ ## 🚀 Usage
60
+
61
+ > [!TIP]
62
+ > If you find **django ninja aio crud** useful, consider :star: this project
63
+ > and why not ... [Buy me a coffee](https://buymeacoffee.com/caspel26)
64
+
65
+ ### ModelSerializer
66
+ - You can serialize your models using ModelSerializer and made them inherit from it. In your models.py import ModelSerializer
67
+ ```Python
68
+ from ninja_aio.models import ModelSerializer
69
+
70
+
71
+ class Foo(ModelSerializer):
72
+ name = mdoels.CharField()
73
+ bar = models.CharField()
74
+
75
+ class ReadSerializer:
76
+ fields = ["id", "name", "bar"]
77
+
78
+ class CreateSerializer:
79
+ fields = ["name", "bar"]
80
+
81
+ class UpdateSerializer:
82
+ fields = ["name", "bar"]
83
+ ```
84
+ - 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).
85
+ ```Python
86
+ from ninja_aio.models import ModelSerializer
87
+
88
+
89
+ class Foo(ModelSerializer):
90
+ name = mdoels.CharField()
91
+ bar = models.CharField()
92
+ active = models.BooleanField(default=False)
93
+
94
+ class ReadSerializer:
95
+ fields = ["id", "name", "bar"]
96
+
97
+ class CreateSerializer:
98
+ customs = [("force_activation", bool, False)]
99
+ fields = ["name", "bar"]
100
+
101
+ class UpdateSerializer:
102
+ fields = ["name", "bar"]
103
+
104
+ async def custom_actions(self, payload: dict[str, Any]):
105
+ if not payload.get("force_activation"):
106
+ return
107
+ setattr(self, "force_activation", True)
108
+
109
+ async def post_create(self) -> None:
110
+ if not hasattr(self, "force_activation") or not getattr(self, "force_activation"):
111
+ return
112
+ self.active = True
113
+ await self.asave()
114
+ ```
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
+
117
+
118
+ ### APIViewSet
119
+ - 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.
120
+ ```Python
121
+ from ninja import NinjAPI
122
+ from ninja_aio.views import APIViewSet
123
+ from ninja_aio.parsers import ORJSONParser
124
+ from ninja_aio.renders import ORJSONRender
125
+
126
+ from .models import Foo
127
+
128
+ api = NinjaAPI(renderer=ORJSONRenderer(), parser=ORJSONParser())
129
+
130
+
131
+ class FooAPI(APIViewSet):
132
+ model = Foo
133
+ api = api
134
+
135
+
136
+ FooAPI().add_views_to_route()
137
+ ```
138
+ - 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".
139
+ ```Python
140
+ from ninja import NinjAPI, Schema
141
+ from ninja_aio.views import APIViewSet
142
+ from ninja_aio.parsers import ORJSONParser
143
+ from ninja_aio.renders import ORJSONRender
144
+
145
+ from .models import Foo
146
+
147
+ api = NinjaAPI(renderer=ORJSONRenderer(), parser=ORJSONParser())
148
+
149
+
150
+ class ExampleSchemaOut(Schema):
151
+ sum: float
152
+
153
+
154
+ class ExampleSchemaIn(Schema):
155
+ n1: float
156
+ n2: float
157
+
158
+
159
+ class FooAPI(APIViewSet):
160
+ model = Foo
161
+ api = api
162
+
163
+ def views(self):
164
+ @self.router.post("numbers-sum/", response={200: ExampleSchemaOut)
165
+ async def sum(request: HttpRequest, data: ExampleSchemaIn):
166
+ return 200, {sum: data.n1 + data.n2}
167
+
168
+
169
+ FooAPI().add_views_to_route()
170
+ ```
171
+
172
+ ### APIView
173
+ - View class to code generic views class based. In your views.py import APIView class.
174
+ ```Python
175
+ from ninja import NinjAPI, Schema
176
+ from ninja_aio.views import APIView
177
+ from ninja_aio.parsers import ORJSONParser
178
+ from ninja_aio.renders import ORJSONRender
179
+
180
+ api = NinjaAPI(renderer=ORJSONRenderer(), parser=ORJSONParser())
181
+
182
+
183
+ class ExampleSchemaOut(Schema):
184
+ sum: float
185
+
186
+
187
+ class ExampleSchemaIn(Schema):
188
+ n1: float
189
+ n2: float
190
+
191
+
192
+ class SumView(APIView):
193
+ api = api
194
+ api_router_path = "numbers-sum/"
195
+ router_tag = "Sum"
196
+
197
+ def views(self):
198
+ @self.router.post(self.api_router_path, response={200: ExampleSchemaOut)
199
+ async def sum(request: HttpRequest, data: ExampleSchemaIn):
200
+ return 200, {sum: data.n1 + data.n2}
201
+
202
+
203
+ SumView().add_views_to_route()
204
+ ```
205
+
206
+ ## 🔒 Authentication
207
+ ### Jwt
208
+ - 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.
209
+ ```Python
210
+ from ninja_aio.auth import AsyncJWTBearer
211
+ from django.conf import settings
212
+ from django.http import HttpRequest
213
+
214
+ from .models import Foo
215
+
216
+
217
+ class CustomJWTBearer(AsyncJWTBearer):
218
+ jwt_public = settings.JWT_PUBLIC
219
+ claims = {"foo_id": {"essential": True}}
220
+
221
+ async def auth_handler(self, request: HttpRequest):
222
+ try:
223
+ request.user = await Foo.objects.aget(id=self.dcd.claims["foo_id"])
224
+ except Foo.DoesNotExist:
225
+ return None
226
+ return request.user
227
+ ```
228
+ - Then add it to views.
229
+ ```Python
230
+ from ninja import NinjAPI, Schema
231
+ from ninja_aio.views import APIViewSet, APIView
232
+ from ninja_aio.parsers import ORJSONParser
233
+ from ninja_aio.renders import ORJSONRender
234
+
235
+ from .models import Foo
236
+
237
+ api = NinjaAPI(renderer=ORJSONRenderer(), parser=ORJSONParser())
238
+
239
+
240
+ class FooAPI(APIViewSet):
241
+ model = Foo
242
+ api = api
243
+ auths = CustomJWTBearer()
244
+
245
+
246
+ class ExampleSchemaOut(Schema):
247
+ sum: float
248
+
249
+
250
+ class ExampleSchemaIn(Schema):
251
+ n1: float
252
+ n2: float
253
+
254
+
255
+ class SumView(APIView):
256
+ api = api
257
+ api_router_path = "numbers-sum/"
258
+ router_tag = "Sum"
259
+ auths = CustomJWTBearer()
260
+
261
+ def views(self):
262
+ @self.router.post(self.api_router_path, response={200: ExampleSchemaOut}, auth=self.auths)
263
+ async def sum(request: HttpRequest, data: ExampleSchemaIn):
264
+ return 200, {sum: data.n1 + data.n2}
265
+
266
+
267
+ FooAPI().add_views_to_route()
268
+ SumView().add_views_to_route()
269
+ ```
270
+
271
+ ## 📌 Notes
272
+ - Feel free to contribute and improve the program. 🛠️
273
+
@@ -0,0 +1,11 @@
1
+ ninja_aio/__init__.py,sha256=b8kPWi0QdIvtWqqVRGTPL-1Nnwvlnf-FNDPVUvZOq8s,69
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.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
10
+ django_ninja_aio_crud-0.1.0.dist-info/METADATA,sha256=atQGOc7SRwQLalwhnT65yJCqdAIiqTQXyyIpz05WH0s,8031
11
+ django_ninja_aio_crud-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
ninja_aio/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """ Django Ninja AIO CRUD - Rest Framework """
2
+
3
+ __version__ = "0.1.0"
ninja_aio/auth.py ADDED
@@ -0,0 +1,50 @@
1
+ from joserfc import jwt, jwk, errors
2
+ from django.http.request import HttpRequest
3
+ from ninja.security.http import HttpBearer
4
+
5
+ from .exceptions import AuthError
6
+
7
+
8
+
9
+ class AsyncJwtBearer(HttpBearer):
10
+ jwt_public: jwk.RSAKey
11
+ claims: dict[str, dict]
12
+ algorithms: list[str] = ["RS256"]
13
+
14
+ @classmethod
15
+ def get_claims(cls):
16
+ return jwt.JWTClaimsRegistry(**cls.claims)
17
+
18
+ def validate_claims(self, claims: jwt.Claims):
19
+ jwt_claims = self.get_claims()
20
+
21
+ try:
22
+ jwt_claims.validate(claims)
23
+ except (
24
+ errors.InvalidClaimError,
25
+ errors.MissingClaimError,
26
+ errors.ExpiredTokenError,
27
+ ):
28
+ raise AuthError()
29
+
30
+ async def auth_handler(self, request: HttpRequest):
31
+ """
32
+ Override this method to make your own authentication
33
+ """
34
+ pass
35
+
36
+ async def authenticate(self, request: HttpRequest, token: str):
37
+ try:
38
+ self.dcd = jwt.decode(token, self.jwt_public, algorithms=self.algorithms)
39
+ except (
40
+ errors.BadSignatureError,
41
+ ValueError,
42
+ ):
43
+ return None
44
+
45
+ try:
46
+ self.validate_claims(self.dcd.claims)
47
+ except AuthError:
48
+ return None
49
+
50
+ return await self.auth_handler(request)
@@ -0,0 +1,24 @@
1
+ class BaseException(Exception):
2
+ error: str | dict = ""
3
+ status_code: int = 400
4
+
5
+ def __init__(
6
+ self,
7
+ error: str | dict = None,
8
+ status_code: int | None = None,
9
+ is_critical: bool = False,
10
+ ) -> None:
11
+ self.error = error or self.error
12
+ self.status_code = status_code or self.status_code
13
+ self.is_critical = is_critical
14
+
15
+ def get_error(self):
16
+ return self.error, self.status_code
17
+
18
+
19
+ class SerializeError(BaseException):
20
+ pass
21
+
22
+
23
+ class AuthError(BaseException):
24
+ pass
ninja_aio/models.py ADDED
@@ -0,0 +1,305 @@
1
+ import base64
2
+ from typing import Any, Literal
3
+
4
+ from ninja.schema import Schema
5
+ from ninja.orm import create_schema
6
+
7
+ from django.db import models
8
+ from django.http import HttpResponse, HttpRequest
9
+ from django.core.exceptions import ObjectDoesNotExist
10
+ from django.db.models.fields.related import OneToOneRel
11
+ from django.db.models.fields.related_descriptors import (
12
+ ReverseManyToOneDescriptor,
13
+ ReverseOneToOneDescriptor,
14
+ )
15
+
16
+ from .exceptions import SerializeError
17
+
18
+ S_TYPES = Literal["create", "update"]
19
+ REL_TYPES = Literal["many", "one"]
20
+
21
+
22
+ class ModelSerializer(models.Model):
23
+ class Meta:
24
+ abstract = True
25
+
26
+ class CreateSerializer:
27
+ fields: list[str] = []
28
+ customs: list[tuple[str, type, Any]] = []
29
+
30
+ class ReadSerializer:
31
+ fields: list[str] = []
32
+
33
+ class UpdateSerializer:
34
+ fields: list[str] = []
35
+ customs: list[tuple[str, type, Any]] = []
36
+
37
+ @property
38
+ def has_custom_fields_create(self):
39
+ return hasattr(self.CreateSerializer, "customs")
40
+
41
+ @property
42
+ def has_custom_fields_update(self):
43
+ return hasattr(self.UpdateSerializer, "customs")
44
+
45
+ @property
46
+ def has_custom_fields(self):
47
+ return self.has_custom_fields_create or self.has_custom_fields_update
48
+
49
+ def has_changed(self, field: str) -> bool:
50
+ """
51
+ Check if a model field has changed
52
+ """
53
+ if not self.pk:
54
+ return False
55
+ old_value = (
56
+ self.__class__._default_manager.filter(pk=self.pk)
57
+ .values(field)
58
+ .get()[field]
59
+ )
60
+ return getattr(self, field) != old_value
61
+
62
+ @classmethod
63
+ async def queryset_request(cls, request: HttpRequest):
64
+ """
65
+ Override this method to return a filtered queryset based
66
+ on the request received
67
+ """
68
+ return cls.objects.select_related().all()
69
+
70
+ async def post_create(self) -> None:
71
+ """
72
+ Override this method to execute code after the object
73
+ has been created
74
+ """
75
+ pass
76
+
77
+ async def custom_actions(self, payload: dict[str, Any]):
78
+ """
79
+ Override this method to execute custom actions based on
80
+ custom given fields. It could be useful for post create method.
81
+ """
82
+ pass
83
+
84
+ @classmethod
85
+ def get_reverse_relations(cls):
86
+ reverse_rels = []
87
+ for f in cls.ReadSerializer.fields:
88
+ field_obj = getattr(cls, f)
89
+ if isinstance(field_obj, ReverseManyToOneDescriptor):
90
+ reverse_rels.append(field_obj.field.__dict__.get("_related_name"))
91
+ if isinstance(field_obj, ReverseOneToOneDescriptor):
92
+ reverse_rels.append(
93
+ list(field_obj.__dict__.values())[0].__dict__.get("related_name")
94
+ )
95
+ return reverse_rels
96
+
97
+ @classmethod
98
+ def get_reverse_relation_schema(
99
+ cls, obj: type["ModelSerializer"], rel_type: type[REL_TYPES], field: str
100
+ ):
101
+ for index, rel_f in enumerate(obj.ReadSerializer.fields):
102
+ if rel_f == cls._meta.model_name:
103
+ obj.ReadSerializer.fields.pop(index)
104
+ break
105
+ rel_schema = obj.generate_read_s(depth=0)
106
+ if rel_type == "many":
107
+ rel_schema = list[rel_schema]
108
+ rel_data = (
109
+ field,
110
+ rel_schema | None,
111
+ None,
112
+ )
113
+ obj.ReadSerializer.fields.append(cls._meta.model_name)
114
+ return rel_data
115
+
116
+ @classmethod
117
+ def get_schema_out_data(cls):
118
+ fields = []
119
+ reverse_rels = []
120
+ for f in cls.ReadSerializer.fields:
121
+ field_obj = getattr(cls, f)
122
+ if isinstance(field_obj, ReverseManyToOneDescriptor):
123
+ rel_obj: ModelSerializer = field_obj.field.__dict__.get("model")
124
+ rel_data = cls.get_reverse_relation_schema(rel_obj, "many", f)
125
+ reverse_rels.append(rel_data)
126
+ continue
127
+ if isinstance(field_obj, ReverseOneToOneDescriptor):
128
+ rel_obj: ModelSerializer = list(field_obj.__dict__.values())[
129
+ 0
130
+ ].__dict__.get("related_model")
131
+ rel_data = cls.get_reverse_relation_schema(rel_obj, "one", f)
132
+ reverse_rels.append(rel_data)
133
+ continue
134
+ fields.append(f)
135
+ return fields, reverse_rels
136
+
137
+ @classmethod
138
+ def is_custom(cls, field: str):
139
+ customs = cls.get_custom_fields("create") or []
140
+ customs.extend(cls.get_custom_fields("update") or [])
141
+ return any(field in custom_f for custom_f in customs)
142
+
143
+ @classmethod
144
+ async def parse_input_data(cls, request: HttpRequest, data: Schema):
145
+ payload = data.model_dump()
146
+ customs = {k: v for k, v in payload.items() if cls.is_custom(k)}
147
+ for k, v in payload.items():
148
+ if cls.is_custom(k):
149
+ continue
150
+ field_obj = getattr(cls, k).field
151
+ if isinstance(field_obj, models.BinaryField):
152
+ if not v.endswith(b"=="):
153
+ v = v + b"=="
154
+ payload |= {k: base64.b64decode(v)}
155
+ if isinstance(field_obj, models.ForeignKey):
156
+ try:
157
+ rel: ModelSerializer = await field_obj.related_model.get_object(
158
+ request, v
159
+ )
160
+ except ObjectDoesNotExist:
161
+ raise SerializeError({k: "not found"}, 404)
162
+ payload |= {k: rel}
163
+ new_payload = {k: v for k, v in payload.items() if k not in customs}
164
+ return new_payload, customs
165
+
166
+ @classmethod
167
+ async def parse_output_data(cls, request: HttpRequest, data: Schema):
168
+ olds_k: list[dict] = []
169
+ payload = data.model_dump()
170
+ for k, v in payload.items():
171
+ try:
172
+ field_obj = getattr(cls, k).field
173
+ except AttributeError:
174
+ field_obj = getattr(cls, k).related
175
+ if isinstance(v, dict) and (
176
+ isinstance(field_obj, models.ForeignKey)
177
+ or isinstance(field_obj, OneToOneRel)
178
+ ):
179
+ rel: ModelSerializer = await field_obj.related_model.get_object(
180
+ request, list(v.values())[0]
181
+ )
182
+ if isinstance(field_obj, models.ForeignKey):
183
+ for rel_k, rel_v in v.items():
184
+ field_rel_obj = getattr(rel, rel_k)
185
+ if isinstance(field_rel_obj, models.ForeignKey):
186
+ olds_k.append({rel_k: rel_v})
187
+ for obj in olds_k:
188
+ for old_k, old_v in obj.items():
189
+ v.pop(old_k)
190
+ v |= {f"{old_k}_id": old_v}
191
+ olds_k = []
192
+ payload |= {k: rel}
193
+ return payload
194
+
195
+ @classmethod
196
+ def get_custom_fields(cls, s_type: type[S_TYPES]):
197
+ try:
198
+ match s_type:
199
+ case "create":
200
+ customs = cls.CreateSerializer.customs
201
+ case "update":
202
+ customs = cls.UpdateSerializer.customs
203
+ except AttributeError:
204
+ return None
205
+ return customs
206
+
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
+ @classmethod
220
+ def generate_read_s(cls, depth: int = 1) -> Schema:
221
+ fields, reverse_rels = cls.get_schema_out_data()
222
+ customs = [custom for custom in reverse_rels]
223
+ return create_schema(
224
+ model=cls,
225
+ name=f"{cls._meta.model_name}SchemaOut",
226
+ depth=depth,
227
+ fields=fields,
228
+ custom_fields=customs,
229
+ )
230
+
231
+ @classmethod
232
+ def generate_create_s(cls) -> Schema:
233
+ return create_schema(
234
+ model=cls,
235
+ name=f"{cls._meta.model_name}SchemaIn",
236
+ fields=cls.CreateSerializer.fields,
237
+ optional_fields=cls.get_optional_fields("create"),
238
+ custom_fields=cls.get_custom_fields("create"),
239
+ )
240
+
241
+ @classmethod
242
+ def generate_update_s(cls) -> Schema:
243
+ return create_schema(
244
+ model=cls,
245
+ name=f"{cls._meta.model_name}SchemaPatch",
246
+ fields=cls.UpdateSerializer.fields,
247
+ optional_fields=cls.get_optional_fields("update"),
248
+ custom_fields=cls.get_custom_fields("update"),
249
+ )
250
+
251
+ @classmethod
252
+ async def get_object(cls, request: HttpRequest, pk: int | str):
253
+ q = {cls._meta.pk.attname: pk}
254
+ try:
255
+ obj = (
256
+ await (await cls.queryset_request(request))
257
+ .prefetch_related(*cls.get_reverse_relations())
258
+ .aget(**q)
259
+ )
260
+ except ObjectDoesNotExist:
261
+ raise SerializeError({cls._meta.model_name: "not found"}, 404)
262
+ return obj
263
+
264
+ @classmethod
265
+ async def create_s(cls, request: HttpRequest, data: Schema):
266
+ try:
267
+ payload, customs = await cls.parse_input_data(request, data)
268
+ pk = (await cls.objects.acreate(**payload)).pk
269
+ obj = await cls.get_object(request, pk)
270
+ except SerializeError as e:
271
+ return e.status_code, e.error
272
+ payload |= customs
273
+ await obj.custom_actions(payload)
274
+ await obj.post_create()
275
+ return await cls.read_s(request, obj)
276
+
277
+ @classmethod
278
+ async def read_s(cls, request: HttpRequest, obj: type["ModelSerializer"]):
279
+ schema = cls.generate_read_s().from_orm(obj)
280
+ return await cls.parse_output_data(request, schema)
281
+
282
+ @classmethod
283
+ async def update_s(cls, request: HttpRequest, data: Schema, pk: int | str):
284
+ try:
285
+ obj = await cls.get_object(request, pk)
286
+ except SerializeError as e:
287
+ return e.status_code, e.error
288
+
289
+ payload, customs = await cls.parse_input_data(request, data)
290
+ for k, v in payload.items():
291
+ if v is not None:
292
+ setattr(obj, k, v)
293
+ payload |= customs
294
+ await obj.custom_actions(payload)
295
+ await obj.asave()
296
+ return await cls.read_s(request, obj)
297
+
298
+ @classmethod
299
+ async def delete_s(cls, request: HttpRequest, pk: int | str):
300
+ try:
301
+ obj = await cls.get_object(request, pk)
302
+ except SerializeError as e:
303
+ return e.status_code, e.error
304
+ await obj.adelete()
305
+ return HttpResponse(status=204)
ninja_aio/parsers.py ADDED
@@ -0,0 +1,7 @@
1
+ import orjson
2
+ from ninja.parser import Parser
3
+
4
+
5
+ class ORJSONParser(Parser):
6
+ def parse_body(self, request):
7
+ return orjson.loads(request.body)
ninja_aio/renders.py ADDED
@@ -0,0 +1,43 @@
1
+ import base64
2
+
3
+ import orjson
4
+ from django.http import HttpRequest
5
+ from ninja.renderers import BaseRenderer
6
+
7
+
8
+ class ORJSONRenderer(BaseRenderer):
9
+ media_type = "application/json"
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))
14
+ return orjson.dumps(self.render_dict(data))
15
+
16
+ @classmethod
17
+ def render_list(cls, data: list[dict]) -> list[dict]:
18
+ return [cls.parse_data(d) for d in data]
19
+
20
+ @classmethod
21
+ def render_dict(cls, data: dict):
22
+ return cls.parse_data(data)
23
+
24
+ @classmethod
25
+ def parse_data(cls, data: dict):
26
+ for k, v in data.items():
27
+ if isinstance(v, bytes):
28
+ data |= {k: base64.b64encode(v).decode()}
29
+ if isinstance(v, dict):
30
+ for k_rel, v_rel in v.items():
31
+ if not isinstance(v_rel, bytes):
32
+ continue
33
+ v |= {k_rel: base64.b64encode(v_rel).decode()}
34
+ data |= {k: v}
35
+ if isinstance(v, list):
36
+ index_rel = 0
37
+ for f_rel in v:
38
+ for k_rel, v_rel in f_rel.items():
39
+ if isinstance(v_rel, bytes):
40
+ v[index_rel] |= {k_rel: base64.b64encode(v_rel).decode()}
41
+ index_rel += 1
42
+ data |= {k: v}
43
+ return data
ninja_aio/schemas.py ADDED
@@ -0,0 +1,5 @@
1
+ from pydantic import RootModel
2
+
3
+
4
+ class GenericMessageSchema(RootModel[dict[str, str]]):
5
+ root: dict[str, str]
ninja_aio/views.py ADDED
@@ -0,0 +1,194 @@
1
+ from typing import List
2
+
3
+ from ninja import NinjaAPI, Router
4
+ from ninja.constants import NOT_SET
5
+ from django.http import HttpRequest
6
+
7
+ from .models import ModelSerializer
8
+ from .schemas import GenericMessageSchema
9
+ from .exceptions import SerializeError
10
+
11
+ ERROR_CODES = frozenset({400, 401, 404, 428})
12
+
13
+
14
+ class APIView:
15
+ api: NinjaAPI
16
+ router_tag: str
17
+ api_route_path: str
18
+ auths: list | None = NOT_SET
19
+
20
+ def __init__(self) -> None:
21
+ self.router = Router(tags=[self.router_tag])
22
+ self.error_codes = ERROR_CODES
23
+
24
+ def views(self):
25
+ """
26
+ Override this method to add your custom views. For example:
27
+ @self.router.get(some_path, response=some_schema)
28
+ async def some_method(request, *args, **kwargs):
29
+ pass
30
+
31
+ You can add multilple views just doing:
32
+
33
+ @self.router.get(some_path, response=some_schema)
34
+ async def some_method(request, *args, **kwargs):
35
+ pass
36
+
37
+ @self.router.post(some_path, response=some_schema)
38
+ async def some_method(request, *args, **kwargs):
39
+ pass
40
+
41
+ If you provided a list of auths you can chose which of your views
42
+ should be authenticated:
43
+
44
+ AUTHENTICATED VIEW:
45
+
46
+ @self.router.get(some_path, response=some_schema, auth=self.auths)
47
+ async def some_method(request, *args, **kwargs):
48
+ pass
49
+
50
+ NOT AUTHENTICATED VIEW:
51
+
52
+ @self.router.post(some_path, response=some_schema)
53
+ async def some_method(request, *args, **kwargs):
54
+ pass
55
+ """
56
+ pass
57
+
58
+ def add_views(self):
59
+ self.views()
60
+ return self.router
61
+
62
+ def add_views_to_route(self):
63
+ return self.api.add_router(f"{self.api_route_path}/", self.add_views())
64
+
65
+
66
+ class APIViewSet:
67
+ model: ModelSerializer
68
+ api: NinjaAPI
69
+ auths: list | None = NOT_SET
70
+
71
+ def __init__(self) -> None:
72
+ self.router = Router(tags=[self.model._meta.model_name.capitalize()])
73
+ self.schema_in = self.model.generate_create_s()
74
+ self.schema_out = self.model.generate_read_s()
75
+ self.schema_update = self.model.generate_update_s()
76
+ self.path = "/"
77
+ self.path_retrieve = f"{self.model._meta.pk.attname}/"
78
+ self.error_codes = ERROR_CODES
79
+
80
+ def create_view(self):
81
+ @self.router.post(
82
+ self.path,
83
+ auth=self.auths,
84
+ response={200: self.schema_out, self.error_codes: GenericMessageSchema},
85
+ )
86
+ async def create(request: HttpRequest, data: self.schema_in):
87
+ return await self.model.create_s(request, data)
88
+
89
+ create.__name__ = f"create_{self.model._meta.model_name}"
90
+
91
+ def list_view(self):
92
+ @self.router.get(
93
+ self.path,
94
+ auth=self.auths,
95
+ response={
96
+ 200: List[self.schema_out],
97
+ self.error_codes: GenericMessageSchema,
98
+ },
99
+ )
100
+ async def list(request: HttpRequest):
101
+ qs = await self.model.queryset_request(request)
102
+ rels = self.model.get_reverse_relations()
103
+ if len(rels) > 0:
104
+ qs = qs.prefetch_related(*rels)
105
+ objs = [await self.model.read_s(request, obj) async for obj in qs.all()]
106
+ return objs
107
+
108
+ list.__name__ = f"list_{self.model._meta.verbose_name_plural}"
109
+
110
+ def retrieve_view(self):
111
+ @self.router.get(
112
+ self.path_retrieve,
113
+ auth=self.auths,
114
+ response={200: self.schema_out, self.error_codes: GenericMessageSchema},
115
+ )
116
+ async def retrieve(request: HttpRequest, pk: int | str):
117
+ try:
118
+ obj = await self.model.get_object(request, pk)
119
+ except SerializeError as e:
120
+ return e.status_code, e.error
121
+ return await self.model.read_s(request, obj)
122
+
123
+ retrieve.__name__ = f"retrieve_{self.model._meta.model_name}"
124
+
125
+ def update_view(self):
126
+ @self.router.patch(
127
+ self.path_retrieve,
128
+ auth=self.auths,
129
+ response={200: self.schema_out, self.error_codes: GenericMessageSchema},
130
+ )
131
+ async def update(request: HttpRequest, data: self.schema_update, pk: int | str):
132
+ return await self.model.update_s(request, data, pk)
133
+
134
+ update.__name__ = f"update_{self.model._meta.model_name}"
135
+
136
+ def delete_view(self):
137
+ @self.router.delete(
138
+ self.path_retrieve,
139
+ auth=self.auths,
140
+ response={204: None, self.error_codes: GenericMessageSchema},
141
+ )
142
+ async def delete(request: HttpRequest, pk: int | str):
143
+ return await self.model.delete_s(request, pk)
144
+
145
+ delete.__name__ = f"delete_{self.model._meta.model_name}"
146
+
147
+ def views(self):
148
+ """
149
+ Override this method to add your custom views. For example:
150
+ @self.router.get(some_path, response=some_schema)
151
+ async def some_method(request, *args, **kwargs):
152
+ pass
153
+
154
+ You can add multilple views just doing:
155
+
156
+ @self.router.get(some_path, response=some_schema)
157
+ async def some_method(request, *args, **kwargs):
158
+ pass
159
+
160
+ @self.router.post(some_path, response=some_schema)
161
+ async def some_method(request, *args, **kwargs):
162
+ pass
163
+
164
+ If you provided a list of auths you can chose which of your views
165
+ should be authenticated:
166
+
167
+ AUTHENTICATED VIEW:
168
+
169
+ @self.router.get(some_path, response=some_schema, auth=self.auths)
170
+ async def some_method(request, *args, **kwargs):
171
+ pass
172
+
173
+ NOT AUTHENTICATED VIEW:
174
+
175
+ @self.router.post(some_path, response=some_schema)
176
+ async def some_method(request, *args, **kwargs):
177
+ pass
178
+ """
179
+ pass
180
+
181
+ def add_views(self):
182
+ self.create_view()
183
+ self.list_view()
184
+ self.retrieve_view()
185
+ self.update_view()
186
+ self.delete_view()
187
+ self.views()
188
+ return self.router
189
+
190
+ def add_views_to_route(self):
191
+ return self.api.add_router(
192
+ f"{self.model._meta.verbose_name_plural}/",
193
+ self.add_views(),
194
+ )