django-ninja-aio-crud 2.0.0rc7__py3-none-any.whl → 2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.0.0rc7
3
+ Version: 2.1.0
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10
@@ -1,21 +1,23 @@
1
- ninja_aio/__init__.py,sha256=b0K4gr4xA4QngOZ7PrtTuDcxgyk3yFTNwNxsCObFqug,123
1
+ ninja_aio/__init__.py,sha256=YGWehZQ4d5yVFCd1ax-s61BSkND60Yh383YmUJq-LtY,119
2
2
  ninja_aio/api.py,sha256=SS1TYUiFkdYjfJLVy6GI90GOzvIHzPEeL-UcqWFRHkM,1684
3
- ninja_aio/auth.py,sha256=8jaEp7oEJvUUB9EuyE2fOYk-khyAaekT3i80E7AbgOA,5101
3
+ ninja_aio/auth.py,sha256=w9TXDQwJSZzCbncsSwemINTUe1IPZGHnLKiqWTeOoFA,9163
4
4
  ninja_aio/decorators.py,sha256=BHoFIiqdIVMFqSxGh-F6WeZFo1xZK4ieDw3dzKfxZIM,8147
5
5
  ninja_aio/exceptions.py,sha256=1-iRbrloIyi0CR6Tcrn5YR4_LloA7PPohKIBaxXJ0-8,2596
6
6
  ninja_aio/models.py,sha256=aJlo5a64O4o-fB8QESLMUJpoA5kcjRJxPBiAIMxg46k,47652
7
7
  ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
8
8
  ninja_aio/renders.py,sha256=VtmSliRJyZ6gjyoib8AXMVUBYF1jPNsiceCHujI_mAs,1699
9
- ninja_aio/types.py,sha256=TJSGlA7bt4g9fvPhJ7gzH5tKbLagPmZUzfgttEOp4xs,468
10
- ninja_aio/views.py,sha256=8vMFw-8au9O0Hpf-79jvB47PwokCzm7P8UY0DDWpN5A,16786
9
+ ninja_aio/types.py,sha256=_QV0DySZhCV1otuh1zhrlHgCaFnr5fd64GQrcA-qKoc,564
11
10
  ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- ninja_aio/helpers/api.py,sha256=kgHxYPfbV6kQbp8Z4qMwQtwFiFIcRAenxI9MDowGtis,20181
11
+ ninja_aio/helpers/api.py,sha256=BTe7OL-X7YgWYeXmka8TmN4-gA43FVZhtH7q0dRjYX0,20238
13
12
  ninja_aio/helpers/query.py,sha256=tE8RjXvSig-WB_0LRQ0LqoE4G_HMHsu0Na5QzTNIm6U,4262
14
13
  ninja_aio/schemas/__init__.py,sha256=iLBwHg0pmL9k_UkIui5Q8QIl_gO4fgxSv2JHxDzqnSI,549
15
14
  ninja_aio/schemas/api.py,sha256=-VwXhBRhmMsZLIAmWJ-P7tB5klxXS75eukjabeKKYsc,360
16
15
  ninja_aio/schemas/generics.py,sha256=frjJsKJMAdM_NdNKv-9ddZNGxYy5PNzjIRGtuycgr-w,112
17
16
  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,,
17
+ ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
18
+ ninja_aio/views/api.py,sha256=ispbr5w6WCUdbXmn2LY94zDQ0qb2nDNeTLLNAUBFoDc,17736
19
+ ninja_aio/views/mixins.py,sha256=Jh6BG8Cs823nurVlODlzCquTxKrLH7Pmo5udPqUGZek,11378
20
+ django_ninja_aio_crud-2.1.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
21
+ django_ninja_aio_crud-2.1.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
22
+ django_ninja_aio_crud-2.1.0.dist-info/METADATA,sha256=91kIanSvlN_aeRmhZuXZR2pIBXEzUUOuYLJkijzN7QI,8672
23
+ django_ninja_aio_crud-2.1.0.dist-info/RECORD,,
ninja_aio/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.0.0-rc7"
3
+ __version__ = "2.1.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
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 RSA public key (JWK format) used to verify the JWT signature.
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: jwk.RSAKey
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)):
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: jwk.RSAKey = 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: jwk.RSAKey | jwk.ECKey = 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
- @unique_view(f"get_{self.related_model_util.model_name}_{rel_path}")
361
- @paginate(self.pagination_class)
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
10
12
 
