djresttoolkit 0.3.0__tar.gz → 0.5.0__tar.gz

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.
Files changed (26) hide show
  1. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/.gitignore +2 -0
  2. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/PKG-INFO +192 -2
  3. djresttoolkit-0.5.0/README.md +239 -0
  4. djresttoolkit-0.5.0/demo/staticfiles/admin/img/LICENSE +20 -0
  5. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/pyproject.toml +7 -1
  6. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/mail/_email_sender.py +2 -3
  7. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/mail/_models.py +2 -2
  8. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/mail/_types.py +1 -3
  9. djresttoolkit-0.5.0/src/djresttoolkit/middlewares/__init__.py +3 -0
  10. djresttoolkit-0.5.0/src/djresttoolkit/middlewares/_response_time_middleware.py +32 -0
  11. djresttoolkit-0.5.0/src/djresttoolkit/renderers/__init__.py +3 -0
  12. djresttoolkit-0.5.0/src/djresttoolkit/renderers/_throttle_info_json_renderer.py +33 -0
  13. djresttoolkit-0.5.0/src/djresttoolkit/throttling/__init__.py +3 -0
  14. djresttoolkit-0.5.0/src/djresttoolkit/throttling/_throttle_inspector.py +179 -0
  15. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/views/_exceptions/_exception_handler.py +11 -0
  16. djresttoolkit-0.3.0/README.md +0 -49
  17. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/LICENSE +0 -0
  18. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/__init__.py +0 -0
  19. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/admin.py +0 -0
  20. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/apps.py +0 -0
  21. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/mail/__init__.py +0 -0
  22. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/migrations/__init__.py +0 -0
  23. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/models/__init__.py +0 -0
  24. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/py.typed +0 -0
  25. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/views/__init__.py +0 -0
  26. {djresttoolkit-0.3.0 → djresttoolkit-0.5.0}/src/djresttoolkit/views/_exceptions/__init__.py +0 -0
@@ -8,4 +8,6 @@ wheels/
8
8
 
9
9
  # Virtual environments
10
10
  .venv
11
+ **/.env
12
+ **/.environ.yaml
11
13
  temp.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djresttoolkit
3
- Version: 0.3.0
3
+ Version: 0.5.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
@@ -85,7 +85,197 @@ djresttoolkit is a collection of utilities and helpers for Django and Django RES
85
85
 
86
86
  ## 📚 API Reference
87
87
 
