djresttoolkit 0.16.1__py3-none-any.whl → 0.17.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.
README.md CHANGED
@@ -54,6 +54,9 @@ djresttoolkit is a collection of utilities and helpers for Django and Django RES
54
54
  - **PaginatedDataBuilder**
55
55
  Builder that combines `PageNumberPagination` + serializers to return standardized paginated responses with `"page"` + `"results"`.
56
56
 
57
+ - **Caching Mixins**
58
+ This module provides a set of DRF mixins to handle caching for `list`, `retrieve`, and `custom actions` with automatic invalidation on create, update, and destroy.
59
+
57
60
  ## 📦 Installation
58
61
 
59
62
  - **By using uv:**
@@ -254,7 +257,7 @@ print(settings.database_url)
254
257
  #### Features
255
258
 
256
259
  - Prioritizes `.env` variables over YAML.
257
- - Supports nested keys: `DATABASE__HOST` `settings.database.host`.
260
+ - Supports nested keys: `DATABASE__HOST`:- `settings.database.host`.
258
261
  - Designed to be subclassed for project-specific settings.
259
262
 
260
263
  ### 4. EmailSender — API Reference
@@ -870,17 +873,17 @@ from djresttoolkit.pagination import PageNumberPagination
870
873
  - Clients can control items per page using `?page-size=`.
871
874
  - Structured pagination metadata:
872
875
 
873
- - `current` current page number
874
- - `total` total number of pages
875
- - `size` number of items per page
876
- - `total_items` total number of items across all pages
877
- - `next` next page URL
878
- - `previous` previous page URL
876
+ - `current`:- current page number
877
+ - `total`:- total number of pages
878
+ - `size`:- number of items per page
879
+ - `total_items`:- total number of items across all pages
880
+ - `next`:- next page URL
881
+ - `previous`:- previous page URL
879
882
  - Standardized API response format.
880
883
 
881
884
  ### Attributes of Page Number Pagination
882
885
 
883
- - `page_size_query_param: str` Query parameter name (`"page-size"`).
886
+ - `page_size_query_param: str`:- Query parameter name (`"page-size"`).
884
887
 
885
888
  ### Page Number Pagination Methods
886
889
 
@@ -925,8 +928,8 @@ from djresttoolkit.pagination import PaginatedDataBuilder
925
928
  - Integrates with **DRF serializers**.
926
929
  - Handles **invalid pages** gracefully by raising `NotFound`.
927
930
  - Returns both:
928
- - `"page"` pagination metadata
929
- - `"results"` serialized data.
931
+ - `"page"`:- pagination metadata
932
+ - `"results"`:- serialized data.
930
933
  - Provides **structured pagination response format**.
931
934
 
932
935
  ---
@@ -941,9 +944,9 @@ builder = PaginatedDataBuilder(
941
944
  )
942
945
  ```
943
946
 
944
- - `request: Request` DRF request object.
945
- - `serializer_class: type[BaseSerializer]` DRF serializer class for the model.
946
- - `queryset: QuerySet` Django queryset to paginate.
947
+ - `request: Request`:- DRF request object.
948
+ - `serializer_class: type[BaseSerializer]`:- DRF serializer class for the model.
949
+ - `queryset: QuerySet`:- Django queryset to paginate.
947
950
 
948
951
  ### Paginated Data Builder Methods
949
952
 
@@ -973,6 +976,80 @@ builder = PaginatedDataBuilder(
973
976
  }
974
977
  ```
975
978
 
