djresttoolkit 0.4.0__tar.gz → 0.6.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 (33) hide show
  1. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/.gitignore +2 -0
  2. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/PKG-INFO +193 -2
  3. djresttoolkit-0.6.0/README.md +239 -0
  4. djresttoolkit-0.6.0/demo/staticfiles/admin/img/LICENSE +20 -0
  5. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/pyproject.toml +8 -1
  6. djresttoolkit-0.6.0/src/djresttoolkit/dbseed/models/__init__.py +4 -0
  7. djresttoolkit-0.6.0/src/djresttoolkit/dbseed/models/_base_seed_model.py +59 -0
  8. djresttoolkit-0.6.0/src/djresttoolkit/dbseed/models/_gen.py +11 -0
  9. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/src/djresttoolkit/mail/_email_sender.py +2 -3
  10. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/src/djresttoolkit/mail/_models.py +2 -2
  11. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/src/djresttoolkit/mail/_types.py +1 -3
  12. djresttoolkit-0.6.0/src/djresttoolkit/management/commands/dbseed.py +115 -0
  13. djresttoolkit-0.6.0/src/djresttoolkit/migrations/__init__.py +0 -0
  14. djresttoolkit-0.6.0/src/djresttoolkit/models/__init__.py +0 -0
  15. djresttoolkit-0.6.0/src/djresttoolkit/py.typed +0 -0
  16. djresttoolkit-0.6.0/src/djresttoolkit/renderers/__init__.py +3 -0
  17. djresttoolkit-0.6.0/src/djresttoolkit/renderers/_throttle_info_json_renderer.py +33 -0
  18. djresttoolkit-0.6.0/src/djresttoolkit/throttling/__init__.py +3 -0
  19. djresttoolkit-0.6.0/src/djresttoolkit/throttling/_throttle_inspector.py +179 -0
  20. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/src/djresttoolkit/views/_exceptions/_exception_handler.py +11 -0
  21. djresttoolkit-0.4.0/README.md +0 -49
  22. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/LICENSE +0 -0
  23. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/src/djresttoolkit/__init__.py +0 -0
  24. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/src/djresttoolkit/admin.py +0 -0
  25. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/src/djresttoolkit/apps.py +0 -0
  26. {djresttoolkit-0.4.0/src/djresttoolkit/migrations → djresttoolkit-0.6.0/src/djresttoolkit/dbseed}/__init__.py +0 -0
  27. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/src/djresttoolkit/mail/__init__.py +0 -0
  28. {djresttoolkit-0.4.0/src/djresttoolkit/models → djresttoolkit-0.6.0/src/djresttoolkit/management}/__init__.py +0 -0
  29. /djresttoolkit-0.4.0/src/djresttoolkit/py.typed → /djresttoolkit-0.6.0/src/djresttoolkit/management/commands/__init__.py +0 -0
  30. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/src/djresttoolkit/middlewares/__init__.py +0 -0
  31. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/src/djresttoolkit/middlewares/_response_time_middleware.py +0 -0
  32. {djresttoolkit-0.4.0 → djresttoolkit-0.6.0}/src/djresttoolkit/views/__init__.py +0 -0
  33. {djresttoolkit-0.4.0 → djresttoolkit-0.6.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.4.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
- ### Under the development
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,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.4.0"
7
+ version = "0.6.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" }
@@ -46,6 +46,7 @@ keywords = [
46
46
  ]
47
47
  requires-python = ">=3.13"
48
48
  dependencies = [
49
+ "faker>=37.5.3",
49
50
  "pydantic>=2.11.7",
50
51
  ]
51
52
 
@@ -79,6 +80,12 @@ dev = [
79
80
  "djangorestframework-stubs>=3.16.2",
80
81
  "pytest>=8.4.1",
81
82
  "djresttoolkit",
83
+ "pyyaml>=6.0.2",
84
+ "pydantic-settings>=2.10.1",
85
+ "daphne>=4.2.1",
86
+ "django-redis>=6.0.0",
87
+ "django-cors-headers>=4.7.0",
88
+ "argon2-cffi-bindings>=21.2.0",
82
89
  ]
83
90
 
84
91
  # =====================
@@ -0,0 +1,4 @@
1
+ from ._base_seed_model import BaseSeedModel
2
+ from ._gen import Field, Gen
3
+
4
+ __all__ = ["BaseSeedModel", "Gen", "Field"]
@@ -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
@@ -0,0 +1,11 @@
1
+ from faker import Faker
2
+ from pydantic import Field as PydField
3
+
4
+
5
+ class Generator:
6
+ @classmethod
7
+ def create_faker(cls) -> Faker:
8
+ return Faker()
9
+
10
+ Gen = Generator.create_faker()
11
+ Field = PydField
@@ -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,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
+ )
File without changes
@@ -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