88
- ### Under the development
88
+ ### 1. EmailSender
89
+
90
+ ```python
91
+ from djresttoolkit.mail import EmailSender, EmailContent, EmailTemplate
92
+ ```
93
+
94
+ ### `EmailSender`
95
+
96
+ Send templated emails.
97
+
98
+ #### Init
99
+
100
+ ```python
101
+ EmailSender(email_content: EmailContent | EmailContentDict)
102
+ ```
103
+
104
+ #### Methods
105
+
106
+ ```python
107
+ send(to: list[str], exceptions: bool = False) -> bool
108
+ ```
109
+
110
+ - `to`: recipient emails
111
+ - `exceptions`: raise on error if `True`, else logs error
112
+ - Returns `True` if sent, `False` on failure
113
+
114
+ #### Example
115
+
116
+ ```python
117
+ content = EmailContent(
118
+ subject="Hello",
119
+ from_email="noreply@example.com",
120
+ context={"username": "Alice"},
121
+ template=EmailTemplate(
122
+ text="emails/welcome.txt",
123
+ html="emails/welcome.html"
124
+ )
125
+ )
126
+ EmailSender(content).send(to=["user@example.com"])
127
+ ```
128
+
129
+ #### `EmailContent`
130
+
131
+ - `subject`, `from_email`, `context`, `template` (EmailTemplate)
132
+
133
+ #### `EmailTemplate`
134
+
135
+ - `text`, `html` — template file paths
136
+
137
+ ### 2. Custom DRF Exception Handler
138
+
139
+ ```python
140
+ from djresttoolkit.views import exception_handler
141
+ ```
142
+
143
+ ### `exception_handler(exc: Exception, context: dict[str, Any]) -> Response | None`
144
+
145
+ A DRF exception handler that:
146
+
147
+ - Preserves DRF’s default exception behavior.
148
+ - Adds throttling support (defaults to `AnonRateThrottle`).
149
+ - Returns **429 Too Many Requests** with `retry_after` if throttle limit is exceeded.
150
+
151
+ #### Parameters
152
+
153
+ - `exc`: Exception object.
154
+ - `context`: DRF context dictionary containing `"request"` and `"view"`.
155
+
156
+ #### Returns
157
+
158
+ - `Response` — DRF Response object (with throttling info if applicable), or `None`.
159
+
160
+ #### Settings Configuration
161
+
162
+ In `settings.py`:
163
+
164
+ ```python
165
+ REST_FRAMEWORK = {
166
+ 'EXCEPTION_HANDLER': 'djresttoolkit.views.exception_handler',
167
+ # Other DRF settings...
168
+ }
169
+ ```
170
+
171
+ #### Throttle Behavior
172
+
173
+ - Uses `view.throttle_classes` if defined, else defaults to `AnonRateThrottle`.
174
+ - Tracks requests in cache and calculates `retry_after`.
175
+ - Cleans expired timestamps automatically.
176
+
177
+ ### 3. Response Time Middleware
178
+
179
+ ```python
180
+ from djresttoolkit.middlewares import ResponseTimeMiddleware
181
+ ```
182
+
183
+ ### `ResponseTimeMiddleware`
184
+
185
+ Middleware to calculate and log **HTTP response time** for each request.
186
+
187
+ #### Constructor from ResponseTimeMiddleware
188
+
189
+ ```python
190
+ ResponseTimeMiddleware(get_response: Callable[[HttpRequest], HttpResponse])
191
+ ```
192
+
193
+ - `get_response`: The next middleware or view callable.
194
+
195
+ #### Usage
196
+
197
+ Add it to your Django `MIDDLEWARE` in `settings.py`:
198
+
199
+ ```python
200
+ MIDDLEWARE = [
201
+ # Other middlewares...
202
+ 'djresttoolkit.middlewares.ResponseTimeMiddleware',
203
+ ]
204
+ ```
205
+
206
+ #### Behavior
207
+
208
+ - Measures the time taken to process each request.
209
+ - Adds a header `X-Response-Time` to each HTTP response.
210
+ - Logs the response time using Django's logging system.
211
+
212
+ #### The response headers will include
213
+
214
+ ```json
215
+ X-Response-Time: 0.01234 seconds
216
+ ```
217
+
218
+ #### Logs a message
219
+
220
+ ```bash
221
+ INFO: Request processed in 0.01234 seconds
222
+ ```
223
+
224
+ ### 4. Throttle Utilities
225
+
226
+ #### `ThrottleInfoJSONRenderer`
227
+
228
+ ```python
229
+ from djresttoolkit.renderers import ThrottleInfoJSONRenderer
230
+ ```
231
+
232
+ A custom DRF JSON renderer that **automatically attaches throttle information to response headers**.
233
+
234
+ #### Usage (settings.py)
235
+
236
+ ```python
237
+ REST_FRAMEWORK = {
238
+ "DEFAULT_RENDERER_CLASSES": [
239
+ "djresttoolkit.renderers.ThrottleInfoJSONRenderer",
240
+ "rest_framework.renderers.BrowsableAPIRenderer",
241
+ ],
242
+ }
243
+ ```
244
+
245
+ When enabled, every response includes throttle headers like:
246
+
247
+ ```plaintext
248
+ X-Throttle-User-Limit: 100
249
+ X-Throttle-User-Remaining: 98
250
+ X-Throttle-User-Reset: 2025-08-18T07:30:00Z
251
+ X-Throttle-User-Retry-After: 0
252
+ ```
253
+
254
+ #### `ThrottleInspector`
255
+
256
+ ```python
257
+ from djresttoolkit.throttling import ThrottleInspector
258
+ ```
259
+
260
+ Utility class to **inspect DRF throttle usage** for a view or request.
261
+
262
+ #### Constructor for ThrottleInspector
263
+
264
+ ```python
265
+ ThrottleInspector(
266
+ view: APIView,
267
+ request: Request | None = None,
268
+ throttle_classes: list[type[BaseThrottle]] | None = None,
269
+ )
270
+ ```
271
+
272
+ #### Key Methods
273
+
274
+ - `get_details() -> dict[str, Any]`
275
+ Returns structured throttle info: limit, remaining, reset time, retry\_after.
276
+
277
+ - `attach_headers(response: Response, throttle_info: dict | None)`
278
+ Attaches throttle data to HTTP headers.
89
279
 