979
+ ### 16. Caching Mixins — API Reference
980
+
981
+ This module provides a set of DRF mixins to handle **caching for list, retrieve, and custom actions** with automatic invalidation on create, update, and destroy.
982
+
983
+ #### 1️ `CacheKeyMixin`
984
+
985
+ - **Purpose**: Generate unique cache keys for DRF viewset actions.
986
+ - **Attributes**:
987
+ - `cache_timeout: int = 300`:- default cache duration in seconds.
988
+ - **Methods**:
989
+ - `get_cache_timeout()`:- returns the cache timeout.
990
+ - `get_cache_key(action_type, pk=None, action_name=None)`:- returns a cache key string based on action type:**
991
+ - `list` or `custom-list`:- hash of query parameters.
992
+ - `retrieve` or `custom-detail`:- uses primary key (`pk`).
993
+
994
+ #### 2️ `CacheOpsMixin`
995
+
996
+ - **Purpose**: Get, set, and invalidate cache.
997
+ - **Methods**:
998
+ - `get_or_set_cache(cache_key, data_fn, timeout=None)`:- fetch from cache or compute and set.
999
+ - `invalidate_cache(pk=None, custom_actions=None)`:- delete cached items:
1000
+ - Deletes retrieve/detail caches for a `pk`.
1001
+ - Deletes list caches (supports `delete_pattern` if available).
1002
+
1003
+ #### 3️ `CacheActionMixin`
1004
+
1005
+ - **Purpose**: Decorator for caching custom DRF `@action` methods.
1006
+ - **Methods**:
1007
+ - `cache_action(detail=False, action_name=None)`:- returns a decorator that caches action results automatically.
1008
+ - Works for:
1009
+ - `detail=False`:- custom-list action cache.
1010
+ - `detail=True`:- custom-detail action cache.
1011
+
1012
+ #### 4️ `CacheListRetrieveMixin`
1013
+
1014
+ - **Purpose**: Caches DRF `list()` and `retrieve()` responses.
1015
+ - **Methods**:
1016
+ - `list(request, *args, **kwargs)`:- caches list responses.
1017
+ - `retrieve(request, *args, **kwargs)`:- caches detail responses.
1018
+ - `_get_list_data(request)`:- internal method to fetch paginated list data.
1019
+ - `_get_detail_data()`:- internal method to fetch a single object.
1020
+
1021
+ #### 5️ `CacheInvalidateMixin`
1022
+
1023
+ - **Purpose**: Automatically invalidates caches on write operations.
1024
+ - **Methods**:
1025
+ - `create(request, *args, **kwargs)`:- invalidates list caches.
1026
+ - `update(request, *args, **kwargs)`:- invalidates detail caches for `pk`.
1027
+ - `destroy(request, *args, **kwargs)`:- invalidates detail caches for `pk`.
1028
+
1029
+ #### Example of Caching Mixins
1030
+
1031
+ ```python
1032
+ from rest_framework.viewsets import ModelViewSet
1033
+ from myapp.models import Book
1034
+ from myapp.serializers import BookSerializer
1035
+ from djresttoolkit.cache_mixins import CacheInvalidateMixin, CacheActionMixin
1036
+
1037
+ class BookViewSet(CacheInvalidateMixin, ModelViewSet):
1038
+ queryset = Book.objects.all()
1039
+ serializer_class = BookSerializer
1040
+ basename = "book"
1041
+
1042
+ @CacheActionMixin.cache_action(detail=False)
1043
+ def popular(self, request):
1044
+ data = Book.objects.filter(is_popular=True)
1045
+ serializer = self.get_serializer(data, many=True)
1046
+ return Response(serializer.data)
1047
+ ```
1048
+
1049
+ - Automatically caches `list`, `retrieve`, and `popular` actions.
1050
+ - Invalidates caches when books are created, updated, or deleted.
1051
+ - Supports custom cache keys per action.
1052
+
976
1053
  ## 🛠️ Planned Features
977
1054
 
978
1055
  - Add more utils
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djresttoolkit
3
- Version: 0.16.1
3
+ Version: 0.17.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
@@ -112,6 +112,9 @@ djresttoolkit is a collection of utilities and helpers for Django and Django RES
112
112
  - **PaginatedDataBuilder**
113
113
  Builder that combines `PageNumberPagination` + serializers to return standardized paginated responses with `"page"` + `"results"`.
114
114
 
