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.
@@ -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
- from django.db.models import Model
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,3 @@
1
+ from ._enhanced_model_serializer import EnhancedModelSerializer
2
+
3
+ __all__ = ["EnhancedModelSerializer"]
@@ -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,6 +1,3 @@
1
- from ._retrieve_object_mixin import RetrieveObjectMixin, QuerysetNotDefinedError
1
+ from ._retrieve_object_mixin import RetrieveObjectMixin
2
2
 
3
- __all__ = [
4
- "RetrieveObjectMixin",
5
- "QuerysetNotDefinedError",
6
- ]
3
+ __all__ = ["RetrieveObjectMixin"]
@@ -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
- pass
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
- Mixin to provide a method for retrieving a single model object by filters.
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 | None:
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 QuerysetNotDefinedError(
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
- return None
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.0.0
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
- ### 3. BaseEnvConfig — API Reference
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 get(self, request, *args, **kwargs):
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
- if book:
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
- - Returns `None` instead of raising `DoesNotExist`, making error handling easier.
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=N4JaJwmfmC2NiC8MqOpkxV8-itKTueaOdawnv_it4bU,2239
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=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
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=K-1tk5d8tCVViMynw5DdffJ3Oo5uHpEx32E3_4X2UxM,154
52
- djresttoolkit/views/mixins/_retrieve_object_mixin.py,sha256=Q9znYPb07YXXUhsL7VIrk3BC-zDwjOhwLJKe2GPJ-k0,1155
53
- djresttoolkit-1.0.0.dist-info/METADATA,sha256=pyiY9lXJeoLC9xHgZSuE4f72eRkugl0Zkt00hWaDrSU,31832
54
- djresttoolkit-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
55
- djresttoolkit-1.0.0.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
56
- djresttoolkit-1.0.0.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
57
- djresttoolkit-1.0.0.dist-info/RECORD,,
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,,