90
280
  ## 🛠️ Planned Features
91
281
 
@@ -0,0 +1,239 @@
1
+ # 🛠️ djresttoolkit (django rest toolkit)
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/djresttoolkit.svg)](https://pypi.org/project/djresttoolkit/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/djresttoolkit.svg)](https://pypi.org/project/djresttoolkit/)
5
+ [![License](https://img.shields.io/pypi/l/djresttoolkit.svg)](https://github.com/shaileshpandit141/djresttoolkit/blob/main/LICENSE)
6
+
7
+ djresttoolkit is a collection of utilities and helpers for Django and Django REST Framework (DRF) that simplify common development tasks such as API handling, authentication, and email sending and much more.
8
+
9
+ ## ✨ Features
10
+
11
+ - Django REST Framework helpers (serializers, views, pagination, etc.)
12
+ - Django utilities (e.g., email sending, model mixins)
13
+ - Ready-to-use shortcuts to speed up API development
14
+ - Lightweight, no unnecessary dependencies
15
+ - Type Safe - written with modern Python type hints.
16
+
17
+ ## 📦 Installation
18
+
19
+ - **By using uv:**
20
+
21
+ ```bash
22
+ uv add djresttoolkit
23
+ ````
24
+
25
+ - **By using pip:**
26
+
27
+ ```bash
28
+ pip install djresttoolkit
29
+ ````
30
+
31
+ ## 📚 API Reference
32
+
33
+ ### 1. EmailSender
34
+
35
+ ```python
36
+ from djresttoolkit.mail import EmailSender, EmailContent, EmailTemplate
37
+ ```
38
+
39
+ ### `EmailSender`
40
+
41
+ Send templated emails.
42
+
43
+ #### Init
44
+
45
+ ```python
46
+ EmailSender(email_content: EmailContent | EmailContentDict)
47
+ ```
48
+
49
+ #### Methods
50
+
51
+ ```python
52
+ send(to: list[str], exceptions: bool = False) -> bool
53
+ ```
54
+
55
+ - `to`: recipient emails
56
+ - `exceptions`: raise on error if `True`, else logs error
57
+ - Returns `True` if sent, `False` on failure
58
+
59
+ #### Example
60
+
61
+ ```python
62
+ content = EmailContent(
63
+ subject="Hello",
64
+ from_email="noreply@example.com",
65
+ context={"username": "Alice"},
66
+ template=EmailTemplate(
67
+ text="emails/welcome.txt",
68
+ html="emails/welcome.html"
69
+ )
70
+ )
71
+ EmailSender(content).send(to=["user@example.com"])
72
+ ```
73
+
74
+ #### `EmailContent`
75
+
76
+ - `subject`, `from_email`, `context`, `template` (EmailTemplate)
77
+
78
+ #### `EmailTemplate`
79
+
80
+ - `text`, `html` — template file paths
81
+
82
+ ### 2. Custom DRF Exception Handler
83
+
84
+ ```python
85
+ from djresttoolkit.views import exception_handler
86
+ ```
87
+
88
+ ### `exception_handler(exc: Exception, context: dict[str, Any]) -> Response | None`
89
+
90
+ A DRF exception handler that:
91
+
92
+ - Preserves DRF’s default exception behavior.
93
+ - Adds throttling support (defaults to `AnonRateThrottle`).
94
+ - Returns **429 Too Many Requests** with `retry_after` if throttle limit is exceeded.
95
+
96
+ #### Parameters
97
+
98
+ - `exc`: Exception object.
99
+ - `context`: DRF context dictionary containing `"request"` and `"view"`.
100
+
101
+ #### Returns
102
+
103
+ - `Response` — DRF Response object (with throttling info if applicable), or `None`.
104
+
105
+ #### Settings Configuration
106
+
107
+ In `settings.py`:
108
+
109
+ ```python
110
+ REST_FRAMEWORK = {
111
+ 'EXCEPTION_HANDLER': 'djresttoolkit.views.exception_handler',
112
+ # Other DRF settings...
113
+ }
114
+ ```
115
+
116
+ #### Throttle Behavior
117
+
118
+ - Uses `view.throttle_classes` if defined, else defaults to `AnonRateThrottle`.
119
+ - Tracks requests in cache and calculates `retry_after`.
120
+ - Cleans expired timestamps automatically.
121
+
122
+ ### 3. Response Time Middleware
123
+
124
+ ```python
125
+ from djresttoolkit.middlewares import ResponseTimeMiddleware
126
+ ```
127
+
128
+ ### `ResponseTimeMiddleware`
129
+
130
+ Middleware to calculate and log **HTTP response time** for each request.
131
+
132
+ #### Constructor from ResponseTimeMiddleware
133
+
134
+ ```python
135
+ ResponseTimeMiddleware(get_response: Callable[[HttpRequest], HttpResponse])
136
+ ```
137
+
138
+ - `get_response`: The next middleware or view callable.
139
+
140
+ #### Usage
141
+
142
+ Add it to your Django `MIDDLEWARE` in `settings.py`:
143
+
144
+ ```python
145
+ MIDDLEWARE = [
146
+ # Other middlewares...
147
+ 'djresttoolkit.middlewares.ResponseTimeMiddleware',
148
+ ]
149
+ ```
150
+
151
+ #### Behavior
152
+
153
+ - Measures the time taken to process each request.
154
+ - Adds a header `X-Response-Time` to each HTTP response.
155
+ - Logs the response time using Django's logging system.
156
+
157
+ #### The response headers will include
158
+
159
+ ```json
160
+ X-Response-Time: 0.01234 seconds
161
+ ```
162
+
163
+ #### Logs a message
164
+
165
+ ```bash
166
+ INFO: Request processed in 0.01234 seconds
167
+ ```
168
+
169
+ ### 4. Throttle Utilities
170
+
171
+ #### `ThrottleInfoJSONRenderer`
172
+
173
+ ```python
174
+ from djresttoolkit.renderers import ThrottleInfoJSONRenderer
175
+ ```
176
+
177
+ A custom DRF JSON renderer that **automatically attaches throttle information to response headers**.
178
+
179
+ #### Usage (settings.py)
180
+
181
+ ```python
182
+ REST_FRAMEWORK = {
183
+ "DEFAULT_RENDERER_CLASSES": [
184
+ "djresttoolkit.renderers.ThrottleInfoJSONRenderer",
185
+ "rest_framework.renderers.BrowsableAPIRenderer",
186
+ ],
187
+ }
188
+ ```
189
+
190
+ When enabled, every response includes throttle headers like:
191
+
192
+ ```plaintext
193
+ X-Throttle-User-Limit: 100
194
+ X-Throttle-User-Remaining: 98
195
+ X-Throttle-User-Reset: 2025-08-18T07:30:00Z
196
+ X-Throttle-User-Retry-After: 0
197
+ ```
198
+
199
+ #### `ThrottleInspector`
200
+
201
+ ```python
202
+ from djresttoolkit.throttling import ThrottleInspector
203
+ ```
204
+
205
+ Utility class to **inspect DRF throttle usage** for a view or request.
206
+
207
+ #### Constructor for ThrottleInspector
208
+
209
+ ```python
210
+ ThrottleInspector(
211
+ view: APIView,
212
+ request: Request | None = None,
213
+ throttle_classes: list[type[BaseThrottle]] | None = None,
214
+ )
215
+ ```
216
+
217
+ #### Key Methods
218
+
219
+ - `get_details() -> dict[str, Any]`
220
+ Returns structured throttle info: limit, remaining, reset time, retry\_after.
221
+
222
+ - `attach_headers(response: Response, throttle_info: dict | None)`
223
+ Attaches throttle data to HTTP headers.
224
+
225
+ ## 🛠️ Planned Features
226
+
227
+ - Add more utils
228
+
229
+ ## 🤝 Contributing
230
+
231
+ Contributions are welcome! Please open an issue or PR for any improvements.
232
+
233
+ ## 📜 License
234
+
235
+ MIT License — See [LICENSE](LICENSE).
236
+
237
+ ## 👤 Author
238
+
239
+ For questions or assistance, contact **Shailesh** at [shaileshpandit141@gmail.com](mailto:shaileshpandit141@gmail.com).
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Code Charm Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -4,7 +4,7 @@
4
4
 
5
5
  [project]
6
6
  name = "djresttoolkit"
7
- version = "0.3.0"
7
+ version = "0.5.0"
8
8
  description = "A collection of Django and DRF utilities to simplify API development."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = { file = "LICENSE" }
@@ -79,6 +79,12 @@ dev = [
79
79
  "djangorestframework-stubs>=3.16.2",
80
80
  "pytest>=8.4.1",
81
81
  "djresttoolkit",
82
+ "pyyaml>=6.0.2",
83
+ "pydantic-settings>=2.10.1",
84
+ "daphne>=4.2.1",
85
+ "django-redis>=6.0.0",
86
+ "django-cors-headers>=4.7.0",
87
+ "argon2-cffi-bindings>=21.2.0",
82
88
  ]
83
89
 
84
90
  # =====================
@@ -5,7 +5,6 @@ from typing import cast
5
5
  from django.core.exceptions import ValidationError
6
6
  from django.core.mail import EmailMultiAlternatives
7
7
  from django.template.loader import render_to_string
8
- from pydantic import EmailStr
9
8
 
10
9
  from ._models import EmailContent
11
10
  from ._types import EmailContentDict
@@ -30,7 +29,7 @@ class EmailSender:
30
29
 
31
30
  Methods
32
31
  -------
33
- send(to: list[EmailStr], exceptions: bool = False) -> bool
32
+ send(to: list[str], exceptions: bool = False) -> bool
34
33
  Sends the email to the given recipients.
35
34
  - `exceptions=True` riase exceptions on failure and returns False if `exceptions=False`.
36
35
 
@@ -64,7 +63,7 @@ class EmailSender:
64
63
 
65
64
  def send(
66
65
  self,
67
- to: list[EmailStr],
66
+ to: list[str],
68
67
  exceptions: bool = False,
69
68
  ) -> bool:
70
69
  """Send email to recipients."""
@@ -1,6 +1,6 @@
1
1
  from typing import Any
2
2
 
3
- from pydantic import BaseModel, EmailStr, field_validator
3
+ from pydantic import BaseModel, field_validator
4
4
 
5
5
 
6
6
  class EmailTemplate(BaseModel):
@@ -48,6 +48,6 @@ class EmailContent(BaseModel):
48
48
  """
49
49
 
50
50
  subject: str
51
- from_email: EmailStr | None
51
+ from_email: str | None
52
52
  context: dict[str, Any] | None = None
53
53
  template: EmailTemplate
@@ -1,7 +1,5 @@
1
1
  from typing import Any, TypedDict
2
2
 
3
- from pydantic import EmailStr
4
-
5
3
 
6
4
  class EmailTemplateDict(TypedDict):
7
5
  """Template for rendering email content."""
@@ -23,6 +21,6 @@ class EmailContentDict(TypedDict):
23
21
  """
24
22
 
25
23
  subject: str
26
- from_email: EmailStr | None
24
+ from_email: str | None
27
25
  context: dict[str, Any] | None
28
26
  template: EmailTemplateDict
@@ -0,0 +1,3 @@
1
+ from ._response_time_middleware import ResponseTimeMiddleware
2
+
3
+ __all__ = ["ResponseTimeMiddleware"]
@@ -0,0 +1,32 @@
1
+ import logging
2
+ import time
3
+ from collections.abc import Callable
4
+
5
+ from django.http import HttpRequest, HttpResponse
6
+
7
+ # Get logger from logging.
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class ResponseTimeMiddleware:
12
+ """Calculte response response time."""
13
+
14
+ def __init__(
15
+ self,
16
+ get_response: Callable[[HttpRequest], HttpResponse],
17
+ ) -> None:
18
+ "Initilize response time middleware."
19
+ self.get_response = get_response
20
+
21
+ def __call__(self, request: HttpRequest) -> HttpResponse:
22
+ """Handle to response response time calculation."""
23
+ start_time = time.perf_counter()
24
+ response = self.get_response(request)
25
+ end_time = time.perf_counter()
26
+
27
+ response_time = f"{round(end_time - start_time, 5)} seconds"
28
+ response["X-Response-Time"] = response_time
29
+
30
+ logger.info(f"Request processed in {response_time}")
31
+
32
+ return response
@@ -0,0 +1,3 @@
1
+ from ._throttle_info_json_renderer import ThrottleInfoJSONRenderer
2
+
3
+ __all__ = ["ThrottleInfoJSONRenderer"]
@@ -0,0 +1,33 @@
1
+ from typing import Any, Mapping
2
+
3
+ from rest_framework.renderers import JSONRenderer
4
+ from rest_framework.response import Response
5
+
6
+ from ..throttling import ThrottleInspector
7
+
8
+
9
+ class ThrottleInfoJSONRenderer(JSONRenderer):
10
+ def render(
11
+ self,
12
+ data: Any,
13
+ accepted_media_type: str | None = None,
14
+ renderer_context: Mapping[str, Any] | None = None,
15
+ ) -> Any:
16
+ """Handle throttle info to headers."""
17
+ if renderer_context:
18
+ response: Response | None = renderer_context.get("response")
19
+ view = renderer_context.get("view")
20
+ if response and view:
21
+ # Attach throttle info to headers
22
+ inspector = ThrottleInspector(view)
23
+ throttle_info = inspector.get_details()
24
+ inspector.attach_headers(
25
+ response=response,
26
+ throttle_info=throttle_info,
27
+ )
28
+ # Retuen Final rendered payload
29
+ return super().render(
30
+ data,
31
+ accepted_media_type,
32
+ renderer_context,
33
+ )
@@ -0,0 +1,3 @@
1
+ from ._throttle_inspector import ThrottleInspector
2
+
3
+ __all__ = ["ThrottleInspector"]
@@ -0,0 +1,179 @@
1
+ import logging
2
+ import re
3
+ from datetime import timedelta
4
+ from datetime import timezone as dt_timezone
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from django.conf import settings
8
+ from django.utils import timezone
9
+ from rest_framework.request import Request
10
+ from rest_framework.response import Response
11
+ from rest_framework.throttling import BaseThrottle, UserRateThrottle
12
+
13
+ if TYPE_CHECKING:
14
+ from rest_framework.views import APIView
15
+
16
+ ViewType = APIView
17
+ else:
18
+ ViewType = object
19
+
20
+ # Get logger from logging.
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ThrottleInspector:
25
+ """
26
+ Inspects and retrieves DRF throttle details for both class-based
27
+ and function-based views.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ view: ViewType,
33
+ request: Request | None = None,
34
+ throttle_classes: list[type[BaseThrottle]] | None = None,
35
+ ) -> None:
36
+ self.view = view
37
+ self.request: Request | None = getattr(view, "request", request)
38
+ self.throttle_classes: list[type[BaseThrottle]] = (
39
+ getattr(view, "throttle_classes", throttle_classes) or []
40
+ )
41
+
42
+ if not self.request:
43
+ logger.warning(f"Request object missing in {self._view_name()}.")
44
+ if not self.throttle_classes:
45
+ logger.info(f"No throttles configured for {self._view_name()}.")
46
+
47
+ def _view_name(self) -> str:
48
+ if callable(self.view):
49
+ return getattr(self.view, "__name__", str(self.view))
50
+ return type(self.view).__name__
51
+
52
+ @staticmethod
53
+ def to_snake_case(name: str) -> str:
54
+ """Convert UpperCamelCase to snake_case and remove 'RateThrottle'."""
55
+ return re.sub(r"(?<!^)(?=[A-Z])", "_", name.replace("RateThrottle", "")).lower()
56
+
57
+ @staticmethod
58
+ def parse_rate(rate: str) -> tuple[int, int] | None:
59
+ """Parse rate string like '100/day' into (limit, duration_in_seconds)."""
60
+ if not rate:
61
+ return None
62
+ match = re.match(r"(\d+)/(second|minute|hour|day)", rate)
63
+ if not match:
64
+ return None
65
+ num_requests, period = match.groups()
66
+ duration_map = {"second": 1, "minute": 60, "hour": 3600, "day": 86400}
67
+ return int(num_requests), duration_map[period]
68
+
69
+ def get_throttle_rate(
70
+ self, throttle_class: type[BaseThrottle]
71
+ ) -> tuple[int, int] | None:
72
+ """Return the (limit, duration_in_seconds) for a throttle class."""
73
+ scope = getattr(throttle_class, "scope", None)
74
+ if not scope:
75
+ logger.warning(f"No scope defined in {throttle_class.__name__}. Skipping.")
76
+ return None
77
+
78
+ rate = settings.REST_FRAMEWORK.get("DEFAULT_THROTTLE_RATES", {}).get(scope)
79
+ if not rate:
80
+ logger.warning(f"No rate limit found for scope '{scope}'. Skipping.")
81
+ return None
82
+
83
+ return self.parse_rate(rate)
84
+
85
+ def get_throttle_usage(
86
+ self,
87
+ throttle: UserRateThrottle,
88
+ limit: int,
89
+ duration: int,
90
+ ) -> dict[str, Any]:
91
+ """Return current usage info for a given throttle instance."""
92
+ if not self.request:
93
+ return {
94
+ "limit": limit,
95
+ "remaining": limit,
96
+ "reset_time": None,
97
+ "retry_after": {"time": None, "unit": "seconds"},
98
+ }
99
+
100
+ cache_key = throttle.get_cache_key(
101
+ self.request,
102
+ getattr(self.view, "view", self.view), # type: ignore
103
+ ) # type: ignore
104
+ history: list[Any] = throttle.cache.get(cache_key, []) if cache_key else []
105
+
106
+ remaining = max(0, limit - len(history))
107
+ first_request_time = ( # type: ignore
108
+ timezone.datetime.fromtimestamp(history[0], tz=dt_timezone.utc) # type: ignore[attr-defined]
109
+ if history
110
+ else timezone.now()
111
+ )
112
+ reset_time = first_request_time + timedelta(seconds=duration) # type: ignore
113
+ retry_after = max(0, int((reset_time - timezone.now()).total_seconds())) # type: ignore
114
+
115
+ return {
116
+ "limit": limit,
117
+ "remaining": remaining,
118
+ "reset_time": reset_time.isoformat(), # type: ignore
119
+ "retry_after": {"time": retry_after, "unit": "seconds"},
120
+ }
121
+
122
+ def get_details(self) -> dict[str, Any]:
123
+ """
124
+ Return detailed throttle info for all configured throttles.
125
+ If throttling is not configured, returns an empty dict.
126
+ """
127
+ if not self.throttle_classes:
128
+ return {}
129
+
130
+ details: dict[str, Any] = {"throttled_by": None, "throttles": {}}
131
+
132
+ for throttle_class in self.throttle_classes:
133
+ throttle = throttle_class()
134
+ parsed_rate = self.get_throttle_rate(throttle_class)
135
+ if not parsed_rate:
136
+ continue
137
+
138
+ limit, duration = parsed_rate
139
+ scope = getattr(
140
+ throttle_class, "scope", self.to_snake_case(throttle_class.__name__)
141
+ )
142
+ usage = self.get_throttle_usage(throttle, limit, duration) # type: ignore[arg-type]
143
+ details["throttles"][scope] = usage
144
+
145
+ if usage["remaining"] == 0 and not details["throttled_by"]:
146
+ details["throttled_by"] = scope
147
+ logger.info(f"Request throttled by {scope}")
148
+
149
+ return details
150
+
151
+ def attach_headers(
152
+ self,
153
+ response: Response,
154
+ throttle_info: dict[str, Any] | None,
155
+ ) -> None:
156
+ """
157
+ Attaches throttle details to response headers in DRF-style.
158
+
159
+ Header format:
160
+ X-Throttle-{throttle_type}-Limit
161
+ X-Throttle-{throttle_type}-Remaining
162
+ X-Throttle-{throttle_type}-Reset
163
+ X-Throttle-{throttle_type}-Retry-After (in seconds)
164
+ """
165
+ if not throttle_info:
166
+ return
167
+
168
+ for throttle_type, data in throttle_info.get("throttles", {}).items():
169
+ response[f"X-Throttle-{throttle_type}-Limit"] = str(data.get("limit", ""))
170
+ response[f"X-Throttle-{throttle_type}-Remaining"] = str(
171
+ data.get("remaining", "")
172
+ )
173
+ response[f"X-Throttle-{throttle_type}-Reset"] = data.get("reset_time") or ""
174
+ retry_after = data.get("retry_after", {}).get("time")
175
+ response[f"X-Throttle-{throttle_type}-Retry-After"] = (
176
+ str(retry_after) if retry_after is not None else "0"
177
+ )
178
+
179
+ logger.info(f"Throttle headers attached to response for {self._view_name()}.")
@@ -1,7 +1,9 @@
1
1
  from typing import Any, cast
2
2
 
3
+ from django.conf import settings
3
4
  from django.core.cache import cache
4
5
  from django.utils import timezone
6
+ from django.utils.module_loading import import_string
5
7
  from rest_framework import status, views
6
8
  from rest_framework.request import Request
7
9
  from rest_framework.response import Response
@@ -26,6 +28,15 @@ def exception_handler(exc: Exception, context: dict[str, Any]) -> Response | Non
26
28
  view, "throttle_classes", [AnonRateThrottle]
27
29
  )
28
30
 
31
+ # Set default throttles if user doesn't provide
32
+ default = settings.REST_FRAMEWORK.get("DEFAULT_THROTTLE_CLASSES", [])
33
+ if not throttle_classes:
34
+ if default:
35
+ throttle_classes = []
36
+ for path in default:
37
+ throttle_classes.append(import_string(path))
38
+
39
+ # Handle all throttles by looping it
29
40
  for throttle_class in throttle_classes:
30
41
  throttle = throttle_class()
31
42
  cache_key = throttle.get_cache_key(request, view)
@@ -1,49 +0,0 @@
1
- # 🛠️ djresttoolkit (django rest toolkit)
2
-
3
- [![PyPI version](https://img.shields.io/pypi/v/djresttoolkit.svg)](https://pypi.org/project/djresttoolkit/)
4
- [![Python versions](https://img.shields.io/pypi/pyversions/djresttoolkit.svg)](https://pypi.org/project/djresttoolkit/)
5
- [![License](https://img.shields.io/pypi/l/djresttoolkit.svg)](https://github.com/shaileshpandit141/djresttoolkit/blob/main/LICENSE)
6
-
7
- djresttoolkit is a collection of utilities and helpers for Django and Django REST Framework (DRF) that simplify common development tasks such as API handling, authentication, and email sending and much more.
8
-
9
- ## ✨ Features
10
-
11
- - Django REST Framework helpers (serializers, views, pagination, etc.)
12
- - Django utilities (e.g., email sending, model mixins)
13
- - Ready-to-use shortcuts to speed up API development
14
- - Lightweight, no unnecessary dependencies
15
- - Type Safe - written with modern Python type hints.
16
-
17
- ## 📦 Installation
18
-
19
- - **By using uv:**
20
-
21
- ```bash
22
- uv add djresttoolkit
23
- ````
24
-
25
- - **By using pip:**
26
-
27
- ```bash
28
- pip install djresttoolkit
29
- ````
30
-
31
- ## 📚 API Reference
32
-
33
- ### Under the development
34
-
35
- ## 🛠️ Planned Features
36
-
37
- - Add more utils
38
-
39
- ## 🤝 Contributing
40
-
41
- Contributions are welcome! Please open an issue or PR for any improvements.
42
-
43
- ## 📜 License
44
-
45
- MIT License — See [LICENSE](LICENSE).
46
-
47
- ## 👤 Author
48
-
49
- For questions or assistance, contact **Shailesh** at [shaileshpandit141@gmail.com](mailto:shaileshpandit141@gmail.com).
File without changes