djresttoolkit 1.0.0__py3-none-any.whl → 1.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.
- djresttoolkit/pagination/_paginated_data_builder.py +6 -4
- djresttoolkit/serializers/__init__.py +3 -0
- djresttoolkit/serializers/_enhanced_model_serializer.py +59 -0
- djresttoolkit/views/mixins/__init__.py +2 -5
- djresttoolkit/views/mixins/_retrieve_object_mixin.py +28 -10
- {djresttoolkit-1.0.0.dist-info → djresttoolkit-1.2.0.dist-info}/METADATA +30 -7
- {djresttoolkit-1.0.0.dist-info → djresttoolkit-1.2.0.dist-info}/RECORD +10 -9
- {djresttoolkit-1.0.0.dist-info → djresttoolkit-1.2.0.dist-info}/WHEEL +0 -0
- {djresttoolkit-1.0.0.dist-info → djresttoolkit-1.2.0.dist-info}/entry_points.txt +0 -0
- {djresttoolkit-1.0.0.dist-info → djresttoolkit-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,13 @@
|
|
1
1
|
import logging
|
2
2
|
from typing import Any
|
3
3
|
|
4
|
-
from django.db.models import QuerySet
|
4
|
+
from django.db.models import Model, QuerySet
|
5
5
|
from rest_framework.exceptions import NotFound
|
6
6
|
from rest_framework.request import Request
|
7
7
|
from rest_framework.serializers import BaseSerializer
|
8
|
-
|
8
|
+
|
9
|
+
from djresttoolkit.serializers import EnhancedModelSerializer
|
10
|
+
|
9
11
|
from ._page_number_pagination import PageNumberPagination
|
10
12
|
|
11
13
|
# Get logger from logging.
|
@@ -18,7 +20,7 @@ class PaginatedDataBuilder[T: Model]:
|
|
18
20
|
def __init__(
|
19
21
|
self,
|
20
22
|
request: Request,
|
21
|
-
serializer_class: type[BaseSerializer[T]],
|
23
|
+
serializer_class: type[BaseSerializer[T] | EnhancedModelSerializer[T]],
|
22
24
|
queryset: QuerySet[T],
|
23
25
|
) -> None:
|
24
26
|
"""Initilize the PaginatedDataBuilder class."""
|
@@ -49,7 +51,7 @@ class PaginatedDataBuilder[T: Model]:
|
|
49
51
|
)
|
50
52
|
|
51
53
|
# Construct the paginated response
|
52
|
-
paginated_data = {
|
54
|
+
paginated_data: dict[str, Any] = {
|
53
55
|
"page": {
|
54
56
|
"current": paginator.page.number, # type: ignore
|
55
57
|
"total": paginator.page.paginator.num_pages, # type: ignore
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from copy import deepcopy
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from django.db.models import Field as DjangoField
|
7
|
+
from django.db.models import Model
|
8
|
+
from rest_framework.serializers import Field as DrfField
|
9
|
+
from rest_framework.serializers import ModelSerializer
|
10
|
+
from rest_framework.utils.model_meta import RelationInfo
|
11
|
+
|
12
|
+
|
13
|
+
class EnhancedModelSerializer[T: Model](ModelSerializer[Model]):
|
14
|
+
"""
|
15
|
+
A DRF ModelSerializer that automatically applies Django model field
|
16
|
+
`error_messages` unless explicitly overridden in the serializer.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def _merge_error_messages(
|
20
|
+
self,
|
21
|
+
field_kwargs: dict[str, Any],
|
22
|
+
model_field: DjangoField[Any, Any] | None,
|
23
|
+
) -> dict[str, Any]:
|
24
|
+
"""Safely merge model field error_messages with serializer kwargs."""
|
25
|
+
model_errors: dict[str, str] | None = getattr(
|
26
|
+
model_field, "error_messages", None
|
27
|
+
)
|
28
|
+
if model_errors:
|
29
|
+
existing: dict[str, str] = field_kwargs.get("error_messages", {})
|
30
|
+
field_kwargs["error_messages"] = {**deepcopy(model_errors), **existing}
|
31
|
+
return field_kwargs
|
32
|
+
|
33
|
+
def build_standard_field(
|
34
|
+
self,
|
35
|
+
field_name: str,
|
36
|
+
model_field: DjangoField[Any, Any],
|
37
|
+
) -> tuple[type[DrfField[Any, Any, Any, Any]], dict[str, Any]]:
|
38
|
+
field_class, field_kwargs = super().build_standard_field( # type: ignore
|
39
|
+
field_name,
|
40
|
+
model_field,
|
41
|
+
)
|
42
|
+
return field_class, self._merge_error_messages(
|
43
|
+
field_kwargs,
|
44
|
+
model_field,
|
45
|
+
) # type: ignore
|
46
|
+
|
47
|
+
def build_relational_field(
|
48
|
+
self,
|
49
|
+
field_name: str,
|
50
|
+
relation_info: RelationInfo,
|
51
|
+
) -> tuple[type[DrfField[Any, Any, Any, Any]], dict[str, Any]]:
|
52
|
+
field_class, field_kwargs = super().build_relational_field( # type: ignore
|
53
|
+
field_name,
|
54
|
+
relation_info,
|
55
|
+
)
|
56
|
+
return field_class, self._merge_error_messages(
|
57
|
+
field_kwargs,
|
58
|
+
relation_info.model_field, # type: ignore
|
59
|
+
)
|
@@ -1,19 +1,20 @@
|
|
1
1
|
from typing import Any
|
2
|
-
from django.db.models import Model, QuerySet
|
3
|
-
|
4
|
-
|
5
|
-
class QuerysetNotDefinedError(Exception):
|
6
|
-
"""Exception raised when the `queryset` attribute is not set in the class."""
|
7
2
|
|
8
|
-
|
3
|
+
from django.core.exceptions import ImproperlyConfigured
|
4
|
+
from django.db.models import Model, QuerySet
|
5
|
+
from django.http import Http404
|
9
6
|
|
10
7
|
|
11
8
|
class RetrieveObjectMixin[T: Model]:
|
12
9
|
"""
|
13
|
-
|
10
|
+
Retrieve a single model object by filters.
|
14
11
|
|
15
12
|
Requires the `queryset` attribute to be set in the class that inherits this mixin.
|
16
13
|
|
14
|
+
Raises `Http404` when the object is missing.
|
15
|
+
|
16
|
+
This works in both Django views and DRF views.
|
17
|
+
|
17
18
|
Example:
|
18
19
|
```
|
19
20
|
class MyView(RetrieveModelMixin[Book], APIView):
|
@@ -27,15 +28,32 @@ class RetrieveObjectMixin[T: Model]:
|
|
27
28
|
|
28
29
|
queryset: QuerySet[T] | None = None
|
29
30
|
|
30
|
-
def get_object(self, **filters: Any) -> T
|
31
|
+
def get_object(self, **filters: Any) -> T:
|
31
32
|
"""Retrieve a model object based on provided filters."""
|
32
33
|
|
33
34
|
if self.queryset is None:
|
34
|
-
raise
|
35
|
+
raise ImproperlyConfigured(
|
35
36
|
"Queryset attribute is not set in the class.",
|
36
37
|
)
|
37
38
|
|
38
39
|
try:
|
39
40
|
return self.queryset.get(**filters)
|
40
41
|
except self.queryset.model.DoesNotExist:
|
41
|
-
|
42
|
+
raise Http404(self.not_found_detail())
|
43
|
+
|
44
|
+
def not_found_detail(self) -> dict[str, str] | str:
|
45
|
+
"""
|
46
|
+
Hook for customizing the 404 message.
|
47
|
+
Can be overridden per view.
|
48
|
+
"""
|
49
|
+
|
50
|
+
if self.queryset is None:
|
51
|
+
raise ImproperlyConfigured(
|
52
|
+
"Queryset attribute is not set in the class.",
|
53
|
+
)
|
54
|
+
|
55
|
+
verbose_name = self.queryset.model._meta.verbose_name
|
56
|
+
model_name = (
|
57
|
+
verbose_name.title() if verbose_name else self.queryset.model.__name__
|
58
|
+
)
|
59
|
+
return f"The requested {model_name} was not found."
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: djresttoolkit
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.2.0
|
4
4
|
Summary: A collection of Django and DRF utilities to simplify API development.
|
5
5
|
Project-URL: Homepage, https://github.com/shaileshpandit141/djresttoolkit
|
6
6
|
Project-URL: Documentation, https://shaileshpandit141.github.io/djresttoolkit
|
@@ -240,7 +240,7 @@ or
|
|
240
240
|
Flushed 120 records from all models and reset IDs.
|
241
241
|
```
|
242
242
|
|
243
|
-
###
|
243
|
+
### 3. BaseEnvConfig — API Reference
|
244
244
|
|
245
245
|
```python
|
246
246
|
from djresttoolkit.envconfig import BaseEnvConfig
|
@@ -801,6 +801,7 @@ Retrieve a single model object using the provided filter criteria.
|
|
801
801
|
|
802
802
|
```python
|
803
803
|
from rest_framework.views import APIView
|
804
|
+
from rest_framework.response import Respone
|
804
805
|
from django.http import JsonResponse
|
805
806
|
from myapp.models import Book
|
806
807
|
from djresttoolkit.mixins import RetrieveObjectMixin
|
@@ -808,17 +809,18 @@ from djresttoolkit.mixins import RetrieveObjectMixin
|
|
808
809
|
class BookDetailView(RetrieveObjectMixin[Book], APIView):
|
809
810
|
queryset = Book.objects.all()
|
810
811
|
|
811
|
-
def
|
812
|
+
def not_found_detail(self) -> dict[str, str] | str:
|
813
|
+
return "The requested Book was not found."
|
814
|
+
|
815
|
+
def get(self, request, *args, **kwargs) -> Respone:
|
812
816
|
book = self.get_object(id=kwargs["id"])
|
813
|
-
|
814
|
-
return JsonResponse({"title": book.title, "author": book.author})
|
815
|
-
return JsonResponse({"detail": "Not found"}, status=404)
|
817
|
+
return Respone({"title": book.title, "author": book.author})
|
816
818
|
```
|
817
819
|
|
818
820
|
#### Features of Retrieve Object Mixin
|
819
821
|
|
820
822
|
- Simplifies object retrieval in class-based views or DRF views.
|
821
|
-
-
|
823
|
+
- Raise `http404` if requested resource does not extst.
|
822
824
|
- Works with any Django model and queryset.
|
823
825
|
|
824
826
|
### 13. build_absolute_uri — API Reference
|
@@ -1079,6 +1081,27 @@ class BookViewSet(CacheInvalidateMixin, ModelViewSet):
|
|
1079
1081
|
- Invalidates caches when books are created, updated, or deleted.
|
1080
1082
|
- Supports custom cache keys per action.
|
1081
1083
|
|
1084
|
+
### 17. EnhancedModelSerializer — API Reference
|
1085
|
+
|
1086
|
+
A subclass of Django REST Framework’s `ModelSerializer` that automatically merges Django model field `error_messages` into the serializer field, unless explicitly overridden.
|
1087
|
+
This helps maintain consistent validation messages between the model and the serializer.
|
1088
|
+
|
1089
|
+
#### Type Parameters
|
1090
|
+
|
1091
|
+
- `T` (`Model`): The Django model type that the serializer corresponds to.
|
1092
|
+
|
1093
|
+
#### Example of EnhancedModelSerializer
|
1094
|
+
|
1095
|
+
```python
|
1096
|
+
from myapp.models import Book
|
1097
|
+
from myapp.serializers import EnhancedModelSerializer
|
1098
|
+
|
1099
|
+
class BookSerializer(EnhancedModelSerializer[Book]):
|
1100
|
+
class Meta:
|
1101
|
+
model = Book
|
1102
|
+
fields = "__all__"
|
1103
|
+
```
|
1104
|
+
|
1082
1105
|
## 🛠️ Planned Features
|
1083
1106
|
|
1084
1107
|
- Add more utils
|
@@ -32,10 +32,11 @@ djresttoolkit/models/mixins/__init__.py,sha256=MHwv36f3nHwI0bXeejuO7MTYuV93ln2tS
|
|
32
32
|
djresttoolkit/models/mixins/_model_choice_fields_mixin.py,sha256=9FZbe3PwrtIUZYGQh1gcOix5bfeyvKEOaNmkemvZX8E,2843
|
33
33
|
djresttoolkit/pagination/__init__.py,sha256=lQhyyX381RbWBsYV9Os3OQIbY7Z6aouL0QE5kI_u5SU,176
|
34
34
|
djresttoolkit/pagination/_page_number_pagination.py,sha256=NHPdMZfmTurKLdgpMBT2usTiGAoZMyA3dYXq_n11y34,2358
|
35
|
-
djresttoolkit/pagination/_paginated_data_builder.py,sha256=
|
35
|
+
djresttoolkit/pagination/_paginated_data_builder.py,sha256=_HjWbh0kUsn1WswONhqFI4dPCog3foqFQryLpmGarD8,2320
|
36
36
|
djresttoolkit/renderers/__init__.py,sha256=kmFMPRiMfD8CuJTN1_-6Z_Hqil3x8GBM0IN1roZESm0,107
|
37
37
|
djresttoolkit/renderers/_throttle_info_json_renderer.py,sha256=aP2cN4cB_Imcpy732zsPBQrMQqcKEs5R3dld5Y_4AMU,1089
|
38
|
-
djresttoolkit/serializers/__init__.py,sha256=
|
38
|
+
djresttoolkit/serializers/__init__.py,sha256=367CLluj8C15Zpr7BD-euP4DxKbwepm58gnGGcuYOJU,103
|
39
|
+
djresttoolkit/serializers/_enhanced_model_serializer.py,sha256=XNYM-zy5_ecfUNRtmBfaDKSuxEDhmoBV9HNx8iSaDpE,2055
|
39
40
|
djresttoolkit/serializers/mixins/__init__.py,sha256=dRT0kXDckOkZo1RQHrT1gXbGFMIv5M8TBHGF2uF-81Q,225
|
40
41
|
djresttoolkit/serializers/mixins/_absolute_url_file_mixin.py,sha256=5ewael0_RsJZ9b36IfXacxjb-Vx1eQ9Dk6dWuj5D_dc,3261
|
41
42
|
djresttoolkit/serializers/mixins/_bulk_create_mixin.py,sha256=9ZWm2MNaZOhmhKlWOu6VECtlDbUtaPeceGHmivDYwYQ,3248
|
@@ -48,10 +49,10 @@ djresttoolkit/views/_apiviews/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
48
49
|
djresttoolkit/views/_apiviews/_choice_fields_apiview.py,sha256=zABPgqxMVaWd814B_sC64bWL61fDJkyYQZmJXQCa6Xc,1395
|
49
50
|
djresttoolkit/views/_exceptions/__init__.py,sha256=DrCUxuPNyBR4WhzNutn5HDxLa--q51ykIxSG7_bFsOI,83
|
50
51
|
djresttoolkit/views/_exceptions/_exception_handler.py,sha256=_o7If47bzWLl57LeSXSWsIDsJGo2RIpwYAwNQ-hsHVY,2839
|
51
|
-
djresttoolkit/views/mixins/__init__.py,sha256=
|
52
|
-
djresttoolkit/views/mixins/_retrieve_object_mixin.py,sha256=
|
53
|
-
djresttoolkit-1.
|
54
|
-
djresttoolkit-1.
|
55
|
-
djresttoolkit-1.
|
56
|
-
djresttoolkit-1.
|
57
|
-
djresttoolkit-1.
|
52
|
+
djresttoolkit/views/mixins/__init__.py,sha256=mHD49OUxuJ9v81tGfM0hLnUJuJlYi7E-5cTVdplh-vs,91
|
53
|
+
djresttoolkit/views/mixins/_retrieve_object_mixin.py,sha256=v7CQDUkRWjtevFZnAYRBdDl7wcfYWF3evWoKWHAcckA,1749
|
54
|
+
djresttoolkit-1.2.0.dist-info/METADATA,sha256=ZLrUSwjbWip4-Rh6Mrkc2sD2sha_AHF8G-cR39VdiOo,32552
|
55
|
+
djresttoolkit-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
56
|
+
djresttoolkit-1.2.0.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
|
57
|
+
djresttoolkit-1.2.0.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
|
58
|
+
djresttoolkit-1.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|