djresttoolkit 0.4.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.
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/.gitignore +2 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/PKG-INFO +192 -2
- djresttoolkit-0.5.0/README.md +239 -0
- djresttoolkit-0.5.0/demo/staticfiles/admin/img/LICENSE +20 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/pyproject.toml +7 -1
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/mail/_email_sender.py +2 -3
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/mail/_models.py +2 -2
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/mail/_types.py +1 -3
- djresttoolkit-0.5.0/src/djresttoolkit/renderers/__init__.py +3 -0
- djresttoolkit-0.5.0/src/djresttoolkit/renderers/_throttle_info_json_renderer.py +33 -0
- djresttoolkit-0.5.0/src/djresttoolkit/throttling/__init__.py +3 -0
- djresttoolkit-0.5.0/src/djresttoolkit/throttling/_throttle_inspector.py +179 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/views/_exceptions/_exception_handler.py +11 -0
- djresttoolkit-0.4.0/README.md +0 -49
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/LICENSE +0 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/__init__.py +0 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/admin.py +0 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/apps.py +0 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/mail/__init__.py +0 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/middlewares/__init__.py +0 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/middlewares/_response_time_middleware.py +0 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/migrations/__init__.py +0 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/models/__init__.py +0 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/py.typed +0 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/views/__init__.py +0 -0
- {djresttoolkit-0.4.0 → djresttoolkit-0.5.0}/src/djresttoolkit/views/_exceptions/__init__.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: djresttoolkit
|
3
|
-
Version: 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
|
-
###
|
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
|
+
[](https://pypi.org/project/djresttoolkit/)
|
4
|
+
[](https://pypi.org/project/djresttoolkit/)
|
5
|
+
[](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.
|
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[
|
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[
|
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,
|
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:
|
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:
|
24
|
+
from_email: str | None
|
27
25
|
context: dict[str, Any] | None
|
28
26
|
template: EmailTemplateDict
|
@@ -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,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)
|
djresttoolkit-0.4.0/README.md
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
# 🛠️ djresttoolkit (django rest toolkit)
|
2
|
-
|
3
|
-
[](https://pypi.org/project/djresttoolkit/)
|
4
|
-
[](https://pypi.org/project/djresttoolkit/)
|
5
|
-
[](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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|