115
+ - **Caching Mixins**
116
+ This module provides a set of DRF mixins to handle caching for `list`, `retrieve`, and `custom actions` with automatic invalidation on create, update, and destroy.
117
+
115
118
  ## 📦 Installation
116
119
 
117
120
  - **By using uv:**
@@ -312,7 +315,7 @@ print(settings.database_url)
312
315
  #### Features
313
316
 
314
317
  - Prioritizes `.env` variables over YAML.
315
- - Supports nested keys: `DATABASE__HOST` `settings.database.host`.
318
+ - Supports nested keys: `DATABASE__HOST`:- `settings.database.host`.
316
319
  - Designed to be subclassed for project-specific settings.
317
320
 
318
321
  ### 4. EmailSender — API Reference
@@ -928,17 +931,17 @@ from djresttoolkit.pagination import PageNumberPagination
928
931
  - Clients can control items per page using `?page-size=`.
929
932
  - Structured pagination metadata:
930
933
 
931
- - `current` current page number
932
- - `total` total number of pages
933
- - `size` number of items per page
934
- - `total_items` total number of items across all pages
935
- - `next` next page URL
936
- - `previous` previous page URL
934
+ - `current`:- current page number
935
+ - `total`:- total number of pages
936
+ - `size`:- number of items per page
937
+ - `total_items`:- total number of items across all pages
938
+ - `next`:- next page URL
939
+ - `previous`:- previous page URL
937
940
  - Standardized API response format.
938
941
 
939
942
  ### Attributes of Page Number Pagination
940
943
 
941
- - `page_size_query_param: str` Query parameter name (`"page-size"`).
944
+ - `page_size_query_param: str`:- Query parameter name (`"page-size"`).
942
945
 
943
946
  ### Page Number Pagination Methods
944
947
 
@@ -983,8 +986,8 @@ from djresttoolkit.pagination import PaginatedDataBuilder
983
986
  - Integrates with **DRF serializers**.
984
987
  - Handles **invalid pages** gracefully by raising `NotFound`.
985
988
  - Returns both:
986
- - `"page"` pagination metadata
987
- - `"results"` serialized data.
989
+ - `"page"`:- pagination metadata
990
+ - `"results"`:- serialized data.
988
991
  - Provides **structured pagination response format**.
989
992
 
990
993
  ---
@@ -999,9 +1002,9 @@ builder = PaginatedDataBuilder(
999
1002
  )
1000
1003
  ```
1001
1004
 
1002
- - `request: Request` DRF request object.
1003
- - `serializer_class: type[BaseSerializer]` DRF serializer class for the model.
1004
- - `queryset: QuerySet` Django queryset to paginate.
1005
+ - `request: Request`:- DRF request object.
1006
+ - `serializer_class: type[BaseSerializer]`:- DRF serializer class for the model.
1007
+ - `queryset: QuerySet`:- Django queryset to paginate.
1005
1008
 
1006
1009
  ### Paginated Data Builder Methods
1007
1010
 
@@ -1031,6 +1034,80 @@ builder = PaginatedDataBuilder(
1031
1034
  }
1032
1035
  ```
1033
1036
 
