djresttoolkit 0.4.0__py3-none-any.whl → 0.6.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 +191 -1
- demo/staticfiles/admin/img/LICENSE +20 -0
- {djresttoolkit-0.4.0.dist-info → djresttoolkit-0.6.0.dist-info}/METADATA +193 -2
- djresttoolkit-0.6.0.dist-info/RECORD +34 -0
- src/djresttoolkit/dbseed/__init__.py +0 -0
- src/djresttoolkit/dbseed/models/__init__.py +4 -0
- src/djresttoolkit/dbseed/models/_base_seed_model.py +59 -0
- src/djresttoolkit/dbseed/models/_gen.py +11 -0
- src/djresttoolkit/mail/_email_sender.py +2 -3
- src/djresttoolkit/mail/_models.py +2 -2
- src/djresttoolkit/mail/_types.py +1 -3
- src/djresttoolkit/management/__init__.py +0 -0
- src/djresttoolkit/management/commands/__init__.py +0 -0
- src/djresttoolkit/management/commands/dbseed.py +115 -0
- src/djresttoolkit/renderers/__init__.py +3 -0
- src/djresttoolkit/renderers/_throttle_info_json_renderer.py +33 -0
- src/djresttoolkit/throttling/__init__.py +3 -0
- src/djresttoolkit/throttling/_throttle_inspector.py +179 -0
- src/djresttoolkit/views/_exceptions/_exception_handler.py +11 -0
- djresttoolkit-0.4.0.dist-info/RECORD +0 -22
- {djresttoolkit-0.4.0.dist-info → djresttoolkit-0.6.0.dist-info}/WHEEL +0 -0
- {djresttoolkit-0.4.0.dist-info → djresttoolkit-0.6.0.dist-info}/entry_points.txt +0 -0
- {djresttoolkit-0.4.0.dist-info → djresttoolkit-0.6.0.dist-info}/licenses/LICENSE +0 -0
README.md
CHANGED
@@ -30,7 +30,197 @@ djresttoolkit is a collection of utilities and helpers for Django and Django RES
|
|
30
30
|
|
31
31
|
## 📚 API Reference
|
32
32
|
|
33
|
-
###
|
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.
|
34
224
|
|
35
225
|
## 🛠️ Planned Features
|
36
226
|
|
@@ -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.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: djresttoolkit
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.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
|
@@ -46,6 +46,7 @@ Classifier: Topic :: Software Development :: Libraries
|
|
46
46
|
Classifier: Topic :: Utilities
|
47
47
|
Classifier: Typing :: Typed
|
48
48
|
Requires-Python: >=3.13
|
49
|
+
Requires-Dist: faker>=37.5.3
|
49
50
|
Requires-Dist: pydantic>=2.11.7
|
50
51
|
Provides-Extra: dev
|
51
52
|
Requires-Dist: mypy; extra == 'dev'
|
@@ -85,7 +86,197 @@ djresttoolkit is a collection of utilities and helpers for Django and Django RES
|
|
85
86
|
|
86
87
|
## 📚 API Reference
|
87
88
|
|
88
|
-
###
|
89
|
+
### 1. EmailSender
|
90
|
+
|
91
|
+
```python
|
92
|
+
from djresttoolkit.mail import EmailSender, EmailContent, EmailTemplate
|
93
|
+
```
|
94
|
+
|
95
|
+
### `EmailSender`
|
96
|
+
|
97
|
+
Send templated emails.
|
98
|
+
|
99
|
+
#### Init
|
100
|
+
|
101
|
+
```python
|
102
|
+
EmailSender(email_content: EmailContent | EmailContentDict)
|
103
|
+
```
|
104
|
+
|
105
|
+
#### Methods
|
106
|
+
|
107
|
+
```python
|
108
|
+
send(to: list[str], exceptions: bool = False) -> bool
|
109
|
+
```
|
110
|
+
|
111
|
+
- `to`: recipient emails
|
112
|
+
- `exceptions`: raise on error if `True`, else logs error
|
113
|
+
- Returns `True` if sent, `False` on failure
|
114
|
+
|
115
|
+
#### Example
|
116
|
+
|
117
|
+
```python
|
118
|
+
content = EmailContent(
|
119
|
+
subject="Hello",
|
120
|
+
from_email="noreply@example.com",
|
121
|
+
context={"username": "Alice"},
|
122
|
+
template=EmailTemplate(
|
123
|
+
text="emails/welcome.txt",
|
124
|
+
html="emails/welcome.html"
|
125
|
+
)
|
126
|
+
)
|
127
|
+
EmailSender(content).send(to=["user@example.com"])
|
128
|
+
```
|
129
|
+
|
130
|
+
#### `EmailContent`
|
131
|
+
|
132
|
+
- `subject`, `from_email`, `context`, `template` (EmailTemplate)
|
133
|
+
|
134
|
+
#### `EmailTemplate`
|
135
|
+
|
136
|
+
- `text`, `html` — template file paths
|
137
|
+
|
138
|
+
### 2. Custom DRF Exception Handler
|
139
|
+
|
140
|
+
```python
|
141
|
+
from djresttoolkit.views import exception_handler
|
142
|
+
```
|
143
|
+
|
144
|
+
### `exception_handler(exc: Exception, context: dict[str, Any]) -> Response | None`
|
145
|
+
|
146
|
+
A DRF exception handler that:
|
147
|
+
|
148
|
+
- Preserves DRF’s default exception behavior.
|
149
|
+
- Adds throttling support (defaults to `AnonRateThrottle`).
|
150
|
+
- Returns **429 Too Many Requests** with `retry_after` if throttle limit is exceeded.
|
151
|
+
|
152
|
+
#### Parameters
|
153
|
+
|
154
|
+
- `exc`: Exception object.
|
155
|
+
- `context`: DRF context dictionary containing `"request"` and `"view"`.
|
156
|
+
|
157
|
+
#### Returns
|
158
|
+
|
159
|
+
- `Response` — DRF Response object (with throttling info if applicable), or `None`.
|
160
|
+
|
161
|
+
#### Settings Configuration
|
162
|
+
|
163
|
+
In `settings.py`:
|
164
|
+
|
165
|
+
```python
|
166
|
+
REST_FRAMEWORK = {
|
167
|
+
'EXCEPTION_HANDLER': 'djresttoolkit.views.exception_handler',
|
168
|
+
# Other DRF settings...
|
169
|
+
}
|
170
|
+
```
|
171
|
+
|
172
|
+
#### Throttle Behavior
|
173
|
+
|
174
|
+
- Uses `view.throttle_classes` if defined, else defaults to `AnonRateThrottle`.
|
175
|
+
- Tracks requests in cache and calculates `retry_after`.
|
176
|
+
- Cleans expired timestamps automatically.
|
177
|
+
|
178
|
+
### 3. Response Time Middleware
|
179
|
+
|
180
|
+
```python
|
181
|
+
from djresttoolkit.middlewares import ResponseTimeMiddleware
|
182
|
+
```
|
183
|
+
|
184
|
+
### `ResponseTimeMiddleware`
|
185
|
+
|
186
|
+
Middleware to calculate and log **HTTP response time** for each request.
|
187
|
+
|
188
|
+
#### Constructor from ResponseTimeMiddleware
|
189
|
+
|
190
|
+
```python
|
191
|
+
ResponseTimeMiddleware(get_response: Callable[[HttpRequest], HttpResponse])
|
192
|
+
```
|
193
|
+
|
194
|
+
- `get_response`: The next middleware or view callable.
|
195
|
+
|
196
|
+
#### Usage
|
197
|
+
|
198
|
+
Add it to your Django `MIDDLEWARE` in `settings.py`:
|
199
|
+
|
200
|
+
```python
|
201
|
+
MIDDLEWARE = [
|
202
|
+
# Other middlewares...
|
203
|
+
'djresttoolkit.middlewares.ResponseTimeMiddleware',
|
204
|
+
]
|
205
|
+
```
|
206
|
+
|
207
|
+
#### Behavior
|
208
|
+
|
209
|
+
- Measures the time taken to process each request.
|
210
|
+
- Adds a header `X-Response-Time` to each HTTP response.
|
211
|
+
- Logs the response time using Django's logging system.
|
212
|
+
|
213
|
+
#### The response headers will include
|
214
|
+
|
215
|
+
```json
|
216
|
+
X-Response-Time: 0.01234 seconds
|
217
|
+
```
|
218
|
+
|
219
|
+
#### Logs a message
|
220
|
+
|
221
|
+
```bash
|
222
|
+
INFO: Request processed in 0.01234 seconds
|
223
|
+
```
|
224
|
+
|
225
|
+
### 4. Throttle Utilities
|
226
|
+
|
227
|
+
#### `ThrottleInfoJSONRenderer`
|
228
|
+
|
229
|
+
```python
|
230
|
+
from djresttoolkit.renderers import ThrottleInfoJSONRenderer
|
231
|
+
```
|
232
|
+
|
233
|
+
A custom DRF JSON renderer that **automatically attaches throttle information to response headers**.
|
234
|
+
|
235
|
+
#### Usage (settings.py)
|
236
|
+
|
237
|
+
```python
|
238
|
+
REST_FRAMEWORK = {
|
239
|
+
"DEFAULT_RENDERER_CLASSES": [
|
240
|
+
"djresttoolkit.renderers.ThrottleInfoJSONRenderer",
|
241
|
+
"rest_framework.renderers.BrowsableAPIRenderer",
|
242
|
+
],
|
243
|
+
}
|
244
|
+
```
|
245
|
+
|
246
|
+
When enabled, every response includes throttle headers like:
|
247
|
+
|
248
|
+
```plaintext
|
249
|
+
X-Throttle-User-Limit: 100
|
250
|
+
X-Throttle-User-Remaining: 98
|
251
|
+
X-Throttle-User-Reset: 2025-08-18T07:30:00Z
|
252
|
+
X-Throttle-User-Retry-After: 0
|
253
|
+
```
|
254
|
+
|
255
|
+
#### `ThrottleInspector`
|
256
|
+
|
257
|
+
```python
|
258
|
+
from djresttoolkit.throttling import ThrottleInspector
|
259
|
+
```
|
260
|
+
|
261
|
+
Utility class to **inspect DRF throttle usage** for a view or request.
|
262
|
+
|
263
|
+
#### Constructor for ThrottleInspector
|
264
|
+
|
265
|
+
```python
|
266
|
+
ThrottleInspector(
|
267
|
+
view: APIView,
|
268
|
+
request: Request | None = None,
|
269
|
+
throttle_classes: list[type[BaseThrottle]] | None = None,
|
270
|
+
)
|
271
|
+
```
|
272
|
+
|
273
|
+
#### Key Methods
|
274
|
+
|
275
|
+
- `get_details() -> dict[str, Any]`
|
276
|
+
Returns structured throttle info: limit, remaining, reset time, retry\_after.
|
277
|
+
|
278
|
+
- `attach_headers(response: Response, throttle_info: dict | None)`
|
279
|
+
Attaches throttle data to HTTP headers.
|
89
280
|
|
90
281
|
## 🛠️ Planned Features
|
91
282
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
|
2
|
+
README.md,sha256=mgYuT5MNL2ZmWzXLyU3VN0FA0sgyRM85Xn5EKxOM0Bs,5562
|
3
|
+
demo/staticfiles/admin/img/LICENSE,sha256=0RT6_zSIwWwxmzI13EH5AjnT1j2YU3MwM9j3U19cAAQ,1081
|
4
|
+
src/djresttoolkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
src/djresttoolkit/admin.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
src/djresttoolkit/apps.py,sha256=nKb5GUIEhAB3IL3lTmEXNc5XuvvaZupH-1CCuYKFrEQ,158
|
7
|
+
src/djresttoolkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
src/djresttoolkit/dbseed/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
src/djresttoolkit/dbseed/models/__init__.py,sha256=SpAEE8mfd_cX8qgI-CeC4r1e17YLAwAlxeFgbK8gtE4,118
|
10
|
+
src/djresttoolkit/dbseed/models/_base_seed_model.py,sha256=DlbmdafLicKd_ja1AIvKKzyBxdnxdq23zPJwlqxiTl8,2187
|
11
|
+
src/djresttoolkit/dbseed/models/_gen.py,sha256=qBPQaLvh1rcEam0YmE4JBJqpa-Vv5IFlIIagkEMHDVw,206
|
12
|
+
src/djresttoolkit/mail/__init__.py,sha256=tB9SdMlhfWQ640q4aobZ0H1c7fTWalpDL2I-onkr2VI,268
|
13
|
+
src/djresttoolkit/mail/_email_sender.py,sha256=bPMqgD5HibJcOZgO6xxHOhdK9HEhnGNC6BoMPpo-h7k,3096
|
14
|
+
src/djresttoolkit/mail/_models.py,sha256=of5KsLGvsN2OWgDYgdtLEijulg817TXgsLKuUdsnDQc,1447
|
15
|
+
src/djresttoolkit/mail/_types.py,sha256=zf6CcXR1ei_UmZ1nLAJa378OAJ6ftnBICqEOkzXPNw8,646
|
16
|
+
src/djresttoolkit/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
+
src/djresttoolkit/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
|
+
src/djresttoolkit/management/commands/dbseed.py,sha256=tMpvvDx_es08GxYKgJ1umJddh_kmIB1Fy0J5Ich2hiE,4248
|
19
|
+
src/djresttoolkit/middlewares/__init__.py,sha256=GZHU3Yy4xXoEi62tHn0UJNxN6XgGM2_HES8Bt5AS5Lk,100
|
20
|
+
src/djresttoolkit/middlewares/_response_time_middleware.py,sha256=1wCwdkW5Ng6HJo8zx0F7ylms84OGP-1K0kbyG6Vacuk,908
|
21
|
+
src/djresttoolkit/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
|
+
src/djresttoolkit/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
|
+
src/djresttoolkit/renderers/__init__.py,sha256=kmFMPRiMfD8CuJTN1_-6Z_Hqil3x8GBM0IN1roZESm0,107
|
24
|
+
src/djresttoolkit/renderers/_throttle_info_json_renderer.py,sha256=aP2cN4cB_Imcpy732zsPBQrMQqcKEs5R3dld5Y_4AMU,1089
|
25
|
+
src/djresttoolkit/throttling/__init__.py,sha256=01sjMymjx8XjqnAw3bEBLc-JtfhCDrp5dGxSNXMvPpU,84
|
26
|
+
src/djresttoolkit/throttling/_throttle_inspector.py,sha256=Kss6ZxKy-EXq9UGaGprGDhpSuJ5992bmEYZSWmUVBHo,6480
|
27
|
+
src/djresttoolkit/views/__init__.py,sha256=XrxBrs6sH4HmUzp41omcmy_y94pSaXAVn01ttQ022-4,76
|
28
|
+
src/djresttoolkit/views/_exceptions/__init__.py,sha256=DrCUxuPNyBR4WhzNutn5HDxLa--q51ykIxSG7_bFsOI,83
|
29
|
+
src/djresttoolkit/views/_exceptions/_exception_handler.py,sha256=_o7If47bzWLl57LeSXSWsIDsJGo2RIpwYAwNQ-hsHVY,2839
|
30
|
+
djresttoolkit-0.6.0.dist-info/METADATA,sha256=KKviIBCDtQeoaRNcs8w5zZvlTNVyK1KJXwqNo7hcogM,8501
|
31
|
+
djresttoolkit-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
32
|
+
djresttoolkit-0.6.0.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
|
33
|
+
djresttoolkit-0.6.0.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
|
34
|
+
djresttoolkit-0.6.0.dist-info/RECORD,,
|
File without changes
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from django.db.models import ForeignKey, ManyToManyField, Model, OneToOneField
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
|
9
|
+
class BaseSeedModel(BaseModel):
|
10
|
+
"""
|
11
|
+
Base class for all fake data models.
|
12
|
+
Each subclass must define a `model` attribute.
|
13
|
+
"""
|
14
|
+
|
15
|
+
class Meta:
|
16
|
+
model: type[Model]
|
17
|
+
|
18
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
19
|
+
super().__init_subclass__(**kwargs)
|
20
|
+
if not hasattr(cls, "Meta") or not hasattr(cls.Meta, "model"):
|
21
|
+
raise TypeError(
|
22
|
+
f"{cls.__name__} must define a Meta class with a Django model"
|
23
|
+
)
|
24
|
+
|
25
|
+
@classmethod
|
26
|
+
def get_meta(cls) -> type[Meta]:
|
27
|
+
"""Class-level access."""
|
28
|
+
return cls.Meta
|
29
|
+
|
30
|
+
@classmethod
|
31
|
+
def create_instance(cls) -> tuple[dict[str, Any], list[ManyToManyField[Any, Any]]]:
|
32
|
+
"""Handle ForeignKey, OneToOneField and ManyToMany relationship."""
|
33
|
+
|
34
|
+
# dump pydantic model to python dict
|
35
|
+
data = cls().model_dump()
|
36
|
+
|
37
|
+
# Handle ForeignKey and OneToOneField
|
38
|
+
for field in cls.get_meta().model._meta.get_fields():
|
39
|
+
if isinstance(field, (ForeignKey, OneToOneField)):
|
40
|
+
rel_model = field.remote_field.model
|
41
|
+
if rel_model.objects.exists():
|
42
|
+
# For OneToOne, must ensure unique (pick unused relation)
|
43
|
+
if isinstance(field, OneToOneField):
|
44
|
+
used_ids = cls.get_meta().model.objects.values_list(
|
45
|
+
field.name, flat=True
|
46
|
+
)
|
47
|
+
available = rel_model.objects.exclude(pk__in=used_ids)
|
48
|
+
if available.exists():
|
49
|
+
data[field.name] = available.order_by("?").first()
|
50
|
+
else: # Normal ForeignKey
|
51
|
+
data[field.name] = rel_model.objects.order_by("?").first()
|
52
|
+
|
53
|
+
# Collect ManyToMany fields
|
54
|
+
m2m_fields: list[ManyToManyField[Any, Any]] = [
|
55
|
+
field
|
56
|
+
for field in cls.get_meta().model._meta.get_fields()
|
57
|
+
if isinstance(field, ManyToManyField)
|
58
|
+
]
|
59
|
+
return data, m2m_fields
|
@@ -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
|
src/djresttoolkit/mail/_types.py
CHANGED
@@ -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
|
File without changes
|
File without changes
|
@@ -0,0 +1,115 @@
|
|
1
|
+
import pkgutil
|
2
|
+
from importlib import import_module
|
3
|
+
from types import ModuleType
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from django.apps import apps
|
7
|
+
from django.core.management.base import BaseCommand, CommandParser
|
8
|
+
from django.db import transaction
|
9
|
+
from django.db.models import Model, QuerySet
|
10
|
+
|
11
|
+
from djresttoolkit.dbseed.models import BaseSeedModel
|
12
|
+
|
13
|
+
|
14
|
+
class Command(BaseCommand):
|
15
|
+
help = "Seed the database with fake data for any Django model"
|
16
|
+
|
17
|
+
def add_arguments(self, parser: CommandParser) -> None:
|
18
|
+
"""Add command arguments."""
|
19
|
+
parser.add_argument(
|
20
|
+
"--count",
|
21
|
+
type=int,
|
22
|
+
default=5,
|
23
|
+
help="Number of records per model",
|
24
|
+
)
|
25
|
+
parser.add_argument(
|
26
|
+
"--model",
|
27
|
+
type=str,
|
28
|
+
default=None,
|
29
|
+
help="Specific model name to seed (e.g., User, Product)",
|
30
|
+
)
|
31
|
+
parser.add_argument(
|
32
|
+
"--seed",
|
33
|
+
type=int,
|
34
|
+
default=None,
|
35
|
+
help="Optional Faker seed for reproducible data",
|
36
|
+
)
|
37
|
+
|
38
|
+
def handle(self, *args: Any, **options: Any) -> None:
|
39
|
+
"""Handle ForeignKey, OneToOneField and ManyToMany relationship."""
|
40
|
+
|
41
|
+
count = options["count"]
|
42
|
+
model_name = options["model"]
|
43
|
+
seed = options["seed"]
|
44
|
+
|
45
|
+
if seed is not None:
|
46
|
+
from faker import Faker
|
47
|
+
|
48
|
+
faker = Faker()
|
49
|
+
faker.seed_instance(seed)
|
50
|
+
|
51
|
+
seed_model_classes: list[type[BaseSeedModel]] = []
|
52
|
+
|
53
|
+
# Discover all dbseed dirs in installed apps
|
54
|
+
for app_config in apps.get_app_configs():
|
55
|
+
try:
|
56
|
+
module: ModuleType = import_module(f"{app_config.name}.dbseed")
|
57
|
+
except ModuleNotFoundError:
|
58
|
+
continue # app has no dbseed dir
|
59
|
+
|
60
|
+
# Iterate over modules in the dbseed package
|
61
|
+
for _, name, ispkg in pkgutil.iter_modules(module.__path__):
|
62
|
+
if ispkg:
|
63
|
+
continue
|
64
|
+
|
65
|
+
submodule = import_module(f"{app_config.name}.dbseed.{name}")
|
66
|
+
for attr_name in dir(submodule):
|
67
|
+
attr = getattr(submodule, attr_name)
|
68
|
+
if (
|
69
|
+
isinstance(attr, type)
|
70
|
+
and issubclass(attr, BaseSeedModel)
|
71
|
+
and attr is not BaseSeedModel
|
72
|
+
):
|
73
|
+
# Filter by model name if provided
|
74
|
+
if (
|
75
|
+
not model_name
|
76
|
+
or model_name.lower()
|
77
|
+
== attr.__name__.replace("DBSeedModel", "").lower()
|
78
|
+
):
|
79
|
+
seed_model_classes.append(attr)
|
80
|
+
|
81
|
+
if not seed_model_classes:
|
82
|
+
self.stdout.write(self.style.WARNING("No matching dbseed models found."))
|
83
|
+
return None
|
84
|
+
|
85
|
+
# Generate fake data for each discovered dbseed model
|
86
|
+
for dbseed_cls in seed_model_classes:
|
87
|
+
django_model = dbseed_cls.get_meta().model
|
88
|
+
created_count: int = 0
|
89
|
+
|
90
|
+
for _ in range(count):
|
91
|
+
try:
|
92
|
+
with transaction.atomic():
|
93
|
+
data, m2m_fields = dbseed_cls.create_instance()
|
94
|
+
obj = django_model.objects.create(**data)
|
95
|
+
created_count += 1
|
96
|
+
|
97
|
+
# Assign ManyToMany fields
|
98
|
+
for m2m_field in m2m_fields:
|
99
|
+
rel_model = m2m_field.remote_field.model
|
100
|
+
related_instances: QuerySet[Model] | list[Any] = (
|
101
|
+
rel_model.objects.order_by("?")[:2]
|
102
|
+
if rel_model.objects.exists()
|
103
|
+
else []
|
104
|
+
)
|
105
|
+
getattr(obj, m2m_field.name).set(related_instances)
|
106
|
+
except Exception as error:
|
107
|
+
self.stderr.write(
|
108
|
+
f"Error creating {django_model.__name__} instance: {error}"
|
109
|
+
)
|
110
|
+
continue
|
111
|
+
self.stdout.write(
|
112
|
+
self.style.SUCCESS(
|
113
|
+
f"{created_count} records inserted for {django_model.__name__}"
|
114
|
+
)
|
115
|
+
)
|
@@ -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)
|
@@ -1,22 +0,0 @@
|
|
1
|
-
LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
|
2
|
-
README.md,sha256=P0hcRXIpkEZZIiyWKsvuCkOIlqPxQ4tRkdb5R_mTARQ,1464
|
3
|
-
src/djresttoolkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
src/djresttoolkit/admin.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
src/djresttoolkit/apps.py,sha256=nKb5GUIEhAB3IL3lTmEXNc5XuvvaZupH-1CCuYKFrEQ,158
|
6
|
-
src/djresttoolkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
-
src/djresttoolkit/mail/__init__.py,sha256=tB9SdMlhfWQ640q4aobZ0H1c7fTWalpDL2I-onkr2VI,268
|
8
|
-
src/djresttoolkit/mail/_email_sender.py,sha256=vvTPZzSAfX2FXHv1IY0ST8dEsq8M4wAxkihm0JbRh1Y,3136
|
9
|
-
src/djresttoolkit/mail/_models.py,sha256=_41pH3xC0jP8SHSty2FkxvRh2_ddKk-4peT11OHcBBE,1462
|
10
|
-
src/djresttoolkit/mail/_types.py,sha256=est1mrN80vB_4-j-2yuAr_l_7rNzO4AJlqpq74zO5ow,682
|
11
|
-
src/djresttoolkit/middlewares/__init__.py,sha256=GZHU3Yy4xXoEi62tHn0UJNxN6XgGM2_HES8Bt5AS5Lk,100
|
12
|
-
src/djresttoolkit/middlewares/_response_time_middleware.py,sha256=1wCwdkW5Ng6HJo8zx0F7ylms84OGP-1K0kbyG6Vacuk,908
|
13
|
-
src/djresttoolkit/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
|
-
src/djresttoolkit/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
-
src/djresttoolkit/views/__init__.py,sha256=XrxBrs6sH4HmUzp41omcmy_y94pSaXAVn01ttQ022-4,76
|
16
|
-
src/djresttoolkit/views/_exceptions/__init__.py,sha256=DrCUxuPNyBR4WhzNutn5HDxLa--q51ykIxSG7_bFsOI,83
|
17
|
-
src/djresttoolkit/views/_exceptions/_exception_handler.py,sha256=lg6kfAABex-UbAsF78uX-M-ZnJ3u1vK1eIXytb-4KSw,2375
|
18
|
-
djresttoolkit-0.4.0.dist-info/METADATA,sha256=FGDP5PR0AZoaPvEyrG95DVC516vGOWlXk2ZgyLfm8kE,4374
|
19
|
-
djresttoolkit-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
20
|
-
djresttoolkit-0.4.0.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
|
21
|
-
djresttoolkit-0.4.0.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
|
22
|
-
djresttoolkit-0.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|