11
13
  class ModelSerializerType(type):
12
14
  def __repr__(self):
@@ -0,0 +1,3 @@
1
+ from .api import APIView, APIViewSet, ReadOnlyViewSet, WriteOnlyViewSet
2
+
3
+ __all__ = ["APIView", "APIViewSet", "ReadOnlyViewSet", "WriteOnlyViewSet"]
@@ -9,14 +9,14 @@ 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
21
  ERROR_CODES = frozenset({400, 401, 404, 428})
22
22
 
@@ -103,7 +103,7 @@ class APIViewSet:
103
103
  filters = { "field_name": (type, default) }
104
104
  A dynamic Pydantic Filters schema is generated and exposed as query params
105
105
  on the related GET endpoint: /{pk}/{related_path}?field_name=value.
106
- To apply custom filter logic implement an async hook named:
106
+ To apply custom filter logic implement an hook named:
107
107
  <related_name>_query_params_handler(self, queryset, filters_dict)
108
108
  It receives the initial related queryset and the validated/dumped filters
109
109
  dict, and must return the (optionally) filtered queryset.
@@ -121,7 +121,7 @@ class APIViewSet:
121
121
  )
122
122
  ]
123
123
 
124
- async def tags_query_params_handler(self, queryset, filters):
124
+ def tags_query_params_handler(self, queryset, filters):
125
125
  name_filter = filters.get("name")
126
126
  if name_filter:
127
127
  queryset = queryset.filter(name__icontains=name_filter)
@@ -145,7 +145,7 @@ class APIViewSet:
145
145
 
146
146
  Overridable hooks:
147
147
  views(): Register extra custom endpoints on self.router.
148
- query_params_handler(queryset, filters): Async hook to apply list filters.
148
+ query_params_handler(queryset, filters): Sync/Async hook to apply list filters.
149
149
  <related_name>_query_params_handler(queryset, filters): Async hook for per-M2M filtering.
150
150
 
151
151
  Error responses:
@@ -278,15 +278,18 @@ class APIViewSet:
278
278
 
279
279
  def get_schemas(self):
280
280
  """
281
- Return (schema_out, schema_in, schema_update), generating them if model is a ModelSerializer.
281
+ Compute and return (schema_out, schema_in, schema_update).
282
+
283
+ - If model is a ModelSerializer (ModelSerializerMeta), auto-generate read/create/update schemas.
284
+ - Otherwise, return the schemas already set on the viewset (may be None).
282
285
  """
283
- if isinstance(self.model, ModelSerializerMeta):
284
- return (
285
- self.model.generate_read_s(),
286
- self.model.generate_create_s(),
287
- self.model.generate_update_s(),
288
- )
289
- return self.schema_out, self.schema_in, self.schema_update
286
+ if not isinstance(self.model, ModelSerializerMeta):
287
+ return self.schema_out, self.schema_in, self.schema_update
288
+ return (
289
+ self.schema_out or self.model.generate_read_s(),
290
+ self.schema_in or self.model.generate_create_s(),
291
+ self.schema_update or self.model.generate_update_s(),
292
+ )
290
293
 
291
294
  async def query_params_handler(
292
295
  self, queryset: QuerySet[ModelSerializer], filters: dict
@@ -452,3 +455,31 @@ class APIViewSet:
452
455
  Attach router with registered endpoints to the NinjaAPI instance.
453
456
  """
454
457
  return self.api.add_router(f"{self.api_route_path}", self._add_views())
458
+
459
+
460
+ class ReadOnlyViewSet(APIViewSet):
461
+ """
462
+ ReadOnly viewset generating async List + Retrieve endpoints for a Django model.
463
+
464
+ Usage:
465
+ class MyModelReadOnlyViewSet(ReadOnlyViewSet):
466
+ model = MyModel
467
+ api = api
468
+ MyModelReadOnlyViewSet().add_views_to_route()
469
+ """
470
+
471
+ disable = ["create", "update", "delete"]
472
+
473
+
474
+ class WriteOnlyViewSet(APIViewSet):
475
+ """
476
+ WriteOnly viewset generating async Create + Update + Delete endpoints for a Django model.
477
+
478
+ Usage:
479
+ class MyModelWriteOnlyViewSet(WriteOnlyViewSet):
480
+ model = MyModel
481
+ api = api
482
+ MyModelWriteOnlyViewSet().add_views_to_route()
483
+ """
484
+
485
+ 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"