1037
+ ### 16. Caching Mixins — API Reference
1038
+
1039
+ This module provides a set of DRF mixins to handle **caching for list, retrieve, and custom actions** with automatic invalidation on create, update, and destroy.
1040
+
1041
+ #### 1️ `CacheKeyMixin`
1042
+
1043
+ - **Purpose**: Generate unique cache keys for DRF viewset actions.
1044
+ - **Attributes**:
1045
+ - `cache_timeout: int = 300`:- default cache duration in seconds.
1046
+ - **Methods**:
1047
+ - `get_cache_timeout()`:- returns the cache timeout.
1048
+ - `get_cache_key(action_type, pk=None, action_name=None)`:- returns a cache key string based on action type:**
1049
+ - `list` or `custom-list`:- hash of query parameters.
1050
+ - `retrieve` or `custom-detail`:- uses primary key (`pk`).
1051
+
1052
+ #### 2️ `CacheOpsMixin`
1053
+
1054
+ - **Purpose**: Get, set, and invalidate cache.
1055
+ - **Methods**:
1056
+ - `get_or_set_cache(cache_key, data_fn, timeout=None)`:- fetch from cache or compute and set.
1057
+ - `invalidate_cache(pk=None, custom_actions=None)`:- delete cached items:
1058
+ - Deletes retrieve/detail caches for a `pk`.
1059
+ - Deletes list caches (supports `delete_pattern` if available).
1060
+
1061
+ #### 3️ `CacheActionMixin`
1062
+
1063
+ - **Purpose**: Decorator for caching custom DRF `@action` methods.
1064
+ - **Methods**:
1065
+ - `cache_action(detail=False, action_name=None)`:- returns a decorator that caches action results automatically.
1066
+ - Works for:
1067
+ - `detail=False`:- custom-list action cache.
1068
+ - `detail=True`:- custom-detail action cache.
1069
+
1070
+ #### 4️ `CacheListRetrieveMixin`
1071
+
1072
+ - **Purpose**: Caches DRF `list()` and `retrieve()` responses.
1073
+ - **Methods**:
1074
+ - `list(request, *args, **kwargs)`:- caches list responses.
1075
+ - `retrieve(request, *args, **kwargs)`:- caches detail responses.
1076
+ - `_get_list_data(request)`:- internal method to fetch paginated list data.
1077
+ - `_get_detail_data()`:- internal method to fetch a single object.
1078
+
1079
+ #### 5️ `CacheInvalidateMixin`
1080
+
1081
+ - **Purpose**: Automatically invalidates caches on write operations.
1082
+ - **Methods**:
1083
+ - `create(request, *args, **kwargs)`:- invalidates list caches.
1084
+ - `update(request, *args, **kwargs)`:- invalidates detail caches for `pk`.
1085
+ - `destroy(request, *args, **kwargs)`:- invalidates detail caches for `pk`.
1086
+
1087
+ #### Example of Caching Mixins
1088
+
1089
+ ```python
1090
+ from rest_framework.viewsets import ModelViewSet
1091
+ from myapp.models import Book
1092
+ from myapp.serializers import BookSerializer
1093
+ from djresttoolkit.cache_mixins import CacheInvalidateMixin, CacheActionMixin
1094
+
1095
+ class BookViewSet(CacheInvalidateMixin, ModelViewSet):
1096
+ queryset = Book.objects.all()
1097
+ serializer_class = BookSerializer
1098
+ basename = "book"
1099
+
1100
+ @CacheActionMixin.cache_action(detail=False)
1101
+ def popular(self, request):
1102
+ data = Book.objects.filter(is_popular=True)
1103
+ serializer = self.get_serializer(data, many=True)
1104
+ return Response(serializer.data)
1105
+ ```
1106
+
1107
+ - Automatically caches `list`, `retrieve`, and `popular` actions.
1108
+ - Invalidates caches when books are created, updated, or deleted.
1109
+ - Supports custom cache keys per action.
1110
+
1034
1111
  ## 🛠️ Planned Features
1035
1112
 
1036
1113
  - Add more utils
@@ -1,5 +1,5 @@
1
1
  LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
2
- README.md,sha256=67zRUEO0Ccp-TqVpe3FsgHqRi5JGfckiZlAjLyI2jSw,26809
2
+ README.md,sha256=hLkGp4AnOR6soIuFgzTKW1nM5s1s8tNL8isKJfqLjwQ,29920
3
3
  demo/staticfiles/admin/img/LICENSE,sha256=0RT6_zSIwWwxmzI13EH5AjnT1j2YU3MwM9j3U19cAAQ,1081
4
4
  src/djresttoolkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  src/djresttoolkit/admin.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -44,10 +44,15 @@ src/djresttoolkit/views/_api_views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQe
44
44
  src/djresttoolkit/views/_api_views/_choice_fields_apiview.py,sha256=zABPgqxMVaWd814B_sC64bWL61fDJkyYQZmJXQCa6Xc,1395
