django-ninja-aio-crud 2.0.0rc7__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.
- {django_ninja_aio_crud-2.0.0rc7.dist-info → django_ninja_aio_crud-2.2.0.dist-info}/METADATA +2 -2
- django_ninja_aio_crud-2.2.0.dist-info/RECORD +23 -0
- ninja_aio/__init__.py +1 -1
- ninja_aio/api.py +24 -0
- ninja_aio/auth.py +121 -3
- ninja_aio/helpers/api.py +5 -3
- ninja_aio/types.py +3 -1
- ninja_aio/views/__init__.py +3 -0
- ninja_aio/{views.py → views/api.py} +144 -46
- ninja_aio/views/mixins.py +275 -0
- django_ninja_aio_crud-2.0.0rc7.dist-info/RECORD +0 -21
- {django_ninja_aio_crud-2.0.0rc7.dist-info → django_ninja_aio_crud-2.2.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-2.0.0rc7.dist-info → django_ninja_aio_crud-2.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-ninja-aio-crud
|
|
3
|
-
Version: 2.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
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
ninja_aio/decorators.py,sha256=BHoFIiqdIVMFqSxGh-F6WeZFo1xZK4ieDw3dzKfxZIM,8147
|
|
5
|
+
ninja_aio/exceptions.py,sha256=1-iRbrloIyi0CR6Tcrn5YR4_LloA7PPohKIBaxXJ0-8,2596
|
|
6
|
+
ninja_aio/models.py,sha256=aJlo5a64O4o-fB8QESLMUJpoA5kcjRJxPBiAIMxg46k,47652
|
|
7
|
+
ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
|
|
8
|
+
ninja_aio/renders.py,sha256=VtmSliRJyZ6gjyoib8AXMVUBYF1jPNsiceCHujI_mAs,1699
|
|
9
|
+
ninja_aio/types.py,sha256=nFqWEopm7eoEaHRzbi6EyA9WZ5Cneyd602ilFKypeQI,577
|
|
10
|
+
ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
ninja_aio/helpers/api.py,sha256=BTe7OL-X7YgWYeXmka8TmN4-gA43FVZhtH7q0dRjYX0,20238
|
|
12
|
+
ninja_aio/helpers/query.py,sha256=tE8RjXvSig-WB_0LRQ0LqoE4G_HMHsu0Na5QzTNIm6U,4262
|
|
13
|
+
ninja_aio/schemas/__init__.py,sha256=iLBwHg0pmL9k_UkIui5Q8QIl_gO4fgxSv2JHxDzqnSI,549
|
|
14
|
+
ninja_aio/schemas/api.py,sha256=-VwXhBRhmMsZLIAmWJ-P7tB5klxXS75eukjabeKKYsc,360
|
|
15
|
+
ninja_aio/schemas/generics.py,sha256=frjJsKJMAdM_NdNKv-9ddZNGxYy5PNzjIRGtuycgr-w,112
|
|
16
|
+
ninja_aio/schemas/helpers.py,sha256=rmE0D15lJg95Unv8PU44Hbf0VDTcErMCZZFG3D_znTo,2823
|
|
17
|
+
ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
|
|
18
|
+
ninja_aio/views/api.py,sha256=O_QQBxk-MltU-bPjxOSu5fFpHaDNP5J3Wk6E5rSBgi4,19744
|
|
19
|
+
ninja_aio/views/mixins.py,sha256=Jh6BG8Cs823nurVlODlzCquTxKrLH7Pmo5udPqUGZek,11378
|
|
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
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
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
1
4
|
from joserfc import jwt, jwk, errors
|
|
2
5
|
from django.http.request import HttpRequest
|
|
6
|
+
from django.utils import timezone
|
|
7
|
+
from django.conf import settings
|
|
3
8
|
from ninja.security.http import HttpBearer
|
|
4
9
|
|
|
10
|
+
from ninja_aio.types import JwtKeys
|
|
11
|
+
|
|
12
|
+
JWT_MANDATORY_CLAIMS = [
|
|
13
|
+
("iss", "JWT_ISSUER"),
|
|
14
|
+
("aud", "JWT_AUDIENCE"),
|
|
15
|
+
]
|
|
16
|
+
|
|
5
17
|
|
|
6
18
|
class AsyncJwtBearer(HttpBearer):
|
|
7
19
|
"""
|
|
@@ -9,8 +21,8 @@ class AsyncJwtBearer(HttpBearer):
|
|
|
9
21
|
using HTTP Bearer tokens. It decodes and validates JWTs against a configured public key
|
|
10
22
|
and claim registry, then delegates user retrieval to an overridable async handler.
|
|
11
23
|
Attributes:
|
|
12
|
-
jwt_public (jwk.RSAKey):
|
|
13
|
-
The
|
|
24
|
+
jwt_public (jwk.RSAKey | jwk.ECKey):
|
|
25
|
+
The public key (JWK format) used to verify the JWT signature.
|
|
14
26
|
Must be set externally before authentication occurs.
|
|
15
27
|
claims (dict[str, dict]):
|
|
16
28
|
A mapping defining expected JWT claims passed to jwt.JWTClaimsRegistry.
|
|
@@ -70,7 +82,7 @@ class AsyncJwtBearer(HttpBearer):
|
|
|
70
82
|
- authenticate -> user object (success) | False (failure)
|
|
71
83
|
"""
|
|
72
84
|
|
|
73
|
-
jwt_public:
|
|
85
|
+
jwt_public: JwtKeys
|
|
74
86
|
claims: dict[str, dict]
|
|
75
87
|
algorithms: list[str] = ["RS256"]
|
|
76
88
|
|
|
@@ -105,3 +117,109 @@ class AsyncJwtBearer(HttpBearer):
|
|
|
105
117
|
return False
|
|
106
118
|
|
|
107
119
|
return await self.auth_handler(request)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def validate_key(key: Optional[JwtKeys], setting_name: str) -> JwtKeys:
|
|
123
|
+
if key is None:
|
|
124
|
+
key = getattr(settings, setting_name, None)
|
|
125
|
+
if key is None:
|
|
126
|
+
raise ValueError(f"{setting_name} is required")
|
|
127
|
+
if not isinstance(key, (jwk.RSAKey, jwk.ECKey, jwk.OctKey)):
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"{setting_name} must be an instance of jwk.RSAKey or jwk.ECKey"
|
|
130
|
+
)
|
|
131
|
+
return key
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def validate_mandatory_claims(claims: dict) -> dict:
|
|
135
|
+
for claim_key, setting_name in JWT_MANDATORY_CLAIMS:
|
|
136
|
+
if claims.get(claim_key) is not None:
|
|
137
|
+
continue
|
|
138
|
+
value = getattr(settings, setting_name, None)
|
|
139
|
+
if value is None:
|
|
140
|
+
raise ValueError(f"jwt {claim_key} is required")
|
|
141
|
+
claims[claim_key] = value
|
|
142
|
+
return claims
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def encode_jwt(
|
|
146
|
+
claims: dict, duration: int, private_key: JwtKeys = None, algorithm: str = None
|
|
147
|
+
) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Encode and sign a JWT.
|
|
150
|
+
|
|
151
|
+
Adds time-based claims and ensures mandatory issuer/audience:
|
|
152
|
+
- iat: current time (timezone-aware)
|
|
153
|
+
- nbf: current time
|
|
154
|
+
- exp: current time + duration (seconds)
|
|
155
|
+
- iss/aud: from claims if provided; otherwise from settings.JWT_ISSUER and settings.JWT_AUDIENCE
|
|
156
|
+
|
|
157
|
+
Parameters:
|
|
158
|
+
- claims (dict): additional claims to merge into the payload (can override defaults)
|
|
159
|
+
- duration (int): token lifetime in seconds
|
|
160
|
+
- private_key (jwk.RSAKey): RSA/EC JWK for signing; defaults to settings.JWT_PRIVATE_KEY
|
|
161
|
+
- algorithm (str): JWS algorithm (default "RS256")
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
- str: JWT compact string
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
- ValueError: if private_key is missing or not jwk.RSAKey/jwk.ECKey
|
|
168
|
+
- ValueError: if mandatory claims (iss, aud) are missing and not in settings
|
|
169
|
+
|
|
170
|
+
Notes:
|
|
171
|
+
- Header includes alg, typ=JWT, and kid from the private key (if available).
|
|
172
|
+
- Uses timezone-aware timestamps from django.utils.timezone.
|
|
173
|
+
"""
|
|
174
|
+
now = timezone.now()
|
|
175
|
+
nbf = now
|
|
176
|
+
pkey = validate_key(private_key, "JWT_PRIVATE_KEY")
|
|
177
|
+
algorithm = algorithm or "RS256"
|
|
178
|
+
claims = validate_mandatory_claims(claims)
|
|
179
|
+
kid_h = {"kid": pkey.kid} if pkey.kid else {}
|
|
180
|
+
return jwt.encode(
|
|
181
|
+
header={"alg": algorithm, "typ": "JWT"} | kid_h,
|
|
182
|
+
claims={
|
|
183
|
+
"iat": now,
|
|
184
|
+
"nbf": nbf,
|
|
185
|
+
"exp": now + datetime.timedelta(seconds=duration),
|
|
186
|
+
}
|
|
187
|
+
| claims,
|
|
188
|
+
key=pkey,
|
|
189
|
+
algorithms=[algorithm],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def decode_jwt(
|
|
194
|
+
token: str,
|
|
195
|
+
public_key: JwtKeys = None,
|
|
196
|
+
algorithms: list[str] = None,
|
|
197
|
+
) -> jwt.Token:
|
|
198
|
+
"""
|
|
199
|
+
Decode and verify a JSON Web Token (JWT) using the provided RSA public key.
|
|
200
|
+
This function decodes the JWT, verifies its signature, and returns the decoded token object.
|
|
201
|
+
Parameters:
|
|
202
|
+
- token (str): The JWT string to decode.
|
|
203
|
+
- public_key (jwk.RSAKey, optional): RSA public key used to verify the token's signature.
|
|
204
|
+
If not provided, settings.JWT_PUBLIC_KEY will be used. Must be an instance of jwk.RSAKey.
|
|
205
|
+
- algorithms (list[str], optional): List of permitted algorithms for signature verification.
|
|
206
|
+
Defaults to ["RS256"] if not provided.
|
|
207
|
+
Returns:
|
|
208
|
+
- jwt.Token: The decoded JWT token object containing header and claims.
|
|
209
|
+
Raises:
|
|
210
|
+
- ValueError: If no public key is provided or if the provided key is not an instance of jwk.RSAKey.
|
|
211
|
+
- jose.errors.JoseError: If the token is invalid or fails verification.
|
|
212
|
+
Notes:
|
|
213
|
+
- The function uses the specified algorithms to restrict acceptable signing methods.
|
|
214
|
+
Example:
|
|
215
|
+
decoded_token = decode_jwt(
|
|
216
|
+
token=my_jwt,
|
|
217
|
+
public_key=my_rsa_jwk,
|
|
218
|
+
algorithms=["RS256"],
|
|
219
|
+
)
|
|
220
|
+
"""
|
|
221
|
+
return jwt.decode(
|
|
222
|
+
token,
|
|
223
|
+
validate_key(public_key, "JWT_PUBLIC_KEY"),
|
|
224
|
+
algorithms=algorithms or ["RS256"],
|
|
225
|
+
)
|
ninja_aio/helpers/api.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Coroutine
|
|
|
4
4
|
from django.http import HttpRequest
|
|
5
5
|
from ninja import Path, Query
|
|
6
6
|
from ninja.pagination import paginate
|
|
7
|
-
from ninja_aio.decorators import unique_view
|
|
7
|
+
from ninja_aio.decorators import unique_view, decorate_view
|
|
8
8
|
from ninja_aio.models import ModelSerializer, ModelUtil
|
|
9
9
|
from ninja_aio.schemas.helpers import ObjectsQuerySchema
|
|
10
10
|
from ninja_aio.schemas import (
|
|
@@ -357,8 +357,10 @@ class ManyToManyAPI:
|
|
|
357
357
|
summary=f"Get {rel_util.model._meta.verbose_name_plural.capitalize()}",
|
|
358
358
|
description=f"Get all related {rel_util.model._meta.verbose_name_plural.capitalize()}",
|
|
359
359
|
)
|
|
360
|
-
@
|
|
361
|
-
|
|
360
|
+
@decorate_view(
|
|
361
|
+
unique_view(f"get_{self.related_model_util.model_name}_{rel_path}"),
|
|
362
|
+
paginate(self.pagination_class),
|
|
363
|
+
)
|
|
362
364
|
async def get_related(
|
|
363
365
|
request: HttpRequest,
|
|
364
366
|
pk: Path[self.path_schema], # type: ignore
|
ninja_aio/types.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
2
|
|
|
3
|
+
from joserfc import jwk
|
|
3
4
|
from django.db.models import Model
|
|
5
|
+
from typing import TypeAlias
|
|
4
6
|
|
|
5
7
|
S_TYPES = Literal["read", "create", "update"]
|
|
6
8
|
F_TYPES = Literal["fields", "customs", "optionals", "excludes"]
|
|
7
9
|
SCHEMA_TYPES = Literal["In", "Out", "Patch", "Related"]
|
|
8
10
|
VIEW_TYPES = Literal["list", "retrieve", "create", "update", "delete", "all"]
|
|
9
|
-
|
|
11
|
+
JwtKeys: TypeAlias = jwk.RSAKey | jwk.ECKey | jwk.OctKey
|
|
10
12
|
|
|
11
13
|
class ModelSerializerType(type):
|
|
12
14
|
def __repr__(self):
|
|
@@ -9,28 +9,25 @@ from pydantic import create_model
|
|
|
9
9
|
|
|
10
10
|
from ninja_aio.schemas.helpers import ModelQuerySetSchema, QuerySchema, DecoratorsSchema
|
|
11
11
|
|
|
12
|
-
from .models import ModelSerializer, ModelUtil
|
|
13
|
-
from .schemas import (
|
|
12
|
+
from ninja_aio.models import ModelSerializer, ModelUtil
|
|
13
|
+
from ninja_aio.schemas import (
|
|
14
14
|
GenericMessageSchema,
|
|
15
15
|
M2MRelationSchema,
|
|
16
16
|
)
|
|
17
|
-
from .helpers.api import ManyToManyAPI
|
|
18
|
-
from .types import ModelSerializerMeta, VIEW_TYPES
|
|
19
|
-
from .decorators import unique_view, decorate_view
|
|
17
|
+
from ninja_aio.helpers.api import ManyToManyAPI
|
|
18
|
+
from ninja_aio.types import ModelSerializerMeta, VIEW_TYPES
|
|
19
|
+
from ninja_aio.decorators import unique_view, decorate_view
|
|
20
20
|
|
|
21
|
-
ERROR_CODES = frozenset({400, 401, 404
|
|
21
|
+
ERROR_CODES = frozenset({400, 401, 404})
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
class
|
|
25
|
-
api: NinjaAPI
|
|
26
|
-
router_tag: str
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -103,14 +160,14 @@ class APIViewSet:
|
|
|
103
160
|
filters = { "field_name": (type, default) }
|
|
104
161
|
A dynamic Pydantic Filters schema is generated and exposed as query params
|
|
105
162
|
on the related GET endpoint: /{pk}/{related_path}?field_name=value.
|
|
106
|
-
To apply custom filter logic implement an
|
|
163
|
+
To apply custom filter logic implement an hook named:
|
|
107
164
|
<related_name>_query_params_handler(self, queryset, filters_dict)
|
|
108
165
|
It receives the initial related queryset and the validated/dumped filters
|
|
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,
|
|
@@ -121,7 +178,7 @@ class APIViewSet:
|
|
|
121
178
|
)
|
|
122
179
|
]
|
|
123
180
|
|
|
124
|
-
|
|
181
|
+
def tags_query_params_handler(self, queryset, filters):
|
|
125
182
|
name_filter = filters.get("name")
|
|
126
183
|
if name_filter:
|
|
127
184
|
queryset = queryset.filter(name__icontains=name_filter)
|
|
@@ -145,11 +202,11 @@ class APIViewSet:
|
|
|
145
202
|
|
|
146
203
|
Overridable hooks:
|
|
147
204
|
views(): Register extra custom endpoints on self.router.
|
|
148
|
-
query_params_handler(queryset, filters): Async hook to apply list filters.
|
|
205
|
+
query_params_handler(queryset, filters): Sync/Async hook to apply list filters.
|
|
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
|
|
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__(
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -278,15 +340,18 @@ class APIViewSet:
|
|
|
278
340
|
|
|
279
341
|
def get_schemas(self):
|
|
280
342
|
"""
|
|
281
|
-
|
|
343
|
+
Compute and return (schema_out, schema_in, schema_update).
|
|
344
|
+
|
|
345
|
+
- If model is a ModelSerializer (ModelSerializerMeta), auto-generate read/create/update schemas.
|
|
346
|
+
- Otherwise, return the schemas already set on the viewset (may be None).
|
|
282
347
|
"""
|
|
283
|
-
if isinstance(self.model, ModelSerializerMeta):
|
|
284
|
-
return
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
)
|
|
289
|
-
|
|
348
|
+
if not isinstance(self.model, ModelSerializerMeta):
|
|
349
|
+
return self.schema_out, self.schema_in, self.schema_update
|
|
350
|
+
return (
|
|
351
|
+
self.schema_out or self.model.generate_read_s(),
|
|
352
|
+
self.schema_in or self.model.generate_create_s(),
|
|
353
|
+
self.schema_update or self.model.generate_update_s(),
|
|
354
|
+
)
|
|
290
355
|
|
|
291
356
|
async def query_params_handler(
|
|
292
357
|
self, queryset: QuerySet[ModelSerializer], filters: dict
|
|
@@ -447,8 +512,41 @@ class APIViewSet:
|
|
|
447
512
|
|
|
448
513
|
return self._set_additional_views()
|
|
449
514
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
515
|
+
|
|
516
|
+
class ReadOnlyViewSet(APIViewSet):
|
|
517
|
+
"""
|
|
518
|
+
ReadOnly viewset generating async List + Retrieve endpoints for a Django model.
|
|
519
|
+
|
|
520
|
+
Usage:
|
|
521
|
+
@api.viewset(model=MyModel)
|
|
522
|
+
class MyModelReadOnlyViewSet(ReadOnlyViewSet):
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
or
|
|
526
|
+
|
|
527
|
+
class MyModelReadOnlyViewSet(ReadOnlyViewSet):
|
|
528
|
+
model = MyModel
|
|
529
|
+
api = api
|
|
530
|
+
MyModelReadOnlyViewSet().add_views_to_route()
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
disable = ["create", "update", "delete"]
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class WriteOnlyViewSet(APIViewSet):
|
|
537
|
+
"""
|
|
538
|
+
WriteOnly viewset generating async Create + Update + Delete endpoints for a Django model.
|
|
539
|
+
|
|
540
|
+
Usage:
|
|
541
|
+
@api.viewset(model=MyModel)
|
|
542
|
+
class MyModelWriteOnlyViewSet(WriteOnlyViewSet):
|
|
543
|
+
pass
|
|
544
|
+
|
|
545
|
+
or
|
|
546
|
+
|
|
547
|
+
class MyModelWriteOnlyViewSet(WriteOnlyViewSet):
|
|
548
|
+
model = MyModel
|
|
549
|
+
api = api
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
disable = ["list", "retrieve"]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
from ninja_aio.views.api import APIViewSet
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IcontainsFilterViewSetMixin(APIViewSet):
|
|
5
|
+
"""
|
|
6
|
+
Mixin providing a convenience method to apply case-insensitive substring filters
|
|
7
|
+
to a Django queryset based on request query parameters.
|
|
8
|
+
|
|
9
|
+
This mixin is intended for use with viewsets that support asynchronous handling
|
|
10
|
+
and dynamic filtering. It converts string-type filter values into Django ORM
|
|
11
|
+
`__icontains` lookups, enabling partial, case-insensitive matches.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
- Include this mixin in a viewset class that exposes a queryset and
|
|
15
|
+
passes a dictionary of filters (e.g., from request query params) to
|
|
16
|
+
`query_params_handler`.
|
|
17
|
+
- Only string values are considered; non-string values are ignored.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
filters = {"name": "john", "email": "example.com", "age": 30}
|
|
21
|
+
# Resulting queryset filter:
|
|
22
|
+
# queryset.filter(name__icontains="john", email__icontains="example.com")
|
|
23
|
+
# Note: "age" is ignored because its value is not a string.
|
|
24
|
+
|
|
25
|
+
Apply `__icontains` filters to the provided queryset based on the given filter
|
|
26
|
+
dictionary.
|
|
27
|
+
|
|
28
|
+
Parameters:
|
|
29
|
+
queryset (django.db.models.QuerySet):
|
|
30
|
+
The base queryset to filter.
|
|
31
|
+
filters (dict[str, Any]):
|
|
32
|
+
A mapping of field names to desired filter values. Only entries with
|
|
33
|
+
string values will be transformed into `field__icontains=value`
|
|
34
|
+
filters. Non-string values are ignored.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
django.db.models.QuerySet:
|
|
38
|
+
A queryset filtered with `__icontains` lookups for all string-valued
|
|
39
|
+
keys in `filters`.
|
|
40
|
+
|
|
41
|
+
Notes:
|
|
42
|
+
- This method is asynchronous to align with async view workflows, but it
|
|
43
|
+
performs a synchronous queryset filter call typical in Django ORM usage.
|
|
44
|
+
- Ensure that the fields referenced in `filters` exist on the model
|
|
45
|
+
associated with `queryset`, otherwise a FieldError may be raised at
|
|
46
|
+
evaluation time.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
async def query_params_handler(self, queryset, filters):
|
|
50
|
+
"""
|
|
51
|
+
Apply icontains filter to the queryset based on provided filters.
|
|
52
|
+
"""
|
|
53
|
+
base_qs = await super().query_params_handler(queryset, filters)
|
|
54
|
+
return base_qs.filter(
|
|
55
|
+
**{
|
|
56
|
+
f"{key}__icontains": value
|
|
57
|
+
for key, value in filters.items()
|
|
58
|
+
if isinstance(value, str)
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BooleanFilterViewSetMixin(APIViewSet):
|
|
64
|
+
"""
|
|
65
|
+
Mixin providing boolean-based filtering for Django QuerySets.
|
|
66
|
+
|
|
67
|
+
This mixin defines a helper to apply boolean filters from a dictionary of query
|
|
68
|
+
parameters, selecting only those values that are strictly boolean (True/False)
|
|
69
|
+
and ignoring any non-boolean entries. It is intended to be used with viewsets
|
|
70
|
+
that expose queryable endpoints.
|
|
71
|
+
|
|
72
|
+
Methods:
|
|
73
|
+
query_params_handler(queryset, filters):
|
|
74
|
+
Apply boolean filters to the given queryset based on the provided
|
|
75
|
+
dictionary. Only keys with boolean values are included in the filter.
|
|
76
|
+
|
|
77
|
+
Parameters:
|
|
78
|
+
queryset (QuerySet): A Django QuerySet to be filtered.
|
|
79
|
+
filters (dict): A mapping of field names to potential filter values.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
QuerySet: A new QuerySet filtered by the boolean entries in `filters`.
|
|
83
|
+
|
|
84
|
+
Notes:
|
|
85
|
+
- Non-boolean values in `filters` are ignored.
|
|
86
|
+
- Keys should correspond to valid model fields or lookups.
|
|
87
|
+
- This method does not mutate the original queryset; it returns a
|
|
88
|
+
filtered clone.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
async def query_params_handler(self, queryset, filters):
|
|
92
|
+
"""
|
|
93
|
+
Apply boolean filter to the queryset based on provided filters.
|
|
94
|
+
"""
|
|
95
|
+
base_qs = await super().query_params_handler(queryset, filters)
|
|
96
|
+
return base_qs.filter(
|
|
97
|
+
**{key: value for key, value in filters.items() if isinstance(value, bool)}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class NumericFilterViewSetMixin(APIViewSet):
|
|
102
|
+
"""
|
|
103
|
+
Mixin providing numeric filtering for Django QuerySets.
|
|
104
|
+
|
|
105
|
+
This mixin defines a helper to apply numeric filters from a dictionary of query
|
|
106
|
+
parameters, selecting only those values that are of numeric type (int or float)
|
|
107
|
+
and ignoring any non-numeric entries. It is intended to be used with viewsets
|
|
108
|
+
that expose queryable endpoints.
|
|
109
|
+
|
|
110
|
+
Methods:
|
|
111
|
+
query_params_handler(queryset, filters):
|
|
112
|
+
Apply numeric filters to the given queryset based on the provided
|
|
113
|
+
dictionary. Only keys with numeric values are included in the filter.
|
|
114
|
+
|
|
115
|
+
Parameters:
|
|
116
|
+
queryset (QuerySet): A Django QuerySet to be filtered.
|
|
117
|
+
filters (dict): A mapping of field names to potential filter values.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
QuerySet: A new QuerySet filtered by the numeric entries in `filters`.
|
|
121
|
+
|
|
122
|
+
Notes:
|
|
123
|
+
- Non-numeric values in `filters` are ignored.
|
|
124
|
+
- Keys should correspond to valid model fields or lookups.
|
|
125
|
+
- This method does not mutate the original queryset; it returns a
|
|
126
|
+
filtered clone.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
async def query_params_handler(self, queryset, filters):
|
|
130
|
+
"""
|
|
131
|
+
Apply numeric filter to the queryset based on provided filters.
|
|
132
|
+
"""
|
|
133
|
+
base_qs = await super().query_params_handler(queryset, filters)
|
|
134
|
+
return base_qs.filter(
|
|
135
|
+
**{
|
|
136
|
+
key: value
|
|
137
|
+
for key, value in filters.items()
|
|
138
|
+
if isinstance(value, (int, float))
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class DateFilterViewSetMixin(APIViewSet):
|
|
144
|
+
"""
|
|
145
|
+
Mixin enabling date/datetime-based filtering for Django QuerySets.
|
|
146
|
+
|
|
147
|
+
Purpose:
|
|
148
|
+
- Apply dynamic date/datetime filters based on incoming query parameters.
|
|
149
|
+
- Support customizable comparison operators via `_compare_attr` (e.g., "__gt", "__lt", "__gte", "__lte").
|
|
150
|
+
|
|
151
|
+
Behavior:
|
|
152
|
+
- Filters only entries whose values implement `isoformat` (dates or datetimes).
|
|
153
|
+
- Builds lookups as "<field><_compare_attr>" with the provided value.
|
|
154
|
+
|
|
155
|
+
Attributes:
|
|
156
|
+
- _compare_attr (str): Django ORM comparison operator suffix to append to field names.
|
|
157
|
+
|
|
158
|
+
Notes:
|
|
159
|
+
- Ensure provided filter values are compatible with the target model fields.
|
|
160
|
+
- Subclasses should set `_compare_attr` to control comparison semantics.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
_compare_attr: str = ""
|
|
164
|
+
|
|
165
|
+
async def query_params_handler(self, queryset, filters):
|
|
166
|
+
"""
|
|
167
|
+
Apply date/datetime filters using `_compare_attr`.
|
|
168
|
+
|
|
169
|
+
- Delegates to `super().query_params_handler` first.
|
|
170
|
+
- Applies filters for keys whose values implement `isoformat`.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
- QuerySet filtered with lookups in the form: field<_compare_attr>=value.
|
|
174
|
+
"""
|
|
175
|
+
base_qs = await super().query_params_handler(queryset, filters)
|
|
176
|
+
return base_qs.filter(
|
|
177
|
+
**{
|
|
178
|
+
f"{key}{self._compare_attr}": value
|
|
179
|
+
for key, value in filters.items()
|
|
180
|
+
if hasattr(value, "isoformat")
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class GreaterDateFilterViewSetMixin(DateFilterViewSetMixin):
|
|
186
|
+
"""
|
|
187
|
+
Mixin that configures date filtering to return records with dates strictly greater than a given value.
|
|
188
|
+
This class extends DateFilterViewSetMixin and sets the internal comparison attribute to `__gt`,
|
|
189
|
+
ensuring that any date-based filtering uses the "greater than" operation.
|
|
190
|
+
Attributes:
|
|
191
|
+
_compare_attr (str): The Django ORM comparison operator used for filtering, set to "__gt".
|
|
192
|
+
Usage:
|
|
193
|
+
- Include this mixin in a ViewSet to filter queryset results where the specified date field is
|
|
194
|
+
greater than the provided date value.
|
|
195
|
+
- Typically used in endpoints that need to fetch items created/updated after a certain timestamp.
|
|
196
|
+
Example:
|
|
197
|
+
class MyViewSet(GreaterDateFilterViewSetMixin, ModelViewSet):
|
|
198
|
+
date_filter_field = "created_at"
|
|
199
|
+
...
|
|
200
|
+
# Filtering will apply: queryset.filter(created_at__gt=<provided_date>)
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
_compare_attr = "__gt"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class LessDateFilterViewSetMixin(DateFilterViewSetMixin):
|
|
207
|
+
"""
|
|
208
|
+
Mixin that configures date filtering to return records with dates strictly less than a given value.
|
|
209
|
+
This class extends DateFilterViewSetMixin and sets the internal comparison attribute to `__lt`,
|
|
210
|
+
ensuring that any date-based filtering uses the "less than" operation.
|
|
211
|
+
Attributes:
|
|
212
|
+
_compare_attr (str): The Django ORM comparison operator used for filtering, set to "__lt".
|
|
213
|
+
Usage:
|
|
214
|
+
- Include this mixin in a ViewSet to filter queryset results where the specified date field is
|
|
215
|
+
less than the provided date value.
|
|
216
|
+
- Typically used in endpoints that need to fetch items created/updated before a certain timestamp.
|
|
217
|
+
Example:
|
|
218
|
+
class MyViewSet(LessDateFilterViewSetMixin, ModelViewSet):
|
|
219
|
+
date_filter_field = "created_at"
|
|
220
|
+
...
|
|
221
|
+
# Filtering will apply: queryset.filter(created_at__lt=<provided_date>)
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
_compare_attr = "__lt"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class GreaterEqualDateFilterViewSetMixin(DateFilterViewSetMixin):
|
|
228
|
+
"""
|
|
229
|
+
Mixin for date-based filtering that uses a "greater than or equal to" comparison.
|
|
230
|
+
|
|
231
|
+
This mixin extends DateFilterViewSetMixin by setting the internal comparison
|
|
232
|
+
attribute to "__gte", enabling querysets to be filtered to include records whose
|
|
233
|
+
date or datetime fields are greater than or equal to the provided value.
|
|
234
|
+
|
|
235
|
+
Intended Use:
|
|
236
|
+
- Apply to Django Ninja or DRF viewsets that support date filtering.
|
|
237
|
+
- Combine with DateFilterViewSetMixin to standardize date filter behavior.
|
|
238
|
+
|
|
239
|
+
Behavior:
|
|
240
|
+
- When a date filter parameter is present, the queryset is filtered using
|
|
241
|
+
Django's "__gte" lookup, e.g., MyModel.objects.filter(created_at__gte=value).
|
|
242
|
+
|
|
243
|
+
Attributes:
|
|
244
|
+
- _compare_attr: str
|
|
245
|
+
The Django ORM comparison operator used for filtering; set to "__gte".
|
|
246
|
+
|
|
247
|
+
Notes:
|
|
248
|
+
- Ensure the target field and provided filter value are compatible (date or datetime).
|
|
249
|
+
- Timezone-aware comparisons should be handled consistently within the project settings.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
_compare_attr = "__gte"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class LessEqualDateFilterViewSetMixin(DateFilterViewSetMixin):
|
|
256
|
+
"""
|
|
257
|
+
Mixin for date-based filtering that uses a "less than or equal to" comparison.
|
|
258
|
+
This mixin extends DateFilterViewSetMixin by setting the internal comparison
|
|
259
|
+
attribute to "__lte", enabling querysets to be filtered to include records whose
|
|
260
|
+
date or datetime fields are less than or equal to the provided value.
|
|
261
|
+
Intended Use:
|
|
262
|
+
- Apply to Django Ninja or DRF viewsets that support date filtering.
|
|
263
|
+
- Combine with DateFilterViewSetMixin to standardize date filter behavior.
|
|
264
|
+
Behavior:
|
|
265
|
+
- When a date filter parameter is present, the queryset is filtered using
|
|
266
|
+
Django's "__lte" lookup, e.g., MyModel.objects.filter(created_at__lte=value).
|
|
267
|
+
Attributes:
|
|
268
|
+
- _compare_attr: str
|
|
269
|
+
The Django ORM comparison operator used for filtering; set to "__lte".
|
|
270
|
+
Notes:
|
|
271
|
+
- Ensure the target field and provided filter value are compatible (date or datetime).
|
|
272
|
+
- Timezone-aware comparisons should be handled consistently within the project settings.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
_compare_attr = "__lte"
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
ninja_aio/__init__.py,sha256=b0K4gr4xA4QngOZ7PrtTuDcxgyk3yFTNwNxsCObFqug,123
|
|
2
|
-
ninja_aio/api.py,sha256=SS1TYUiFkdYjfJLVy6GI90GOzvIHzPEeL-UcqWFRHkM,1684
|
|
3
|
-
ninja_aio/auth.py,sha256=8jaEp7oEJvUUB9EuyE2fOYk-khyAaekT3i80E7AbgOA,5101
|
|
4
|
-
ninja_aio/decorators.py,sha256=BHoFIiqdIVMFqSxGh-F6WeZFo1xZK4ieDw3dzKfxZIM,8147
|
|
5
|
-
ninja_aio/exceptions.py,sha256=1-iRbrloIyi0CR6Tcrn5YR4_LloA7PPohKIBaxXJ0-8,2596
|
|
6
|
-
ninja_aio/models.py,sha256=aJlo5a64O4o-fB8QESLMUJpoA5kcjRJxPBiAIMxg46k,47652
|
|
7
|
-
ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
|
|
8
|
-
ninja_aio/renders.py,sha256=VtmSliRJyZ6gjyoib8AXMVUBYF1jPNsiceCHujI_mAs,1699
|
|
9
|
-
ninja_aio/types.py,sha256=TJSGlA7bt4g9fvPhJ7gzH5tKbLagPmZUzfgttEOp4xs,468
|
|
10
|
-
ninja_aio/views.py,sha256=8vMFw-8au9O0Hpf-79jvB47PwokCzm7P8UY0DDWpN5A,16786
|
|
11
|
-
ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
ninja_aio/helpers/api.py,sha256=kgHxYPfbV6kQbp8Z4qMwQtwFiFIcRAenxI9MDowGtis,20181
|
|
13
|
-
ninja_aio/helpers/query.py,sha256=tE8RjXvSig-WB_0LRQ0LqoE4G_HMHsu0Na5QzTNIm6U,4262
|
|
14
|
-
ninja_aio/schemas/__init__.py,sha256=iLBwHg0pmL9k_UkIui5Q8QIl_gO4fgxSv2JHxDzqnSI,549
|
|
15
|
-
ninja_aio/schemas/api.py,sha256=-VwXhBRhmMsZLIAmWJ-P7tB5klxXS75eukjabeKKYsc,360
|
|
16
|
-
ninja_aio/schemas/generics.py,sha256=frjJsKJMAdM_NdNKv-9ddZNGxYy5PNzjIRGtuycgr-w,112
|
|
17
|
-
ninja_aio/schemas/helpers.py,sha256=rmE0D15lJg95Unv8PU44Hbf0VDTcErMCZZFG3D_znTo,2823
|
|
18
|
-
django_ninja_aio_crud-2.0.0rc7.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
|
|
19
|
-
django_ninja_aio_crud-2.0.0rc7.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
20
|
-
django_ninja_aio_crud-2.0.0rc7.dist-info/METADATA,sha256=bxFgTP1QMN1oSLPw4fhV3Nu9-U90pNN92FoiVqJq6hY,8675
|
|
21
|
-
django_ninja_aio_crud-2.0.0rc7.dist-info/RECORD,,
|
|
File without changes
|
{django_ninja_aio_crud-2.0.0rc7.dist-info → django_ninja_aio_crud-2.2.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|