django-ninja-aio-crud 0.5.0__py3-none-any.whl → 0.6.1__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.3
2
2
  Name: django-ninja-aio-crud
3
- Version: 0.5.0
3
+ Version: 0.6.1
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10
@@ -25,9 +25,16 @@ Classifier: Topic :: Internet :: WWW/HTTP
25
25
  Requires-Dist: django-ninja >=1.3.0
26
26
  Requires-Dist: joserfc >=1.0.0
27
27
  Requires-Dist: orjson >= 3.10.7
28
+ Requires-Dist: coverage ; extra == "test"
28
29
  Project-URL: Repository, https://github.com/caspel26/django-ninja-aio-crud
30
+ Provides-Extra: test
29
31
 
30
32
  # 🥷 django-ninja-aio-crud
33
+ ![Test](https://github.com/caspel26/django-ninja-aio-crud/actions/workflows/coverage.yml/badge.svg)
34
+ [![codecov](https://codecov.io/gh/caspel26/django-ninja-aio-crud/graph/badge.svg?token=DZ5WDT3S20)](https://codecov.io/gh/caspel26/django-ninja-aio-crud)
35
+ [![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/)
36
+ [![PyPI - License](https://img.shields.io/pypi/l/django-ninja-aio-crud)](https://github.com/caspel26/django-ninja-aio-crud/blob/main/LICENSE)
37
+ [![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)
31
38
  > [!NOTE]
32
39
  > 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
40
 
@@ -120,7 +127,7 @@ class Foo(ModelSerializer):
120
127
  if not payload.get("force_activation"):
121
128
  return
122
129
  setattr(self, "force_activation", True)
123
-
130
+
124
131
  async def post_create(self) -> None:
125
132
  if not hasattr(self, "force_activation") or not getattr(self, "force_activation"):
126
133
  return
@@ -150,7 +157,7 @@ class Foo(ModelSerializer):
150
157
  optionals = [("bar", str), ("active", bool)]
151
158
 
152
159
  class UpdateSerializer:
153
- optionals = [[("bar", str), ("active", bool)]
160
+ optionals = [("bar", str), ("active", bool)]
154
161
  ```
155
162
 
156
163
  - Instead of declaring your fields maybe you want to exclude some of them. Declaring "excludes" attribute into serializers will exclude the given fields. (You can declare only one between "fields" and "excludes").
@@ -175,7 +182,7 @@ class Foo(ModelSerializer):
175
182
 
176
183
  class UpdateSerializer:
177
184
  excludes = ["id", "name"]
178
- optionals = [[("bar", str), ("active", bool)]
185
+ optionals = [("bar", str), ("active", bool)]
179
186
  ```
180
187
 
181
188
 
@@ -197,7 +204,7 @@ class FooAPI(APIViewSet):
197
204
  model = Foo
198
205
  api = api
199
206
 
200
-
207
+
201
208
  FooAPI().add_views_to_route()
202
209
  ```
203
210
 
@@ -238,6 +245,10 @@ FooAPI().add_views_to_route()
238
245
 
239
246
  - You can also choose to disable any operation from crud by declaring "disbale" attribute. You can give "all" to disable every crud operation except for additional views.
240
247
 
248
+ > [!TIP]
249
+ > You can exclude by default an endpoint without declaring the serializer.
250
+ > For example if you don't want to give update method to a CRUD just do not declare UpdateSerializer into model.
251
+
241
252
  ```python
242
253
  # views.py
243
254
  from ninja_aio import NinjaAIO
@@ -254,6 +265,29 @@ class FooAPI(APIViewSet):
254
265
  disable = ["retrieve", "update"]
255
266
 
256
267
 
268
+ FooAPI().add_views_to_route()
269
+ ```
270
+ For the list endpoint you can also set query params and handle them. They will be also visible into swagger.
271
+
272
+ ```python
273
+ # views.py
274
+ from ninja_aio import NinjaAIO
275
+ from ninja_aio.views import APIViewSet
276
+
277
+ from .models import Foo
278
+
279
+ api = NinjaAIO()
280
+
281
+
282
+ class FooAPI(APIViewSet):
283
+ model = Foo
284
+ api = api
285
+ query_params = {"name": (str, None), "active": (bool, None)}
286
+
287
+ async def query_params_handler(self, queryset, filters):
288
+ return queryset.filter(**{k: v for k, v in filters.items() if v is not None})
289
+
290
+
257
291
  FooAPI().add_views_to_route()
258
292
  ```
259
293
 
@@ -371,7 +405,7 @@ BarAPI().add_views_to_route()
371
405
 
372
406
  ### Jwt
373
407
 
374
- - 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.
408
+ - 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.
375
409
 
376
410
  ```python
377
411
  from ninja_aio.auth import AsyncJWTBearer
@@ -0,0 +1,13 @@
1
+ ninja_aio/__init__.py,sha256=VSJ4PO3jZ-Z_NF8y2IrjsTc6MSqxlJ_b7TtAvZW4f28,119
2
+ ninja_aio/api.py,sha256=Fe6l3YCy7MW5TY4-Lbl80CFuK2NT2Y7tHfmqPk6Mqak,1735
3
+ ninja_aio/auth.py,sha256=z9gniLIgT8SjRqhGN7ZI0AGHjsALwgU6eyr2m46fwFY,1389
4
+ ninja_aio/exceptions.py,sha256=Lg0nUJe6kWEf0qvXpQ9FqQ8sCFBFH-lP4xovvY-YfiY,1922
5
+ ninja_aio/models.py,sha256=lplwxtZKweoppJEi0ERPSHRtsq5UwQaGK3NtR_I-FBw,15213
6
+ ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
7
+ ninja_aio/renders.py,sha256=mHeKNJtmDhZmgFpS9B6SPn5uZFcyVXrsoMhr149LeW8,1555
8
+ ninja_aio/schemas.py,sha256=EgRkfhnzZqwGvdBmqlZixMtMcoD1ZxV_qzJ3fmaAy20,113
9
+ ninja_aio/types.py,sha256=EHznS-6KWLwSX5hLeXbAi7qHWla09_rGeQraiLpH-aY,491
10
+ ninja_aio/views.py,sha256=z820mKzfbvh9SZ4cALQVKMfc64kx74t8xWGWaUBzHfs,9270
11
+ django_ninja_aio_crud-0.6.1.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
12
+ django_ninja_aio_crud-0.6.1.dist-info/METADATA,sha256=4nH4p3l9UUIUGilQ4C_fDM1Uv-WiF6FveqrG25JosQk,13076
13
+ django_ninja_aio_crud-0.6.1.dist-info/RECORD,,
ninja_aio/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.1"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
ninja_aio/auth.py CHANGED
@@ -2,7 +2,7 @@ from joserfc import jwt, jwk, errors
2
2
  from django.http.request import HttpRequest
3
3
  from ninja.security.http import HttpBearer
4
4
 
5
- from .exceptions import AuthError
5
+ from .exceptions import AuthError, parse_jose_error
6
6
 
7
7
 
8
8
  class AsyncJwtBearer(HttpBearer):
@@ -23,8 +23,8 @@ class AsyncJwtBearer(HttpBearer):
23
23
  errors.InvalidClaimError,
24
24
  errors.MissingClaimError,
25
25
  errors.ExpiredTokenError,
26
- ):
27
- raise AuthError()
26
+ ) as exc:
27
+ raise AuthError(**parse_jose_error(exc), status_code=401)
28
28
 
29
29
  async def auth_handler(self, request: HttpRequest):
30
30
  """
@@ -35,15 +35,11 @@ class AsyncJwtBearer(HttpBearer):
35
35
  async def authenticate(self, request: HttpRequest, token: str):
36
36
  try:
37
37
  self.dcd = jwt.decode(token, self.jwt_public, algorithms=self.algorithms)
38
- except (
39
- errors.BadSignatureError,
40
- ValueError,
41
- ):
42
- return None
38
+ except errors.BadSignatureError as exc:
39
+ raise AuthError(**parse_jose_error(exc), status_code=401)
40
+ except ValueError as exc:
41
+ raise AuthError(", ".join(exc.args), 401)
43
42
 
44
- try:
45
- self.validate_claims(self.dcd.claims)
46
- except AuthError:
47
- return None
43
+ self.validate_claims(self.dcd.claims)
48
44
 
49
45
  return await self.auth_handler(request)
ninja_aio/exceptions.py CHANGED
@@ -1,6 +1,9 @@
1
1
  from functools import partial
2
+
3
+ from joserfc.errors import JoseError
2
4
  from ninja import NinjaAPI
3
5
  from django.http import HttpRequest, HttpResponse
6
+ from pydantic import ValidationError
4
7
 
5
8
 
6
9
  class BaseException(Exception):
@@ -11,11 +14,14 @@ class BaseException(Exception):
11
14
  self,
12
15
  error: str | dict = None,
13
16
  status_code: int | None = None,
14
- is_critical: bool = False,
17
+ details: str | None = None,
15
18
  ) -> None:
16
- self.error = error or self.error
19
+ if isinstance(error, str):
20
+ self.error = {"error": error}
21
+ if isinstance(error, dict):
22
+ self.error = error
23
+ self.error |= {"details": details} if details else {}
17
24
  self.status_code = status_code or self.status_code
18
- self.is_critical = is_critical
19
25
 
20
26
  def get_error(self):
21
27
  return self.error, self.status_code
@@ -29,13 +35,35 @@ class AuthError(BaseException):
29
35
  pass
30
36
 
31
37
 
32
- def _default_serialize_error(
33
- request: HttpRequest, exc: SerializeError, api: "NinjaAPI"
38
+ class PydanticValidationError(BaseException):
39
+ def __init__(self, details=None):
40
+ super().__init__("Validation Error", 400, details)
41
+
42
+
43
+ def _default_error(
44
+ request: HttpRequest, exc: BaseException, api: type[NinjaAPI]
34
45
  ) -> HttpResponse:
35
46
  return api.create_response(request, exc.error, status=exc.status_code)
36
47
 
37
48
 
49
+ def _pydantic_validation_error(
50
+ request: HttpRequest, exc: ValidationError, api: type[NinjaAPI]
51
+ ) -> HttpResponse:
52
+ error = PydanticValidationError(exc.errors(include_input=False))
53
+ return api.create_response(request, error.error, status=error.status_code)
54
+
55
+
38
56
  def set_api_exception_handlers(api: type[NinjaAPI]) -> None:
57
+ api.add_exception_handler(BaseException, partial(_default_error, api=api))
39
58
  api.add_exception_handler(
40
- SerializeError, partial(_default_serialize_error, api=api)
59
+ ValidationError, partial(_pydantic_validation_error, api=api)
60
+ )
61
+
62
+
63
+ def parse_jose_error(jose_exc: JoseError) -> dict:
64
+ error_msg = {"error": jose_exc.error}
65
+ return (
66
+ error_msg | {"details": jose_exc.description}
67
+ if jose_exc.description
68
+ else error_msg
41
69
  )
ninja_aio/models.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import base64
2
2
  from typing import Any
3
3
 
4
- from ninja.schema import Schema
4
+ from ninja import Schema
5
5
  from ninja.orm import create_schema
6
6
 
7
7
  from django.db import models
@@ -27,21 +27,48 @@ class ModelUtil:
27
27
  return self.model.get_fields("read")
28
28
  return [field.name for field in self.model._meta.get_fields()]
29
29
 
30
+ @property
31
+ def model_name(self) -> str:
32
+ return self.model._meta.model_name
33
+
34
+ @property
35
+ def model_pk_name(self) -> str:
36
+ return self.model._meta.pk.attname
37
+
38
+ @property
39
+ def model_verbose_name_plural(self) -> str:
40
+ return self.model._meta.verbose_name_plural
41
+
30
42
  def verbose_name_path_resolver(self) -> str:
31
- return "-".join(self.model._meta.verbose_name_plural.split(" "))
43
+ return "-".join(self.model_verbose_name_plural.split(" "))
32
44
 
33
45
  def verbose_name_view_resolver(self) -> str:
34
- return self.model._meta.verbose_name_plural.replace(" ", "")
46
+ return self.model_verbose_name_plural.replace(" ", "")
47
+
48
+ async def get_object(
49
+ self,
50
+ request: HttpRequest,
51
+ pk: int | str = None,
52
+ filters: dict = None,
53
+ getters: dict = None,
54
+ ):
55
+ get_q = {self.model_pk_name: pk} if pk is not None else {}
56
+ if getters:
57
+ get_q |= getters
35
58
 
36
- async def get_object(self, request: HttpRequest, pk: int | str):
37
- q = {self.model._meta.pk.attname: pk}
38
59
  obj_qs = self.model.objects.select_related()
39
60
  if isinstance(self.model, ModelSerializerMeta):
40
61
  obj_qs = await self.model.queryset_request(request)
62
+
63
+ obj_qs = obj_qs.prefetch_related(*self.get_reverse_relations())
64
+ if filters:
65
+ obj_qs = obj_qs.filter(**filters)
66
+
41
67
  try:
42
- obj = await obj_qs.prefetch_related(*self.get_reverse_relations()).aget(**q)
68
+ obj = await obj_qs.aget(**get_q)
43
69
  except ObjectDoesNotExist:
44
- raise SerializeError({self.model._meta.model_name: "not found"}, 404)
70
+ raise SerializeError({self.model_name: "not found"}, 404)
71
+
45
72
  return obj
46
73
 
47
74
  def get_reverse_relations(self) -> list[str]:
@@ -242,8 +269,11 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
242
269
  exclude=excludes,
243
270
  )
244
271
  fields = cls.get_fields(s_type)
245
- customs = cls.get_custom_fields(s_type) + cls.get_optional_fields(s_type)
272
+ optionals = cls.get_optional_fields(s_type)
273
+ customs = cls.get_custom_fields(s_type) + optionals
246
274
  excludes = cls.get_excluded_fields(s_type)
275
+ if not fields and not excludes:
276
+ fields = [f[0] for f in optionals]
247
277
  return (
248
278
  create_schema(
249
279
  model=cls,
@@ -379,11 +409,11 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
379
409
  (field, field_type, None)
380
410
  for field, field_type in cls._get_fields(s_type, "optionals")
381
411
  ]
382
-
412
+
383
413
  @classmethod
384
414
  def get_excluded_fields(cls, s_type: type[S_TYPES]):
385
415
  return cls._get_fields(s_type, "excludes")
386
-
416
+
387
417
  @classmethod
388
418
  def get_fields(cls, s_type: type[S_TYPES]):
389
419
  return cls._get_fields(s_type, "fields")
ninja_aio/views.py CHANGED
@@ -1,10 +1,10 @@
1
1
  from typing import List
2
2
 
3
- from ninja import NinjaAPI, Router, Schema, Path
3
+ from ninja import NinjaAPI, Router, Schema, Path, Query
4
4
  from ninja.constants import NOT_SET
5
5
  from ninja.pagination import paginate, AsyncPaginationBase, PageNumberPagination
6
6
  from django.http import HttpRequest
7
- from django.db.models import Model
7
+ from django.db.models import Model, QuerySet
8
8
  from pydantic import create_model
9
9
 
10
10
  from .models import ModelSerializer, ModelUtil
@@ -73,16 +73,19 @@ class APIViewSet:
73
73
  schema_update: Schema | None = None
74
74
  auths: list | None = NOT_SET
75
75
  pagination_class: type[AsyncPaginationBase] = PageNumberPagination
76
+ query_params: dict[str, tuple[type, ...]] = {}
76
77
  disable: list[type[VIEW_TYPES]] = []
77
78
 
78
79
  def __init__(self) -> None:
79
- self.router = Router(tags=[self.model._meta.model_name.capitalize()])
80
- self.path = "/"
81
- self.path_retrieve = f"{{{self.model._meta.pk.attname}}}/"
82
80
  self.error_codes = ERROR_CODES
83
81
  self.model_util = ModelUtil(self.model)
84
82
  self.schema_out, self.schema_in, self.schema_update = self.get_schemas()
85
- self.path_schema = self._create_path_schema()
83
+ self.path_schema = self._generate_path_schema()
84
+ self.filters_schema = self._generate_filters_schema()
85
+ self.router_tag = self.model_util.model_name.capitalize()
86
+ self.router = Router(tags=[self.router_tag])
87
+ self.path = "/"
88
+ self.path_retrieve = f"{{{self.model_util.model_pk_name}}}/"
86
89
 
87
90
  @property
88
91
  def _crud_views(self):
@@ -98,11 +101,19 @@ class APIViewSet:
98
101
  "delete": (None, self.delete_view),
99
102
  }
100
103
 
101
- def _create_path_schema(self):
102
- fields = {
103
- self.model._meta.pk.attname: (str | int , ...),
104
- }
105
- return create_model(f"{self.model._meta.model_name}PathSchema", **fields)
104
+ def _generate_schema(self, fields: dict, name: str) -> Schema:
105
+ return create_model(f"{self.model_util.model_name}{name}", **fields)
106
+
107
+ def _generate_path_schema(self):
108
+ return self._generate_schema(
109
+ {self.model_util.model_pk_name: (int | str, ...)}, "PathSchema"
110
+ )
111
+
112
+ def _generate_filters_schema(self):
113
+ return self._generate_schema(self.query_params, "FiltersSchema")
114
+
115
+ def _get_pk(self, data: Schema):
116
+ return data.model_dump()[self.model_util.model_pk_name]
106
117
 
107
118
  def get_schemas(self):
108
119
  if isinstance(self.model, ModelSerializerMeta):
@@ -113,6 +124,16 @@ class APIViewSet:
113
124
  )
114
125
  return self.schema_out, self.schema_in, self.schema_update
115
126
 
127
+ async def query_params_handler(
128
+ self, queryset: QuerySet[ModelSerializer], filters: dict
129
+ ):
130
+ """
131
+ Override this method to handle request query params making queries to the database
132
+ based on filters or any other logic. This method should return a queryset. filters
133
+ are given already dumped by the schema.
134
+ """
135
+ return queryset
136
+
116
137
  def create_view(self):
117
138
  @self.router.post(
118
139
  self.path,
@@ -122,7 +143,7 @@ class APIViewSet:
122
143
  async def create(request: HttpRequest, data: self.schema_in):
123
144
  return 201, await self.model_util.create_s(request, data, self.schema_out)
124
145
 
125
- create.__name__ = f"create_{self.model._meta.model_name}"
146
+ create.__name__ = f"create_{self.model_util.model_name}"
126
147
  return create
127
148
 
128
149
  def list_view(self):
@@ -135,13 +156,17 @@ class APIViewSet:
135
156
  },
136
157
  )
137
158
  @paginate(self.pagination_class)
138
- async def list(request: HttpRequest):
159
+ async def list(
160
+ request: HttpRequest, filters: Query[self.filters_schema] = None
161
+ ):
139
162
  qs = self.model.objects.select_related()
140
163
  if isinstance(self.model, ModelSerializerMeta):
141
164
  qs = await self.model.queryset_request(request)
142
165
  rels = self.model_util.get_reverse_relations()
143
166
  if len(rels) > 0:
144
167
  qs = qs.prefetch_related(*rels)
168
+ if filters is not None:
169
+ qs = await self.query_params_handler(qs, filters.model_dump())
145
170
  objs = [
146
171
  await self.model_util.read_s(request, obj, self.schema_out)
147
172
  async for obj in qs.all()
@@ -158,10 +183,10 @@ class APIViewSet:
158
183
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
159
184
  )
160
185
  async def retrieve(request: HttpRequest, pk: Path[self.path_schema]):
161
- obj = await self.model_util.get_object(request, pk)
186
+ obj = await self.model_util.get_object(request, self._get_pk(pk))
162
187
  return await self.model_util.read_s(request, obj, self.schema_out)
163
188
 
164
- retrieve.__name__ = f"retrieve_{self.model._meta.model_name}"
189
+ retrieve.__name__ = f"retrieve_{self.model_util.model_name}"
165
190
  return retrieve
166
191
 
167
192
  def update_view(self):
@@ -170,10 +195,14 @@ class APIViewSet:
170
195
  auth=self.auths,
171
196
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
172
197
  )
173
- async def update(request: HttpRequest, data: self.schema_update, pk: Path[self.path_schema]):
174
- return await self.model_util.update_s(request, data, pk, self.schema_out)
198
+ async def update(
199
+ request: HttpRequest, data: self.schema_update, pk: Path[self.path_schema]
200
+ ):
201
+ return await self.model_util.update_s(
202
+ request, data, self._get_pk(pk), self.schema_out
203
+ )
175
204
 
176
- update.__name__ = f"update_{self.model._meta.model_name}"
205
+ update.__name__ = f"update_{self.model_util.model_name}"
177
206
  return update
178
207
 
179
208
  def delete_view(self):
@@ -183,9 +212,9 @@ class APIViewSet:
183
212
  response={204: None, self.error_codes: GenericMessageSchema},
184
213
  )
185
214
  async def delete(request: HttpRequest, pk: Path[self.path_schema]):
186
- return 204, await self.model_util.delete_s(request, pk)
215
+ return 204, await self.model_util.delete_s(request, self._get_pk(pk))
187
216
 
188
- delete.__name__ = f"delete_{self.model._meta.model_name}"
217
+ delete.__name__ = f"delete_{self.model_util.model_name}"
189
218
  return delete
190
219
 
191
220
  def views(self):
@@ -1,13 +0,0 @@
1
- ninja_aio/__init__.py,sha256=ZragxlHPdmlSQb8pX6GdLzlHSdO-6qs4EelxyzrSMGw,119
2
- ninja_aio/api.py,sha256=Fe6l3YCy7MW5TY4-Lbl80CFuK2NT2Y7tHfmqPk6Mqak,1735
3
- ninja_aio/auth.py,sha256=3Pr8llYoCN59ZH3J_2qWmzjXxsy-rpQzXrVfwLfY25Q,1299
4
- ninja_aio/exceptions.py,sha256=LP9GZpDk1fYMRRgGHSAstcCvgu5uSDLg8PPDANlZGTs,1008
5
- ninja_aio/models.py,sha256=wCEQfzU3Q96rMwbkqBMjuaPipILRoX2eorId1hfsiR4,14569
6
- ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
7
- ninja_aio/renders.py,sha256=mHeKNJtmDhZmgFpS9B6SPn5uZFcyVXrsoMhr149LeW8,1555
8
- ninja_aio/schemas.py,sha256=EgRkfhnzZqwGvdBmqlZixMtMcoD1ZxV_qzJ3fmaAy20,113
9
- ninja_aio/types.py,sha256=EHznS-6KWLwSX5hLeXbAi7qHWla09_rGeQraiLpH-aY,491
10
- ninja_aio/views.py,sha256=p0wWVMw1TRtdCSHKfIWHPINGn3rkGWFPsRwMyg4HUOs,8121
11
- django_ninja_aio_crud-0.5.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
12
- django_ninja_aio_crud-0.5.0.dist-info/METADATA,sha256=FdZKHgHjgaZqqrv9BNB801Q1lHjSlYaMFUA-cBA_SQs,11573
13
- django_ninja_aio_crud-0.5.0.dist-info/RECORD,,