45
45
  src/djresttoolkit/views/_exceptions/__init__.py,sha256=DrCUxuPNyBR4WhzNutn5HDxLa--q51ykIxSG7_bFsOI,83
46
46
  src/djresttoolkit/views/_exceptions/_exception_handler.py,sha256=_o7If47bzWLl57LeSXSWsIDsJGo2RIpwYAwNQ-hsHVY,2839
47
- src/djresttoolkit/views/mixins/__init__.py,sha256=K-1tk5d8tCVViMynw5DdffJ3Oo5uHpEx32E3_4X2UxM,154
47
+ src/djresttoolkit/views/mixins/__init__.py,sha256=Ge-Dwa6S5-4BpfzMjLfxEkkepk8F3IJn0DNWBETq1Hw,537
48
+ src/djresttoolkit/views/mixins/_cache_action_mixin.py,sha256=Vle_xbHc6dzI2SqxLiM5HsrDkOzSgqj8GFB85MyPdaw,1516
49
+ src/djresttoolkit/views/mixins/_cache_invalidate_mixin.py,sha256=Ax6VC1lg8qYGW5IK339hqq40SRKj61J3Jp-mB_2gPWc,1170
50
+ src/djresttoolkit/views/mixins/_cache_key_mixin.py,sha256=0kt1rgz34q43DCLFwlR4rkyHtoWDYCMLXaTe7XfRZu4,1170
51
+ src/djresttoolkit/views/mixins/_cache_list_retrieve_mixin.py,sha256=nxJpfkNZWuOpf7wDDkGwq_ZQNT_17-iRisdTB2Cc0iY,1935
52
+ src/djresttoolkit/views/mixins/_cache_ops_mixin.py,sha256=veaBwH3Bp3GAzFizIxXT8EhmUF1AY3kCZBcF7rHPfyc,1435
48
53
  src/djresttoolkit/views/mixins/_retrieve_object_mixin.py,sha256=Q9znYPb07YXXUhsL7VIrk3BC-zDwjOhwLJKe2GPJ-k0,1155
49
- djresttoolkit-0.16.1.dist-info/METADATA,sha256=-GXgWkX-WeP5Mo3t1Id8QjeQnJWtHNts4PEAjahmePE,29819
50
- djresttoolkit-0.16.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
51
- djresttoolkit-0.16.1.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
52
- djresttoolkit-0.16.1.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
53
- djresttoolkit-0.16.1.dist-info/RECORD,,
54
+ djresttoolkit-0.17.0.dist-info/METADATA,sha256=YuYyP_Rcc756w88DI3NyasZaNA1uaBoahMxzlXcBCIY,32930
55
+ djresttoolkit-0.17.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
+ djresttoolkit-0.17.0.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
57
+ djresttoolkit-0.17.0.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
58
+ djresttoolkit-0.17.0.dist-info/RECORD,,
@@ -1,6 +1,16 @@
1
1
  from ._retrieve_object_mixin import RetrieveObjectMixin, QuerysetNotDefinedError
2
+ from ._cache_action_mixin import CacheActionMixin
3
+ from ._cache_invalidate_mixin import CacheInvalidateMixin
4
+ from ._cache_key_mixin import CacheKeyMixin
5
+ from ._cache_list_retrieve_mixin import CacheListRetrieveMixin
6
+ from ._cache_ops_mixin import CacheOpsMixin
2
7
 
3
8
  __all__ = [
4
9
  "RetrieveObjectMixin",
5
10
  "QuerysetNotDefinedError",
11
+ "CacheActionMixin",
12
+ "CacheInvalidateMixin",
13
+ "CacheKeyMixin",
14
+ "CacheListRetrieveMixin",
15
+ "CacheOpsMixin",
6
16
  ]
@@ -0,0 +1,44 @@
1
+ from functools import wraps
2
+ from typing import Any, Callable
3
+
4
+ from rest_framework.response import Response
5
+ from ._cache_ops_mixin import CacheOpsMixin
6
+ from rest_framework.request import Request
7
+
8
+
9
+ class CacheActionMixin(CacheOpsMixin):
10
+ """Provides decorator for caching custom @action methods."""
11
+
12
+ def cache_action(
13
+ self,
14
+ detail: bool = False,
15
+ action_name: str | None = None,
16
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
17
+ def decorator(view_method: Callable[..., Any]) -> Callable[..., Any]:
18
+ @wraps(view_method)
19
+ def wrapper(
20
+ viewset: Any,
21
+ request: Request,
22
+ *args: Any,
23
+ **kwargs: Any,
24
+ ) -> Response:
25
+ name = action_name or view_method.__name__
26
+ pk = kwargs.get("pk") if detail else None
27
+ action_type = "custom-detail" if detail else "custom-list"
28
+ key = viewset.get_cache_key(action_type, pk=pk, action_name=name)
29
+
30
+ def get_data() -> None | Any:
31
+ response = view_method(
32
+ viewset,
33
+ request,
34
+ *args,
35
+ **kwargs,
36
+ )
37
+ return response.data if isinstance(response, Response) else response
38
+
39
+ data = viewset.get_or_set_cache(key, get_data)
40
+ return Response(data)
41
+
42
+ return wrapper
43
+
44
+ return decorator
@@ -0,0 +1,38 @@
1
+ from typing import Any
2
+ from rest_framework.request import Request
3
+ from rest_framework.response import Response
4
+ from ._cache_list_retrieve_mixin import CacheListRetrieveMixin
5
+
6
+
7
+ class CacheInvalidateMixin(CacheListRetrieveMixin):
8
+ """Invalidate caches after create, update, destroy."""
9
+
10
+ def create(
11
+ self,
12
+ request: Request,
13
+ *args: Any,
14
+ **kwargs: Any,
15
+ ) -> Response:
16
+ response = super().create(request, *args, **kwargs) # type: ignore
17
+ self.invalidate_cache()
18
+ return response # type: ignore
19
+
20
+ def update(
21
+ self,
22
+ request: Request,
23
+ *args: Any,
24
+ **kwargs: Any,
25
+ ) -> Response:
26
+ response = super().update(request, *args, **kwargs) # type: ignore
27
+ self.invalidate_cache(pk=self.kwargs.get("pk")) # type: ignore
28
+ return response # type: ignore
29
+
30
+ def destroy(
31
+ self,
32
+ request: Request,
33
+ *args: Any,
34
+ **kwargs: Any,
35
+ ) -> Response:
36
+ response = super().destroy(request, *args, **kwargs) # type: ignore
37
+ self.invalidate_cache(pk=self.kwargs.get("pk")) # type: ignore
38
+ return response # type: ignore
@@ -0,0 +1,33 @@
1
+ import hashlib
2
+ import json
3
+ from typing import Any
4
+
5
+
6
+ class CacheKeyMixin:
7
+ """Handles generating unique cache keys for views."""
8
+
9
+ cache_timeout: int = 300
10
+
11
+ def get_cache_timeout(self) -> int:
12
+ return self.cache_timeout
13
+
14
+ def get_cache_key(
15
+ self,
16
+ action_type: str,
17
+ pk: Any | str = None,
18
+ action_name: str | None = None,
19
+ ) -> str | None:
20
+ if action_type in ("list", "custom-list"):
21
+ query_params = dict(sorted(self.request.query_params.items())) # type: ignore
22
+ query_string = json.dumps(query_params, separators=(",", ":"))
23
+ query_hash = hashlib.md5(query_string.encode()).hexdigest()
24
+ if action_type == "list":
25
+ return f"{self.basename}_list_{query_hash}" # type: ignore
26
+ return f"{self.basename}_{action_name}_list_{query_hash}" # type: ignore
27
+
28
+ if action_type in ("retrieve", "custom-detail") and pk is not None:
29
+ if action_type == "retrieve":
30
+ return f"{self.basename}_detail_{pk}" # type: ignore
31
+ return f"{self.basename}_{action_name}_detail_{pk}" # type: ignore
32
+
33
+ return None
@@ -0,0 +1,57 @@
1
+ from typing import Any
2
+
3
+ from rest_framework.response import Response
4
+ from rest_framework.request import Request
5
+ from ._cache_action_mixin import CacheActionMixin
6
+
7
+
8
+ class CacheListRetrieveMixin(CacheActionMixin):
9
+ """Caches list() and retrieve() responses."""
10
+
11
+ def list(
12
+ self,
13
+ request: Request,
14
+ *args: Any,
15
+ **kwargs: Any,
16
+ ) -> Response:
17
+ cache_key = self.get_cache_key("list")
18
+ if not cache_key:
19
+ return super().list(request, *args, **kwargs) # type: ignore
20
+
21
+ data = self.get_or_set_cache(
22
+ cache_key,
23
+ lambda: self._get_list_data(request), # type: ignore
24
+ )
25
+ return Response(data)
26
+
27
+ def _get_list_data(self, request: Response) -> Any:
28
+ queryset = self.filter_queryset(self.get_queryset()) # type: ignore
29
+ page = self.paginate_queryset(queryset) # type: ignore
30
+ if page is not None:
31
+ serializer = self.get_serializer(page, many=True) # type: ignore
32
+ return self.get_paginated_response(serializer.data).data # type: ignore
33
+ else:
34
+ serializer = self.get_serializer(queryset, many=True) # type: ignore
35
+ return serializer.data # type: ignore
36
+
37
+ def retrieve(
38
+ self,
39
+ request: Request,
40
+ *args: Any,
41
+ **kwargs: Any,
42
+ ) -> Response:
43
+ pk = self.kwargs.get("pk") # type: ignore
44
+ cache_key = self.get_cache_key("retrieve", pk=pk) # type: ignore
45
+ if not cache_key:
46
+ return super().retrieve(request, *args, **kwargs) # type: ignore
47
+
48
+ data = self.get_or_set_cache(
49
+ cache_key,
50
+ lambda: self._get_detail_data(),
51
+ )
52
+ return Response(data)
53
+
54
+ def _get_detail_data(self) -> Any:
55
+ instance = self.get_object() # type: ignore
56
+ serializer = self.get_serializer(instance) # type: ignore
57
+ return serializer.data # type: ignore
@@ -0,0 +1,47 @@
1
+ from typing import Any, Callable
2
+
3
+ from django.core.cache import cache
4
+
5
+ from ._cache_key_mixin import CacheKeyMixin
6
+
7
+
8
+ class CacheOpsMixin(CacheKeyMixin):
9
+ """Handles getting, setting, and invalidating cache."""
10
+
11
+ def get_or_set_cache(
12
+ self,
13
+ cache_key: str,
14
+ data_fn: Callable[[], Any],
15
+ timeout: int | None = None,
16
+ ) -> Any:
17
+ data = cache.get(cache_key)
18
+ if data is None:
19
+ data = data_fn()
20
+ cache.set(cache_key, data, timeout or self.get_cache_timeout())
21
+ return data
22
+
23
+ def invalidate_cache(
24
+ self,
25
+ pk: Any | None = None,
26
+ custom_actions: list[str] | None = None,
27
+ ) -> None:
28
+ if pk:
29
+ key = self.get_cache_key("retrieve", pk=pk)
30
+ if key:
31
+ cache.delete(key)
32
+
33
+ if custom_actions:
34
+ for action in custom_actions:
35
+ key = self.get_cache_key(
36
+ "custom-detail",
37
+ pk=pk,
38
+ action_name=action,
39
+ )
40
+ if key:
41
+ cache.delete(key)
42
+
43
+ if hasattr(cache, "delete_pattern"):
44
+ cache.delete_pattern(f"{self.basename}_list_*") # type: ignore
45
+ if custom_actions:
46
+ for action in custom_actions:
47
+ cache.delete_pattern(f"{self.basename}_{action}_list_*") # type: ignore