djresttoolkit 0.7.0__tar.gz → 0.9.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 (40) hide show
  1. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/PKG-INFO +142 -8
  2. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/README.md +139 -7
  3. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/pyproject.toml +3 -1
  4. djresttoolkit-0.9.0/src/djresttoolkit/envconfig/__init__.py +3 -0
  5. djresttoolkit-0.9.0/src/djresttoolkit/envconfig/_env_settings.py +84 -0
  6. djresttoolkit-0.9.0/src/djresttoolkit/serializers/__init__.py +0 -0
  7. djresttoolkit-0.9.0/src/djresttoolkit/serializers/_serializer_create_mixin.py +87 -0
  8. djresttoolkit-0.9.0/src/djresttoolkit/serializers/mixins/__init__.py +6 -0
  9. djresttoolkit-0.9.0/src/djresttoolkit/serializers/mixins/_absolute_url_file_mixin.py +94 -0
  10. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/.gitignore +0 -0
  11. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/LICENSE +0 -0
  12. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/demo/staticfiles/admin/img/LICENSE +0 -0
  13. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/__init__.py +0 -0
  14. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/admin.py +0 -0
  15. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/apps.py +0 -0
  16. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/dbseed/__init__.py +0 -0
  17. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/dbseed/models/__init__.py +0 -0
  18. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/dbseed/models/_choice_field.py +0 -0
  19. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/dbseed/models/_gen.py +0 -0
  20. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/dbseed/models/_seed_model.py +0 -0
  21. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/mail/__init__.py +0 -0
  22. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/mail/_email_sender.py +0 -0
  23. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/mail/_models.py +0 -0
  24. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/mail/_types.py +0 -0
  25. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/management/__init__.py +0 -0
  26. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/management/commands/__init__.py +0 -0
  27. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/management/commands/dbflush.py +0 -0
  28. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/management/commands/dbseed.py +0 -0
  29. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/middlewares/__init__.py +0 -0
  30. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/middlewares/_response_time_middleware.py +0 -0
  31. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/migrations/__init__.py +0 -0
  32. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/models/__init__.py +0 -0
  33. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/py.typed +0 -0
  34. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/renderers/__init__.py +0 -0
  35. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/renderers/_throttle_info_json_renderer.py +0 -0
  36. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/throttling/__init__.py +0 -0
  37. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/throttling/_throttle_inspector.py +0 -0
  38. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/views/__init__.py +0 -0
  39. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/views/_exceptions/__init__.py +0 -0
  40. {djresttoolkit-0.7.0 → djresttoolkit-0.9.0}/src/djresttoolkit/views/_exceptions/_exception_handler.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djresttoolkit
3
- Version: 0.7.0
3
+ Version: 0.9.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
@@ -47,7 +47,9 @@ Classifier: Topic :: Utilities
47
47
  Classifier: Typing :: Typed
48
48
  Requires-Python: >=3.13
49
49
  Requires-Dist: faker>=37.5.3
50
+ Requires-Dist: pydantic-settings>=2.10.1
50
51
  Requires-Dist: pydantic>=2.11.7
52
+ Requires-Dist: pyyaml>=6.0.2
51
53
  Provides-Extra: dev
52
54
  Requires-Dist: mypy; extra == 'dev'
53
55
  Requires-Dist: pytest; extra == 'dev'
@@ -201,7 +203,79 @@ or
201
203
  Flushed 120 records from all models and reset IDs.
202
204
  ```
203
205
 
204
- ### 3. EmailSender
206
+ ### 3. EnvBaseSettings
207
+
208
+ ```python
209
+ from djresttoolkit.envconfig import EnvBaseSettings
210
+ ```
211
+
212
+ #### `EnvBaseSettings`
213
+
214
+ A **base settings class** for managing application configuration using:
215
+
216
+ - YAML files (default `.environ.yaml`)
217
+ - Environment variables (default `.env`)
218
+
219
+ Supports **nested configuration** using double underscores (`__`) in environment variable names.
220
+
221
+ #### Class Attributes
222
+
223
+ - Attributes
224
+ - `env_file`
225
+ - Type: `str`
226
+ - Default: `.env`
227
+ - Description: Environment variable file path.
228
+ - `yaml_file`
229
+ - Type: `str`
230
+ - Default: `.environ.yaml`
231
+ - Description: YAML configuration file path.
232
+ - `model_config`
233
+ - Type: `SettingsConfigDict`
234
+ - Description: Pydantic settings configuration (file encoding, nested delimiter).
235
+
236
+ #### Methods
237
+
238
+ #### `load(cls, *, env_file: str | None = None, ymal_file: str | None = None, warning: bool = True) -> EnvBaseSettings`
239
+
240
+ Loads configuration from **YAML first**, then overrides with **environment variables**.
241
+
242
+ #### Parameters
243
+
244
+ - `env_file` — Optional custom `.env` file path.
245
+ - `ymal_file` — Optional custom YAML file path.
246
+ - `warning` — Emit a warning if YAML file is missing (default `True`).
247
+
248
+ #### Returns
249
+
250
+ - Instance of `EnvBaseSettings` (or subclass) with loaded configuration.
251
+
252
+ #### Raises
253
+
254
+ - `UserWarning` if YAML file not found and `warning=True`.
255
+
256
+ ### Usage Example
257
+
258
+ ```python
259
+ from djresttoolkit.envconfig import EnvBaseSettings
260
+
261
+ class EnvSettings(EnvBaseSettings):
262
+ debug: bool = False
263
+ database_url: str
264
+
265
+ # Load settings
266
+ settings = EnvSettings.load(warning=False)
267
+
268
+ print(settings.debug)
269
+ print(settings.database_url)
270
+ ```
271
+
272
+ #### Features
273
+
274
+ - Prioritizes `.env` variables over YAML.
275
+ - Supports nested keys: `DATABASE__HOST` → `settings.database.host`.
276
+ - Designed to be subclassed for project-specific settings.
277
+
278
+ ### 4. EmailSender
205
279
 
206
280
  ```python
207
281
  from djresttoolkit.mail import EmailSender, EmailContent, EmailTemplate
@@ -217,7 +291,7 @@ Send templated emails.
217
291
  EmailSender(email_content: EmailContent | EmailContentDict)
218
292
  ```
219
293
 
220
- #### Methods
294
+ #### EmailSender Methods
221
295
 
222
296
  ```python
223
297
  send(to: list[str], exceptions: bool = False) -> bool
@@ -250,7 +324,7 @@ EmailSender(content).send(to=["user@example.com"])
250
324
 
251
325
  - `text`, `html` — template file paths
252
326
 
253
- ### 4. Custom DRF Exception Handler
327
+ ### 5. Custom DRF Exception Handler
254
328
 
255
329
  ```python
256
330
  from djresttoolkit.views import exception_handler
@@ -264,12 +338,12 @@ A DRF exception handler that:
264
338
  - Adds throttling support (defaults to `AnonRateThrottle`).
265
339
  - Returns **429 Too Many Requests** with `retry_after` if throttle limit is exceeded.
266
340
 
267
- #### Parameters
341
+ #### Exception Handler Parameters
268
342
 
269
343
  - `exc`: Exception object.
270
344
  - `context`: DRF context dictionary containing `"request"` and `"view"`.
271
345
 
272
- #### Returns
346
+ #### Returns Type of Exception Handler
273
347
 
274
348
  - `Response` — DRF Response object (with throttling info if applicable), or `None`.
275
349
 
@@ -290,7 +364,7 @@ REST_FRAMEWORK = {
290
364
  - Tracks requests in cache and calculates `retry_after`.
291
365
  - Cleans expired timestamps automatically.
292
366
 
293
- ### 5. Response Time Middleware
367
+ ### 6. Response Time Middleware
294
368
 
295
369
  ```python
296
370
  from djresttoolkit.middlewares import ResponseTimeMiddleware
@@ -337,7 +411,7 @@ X-Response-Time: 0.01234 seconds
337
411
  INFO: Request processed in 0.01234 seconds
338
412
  ```
339
413
 
340
- ### 6. Throttle Utilities
414
+ ### 7. Throttle Utilities
341
415
 
342
416
  #### `ThrottleInfoJSONRenderer`
343
417
 
@@ -393,6 +467,66 @@ ThrottleInspector(
393
467
  - `attach_headers(response: Response, throttle_info: dict | None)`
394
468
  Attaches throttle data to HTTP headers.
395
469
 
470
+ ### 8. AbsoluteUrlFileMixin — API Reference
471
+
472
+ ```python
473
+ from djresttoolkit.serializers.mixins import AbsoluteUrlFileMixin
474
+ ```
475
+
476
+ ### `AbsoluteUrlFileMixin`
477
+
478
+ A **serializer mixin** that converts **FileField** and **ImageField** URLs to **absolute URLs**, ensuring compatibility with cloud storage backends.
479
+
480
+ ---
481
+
482
+ ### Attributes
483
+
484
+ - `file_fields`
485
+ - type: `list[str] | None`
486
+ - default: `None`
487
+ - description: Manual list of file field names for non-model serializers.
488
+
489
+ ### Absolute Url File Mixin Methods
490
+
491
+ #### `to_representation(self, instance: Any) -> dict[str, Any]`
492
+
493
+ - Overrides default serializer `to_representation`.
494
+ - Enhances all file-related fields in the serialized output to **absolute URLs**.
495
+
496
+ #### `enhance_file_fields(self, instance: Any, representation: dict[str, Any], request: Any) -> dict[str, Any]`
497
+
498
+ - Core logic to process each file field.
499
+ - Converts relative URLs to absolute URLs using `request.build_absolute_uri()`.
500
+ - Supports model serializers or manual `file_fields`.
501
+ - Logs warnings if request context is missing or file is not found.
502
+
503
+ #### Exceptions
504
+
505
+ - `MissingRequestContext`: Raised if the request object is missing in serializer context and `DEBUG=True`.
506
+
507
+ ### Absolute Url File Mixin Example
508
+
509
+ ```python
510
+ from rest_framework import serializers
511
+ from djresttoolkit.serializers.mixins import AbsoluteUrlFileMixin
512
+ from myapp.models import Document
513
+
514
+ class DocumentSerializer(AbsoluteUrlFileMixin, serializers.ModelSerializer):
515
+ class Meta:
516
+ model = Document
517
+ fields = ["id", "title", "file"]
518
+
519
+ # Output will convert `file` field to an absolute URL
520
+ serializer = DocumentSerializer(instance, context={"request": request})
521
+ data = serializer.data
522
+ ```
523
+
524
+ #### Notes
525
+
526
+ - Works with both Django model serializers and custom serializers.
527
+ - Relative file paths are automatically converted to absolute URLs.
528
+ - Can manually specify fields via `file_fields` for non-model serializers.
529
+
396
530
  ## 🛠️ Planned Features
397
531
 
398
532
  - Add more utils
@@ -145,7 +145,79 @@ or
145
145
  Flushed 120 records from all models and reset IDs.
146
146
  ```
147
147
 
148
- ### 3. EmailSender
148
+ ### 3. EnvBaseSettings
149
+
150
+ ```python
151
+ from djresttoolkit.envconfig import EnvBaseSettings
152
+ ```
153
+
154
+ #### `EnvBaseSettings`
155
+
156
+ A **base settings class** for managing application configuration using:
157
+
158
+ - YAML files (default `.environ.yaml`)
159
+ - Environment variables (default `.env`)
160
+
161
+ Supports **nested configuration** using double underscores (`__`) in environment variable names.
162
+
163
+ #### Class Attributes
164
+
165
+ - Attributes
166
+ - `env_file`
167
+ - Type: `str`
168
+ - Default: `.env`
169
+ - Description: Environment variable file path.
170
+ - `yaml_file`
171
+ - Type: `str`
172
+ - Default: `.environ.yaml`
173
+ - Description: YAML configuration file path.
174
+ - `model_config`
175
+ - Type: `SettingsConfigDict`
176
+ - Description: Pydantic settings configuration (file encoding, nested delimiter).
177
+
178
+ #### Methods
179
+
180
+ #### `load(cls, *, env_file: str | None = None, ymal_file: str | None = None, warning: bool = True) -> EnvBaseSettings`
181
+
182
+ Loads configuration from **YAML first**, then overrides with **environment variables**.
183
+
184
+ #### Parameters
185
+
186
+ - `env_file` — Optional custom `.env` file path.
187
+ - `ymal_file` — Optional custom YAML file path.
188
+ - `warning` — Emit a warning if YAML file is missing (default `True`).
189
+
190
+ #### Returns
191
+
192
+ - Instance of `EnvBaseSettings` (or subclass) with loaded configuration.
193
+
194
+ #### Raises
195
+
196
+ - `UserWarning` if YAML file not found and `warning=True`.
197
+
198
+ ### Usage Example
199
+
200
+ ```python
201
+ from djresttoolkit.envconfig import EnvBaseSettings
202
+
203
+ class EnvSettings(EnvBaseSettings):
204
+ debug: bool = False
205
+ database_url: str
206
+
207
+ # Load settings
208
+ settings = EnvSettings.load(warning=False)
209
+
210
+ print(settings.debug)
211
+ print(settings.database_url)
212
+ ```
213
+
214
+ #### Features
215
+
216
+ - Prioritizes `.env` variables over YAML.
217
+ - Supports nested keys: `DATABASE__HOST` → `settings.database.host`.
218
+ - Designed to be subclassed for project-specific settings.
219
+
220
+ ### 4. EmailSender
149
221
 
150
222
  ```python
151
223
  from djresttoolkit.mail import EmailSender, EmailContent, EmailTemplate
@@ -161,7 +233,7 @@ Send templated emails.
161
233
  EmailSender(email_content: EmailContent | EmailContentDict)
162
234
  ```
163
235
 
164
- #### Methods
236
+ #### EmailSender Methods
165
237
 
166
238
  ```python
167
239
  send(to: list[str], exceptions: bool = False) -> bool
@@ -194,7 +266,7 @@ EmailSender(content).send(to=["user@example.com"])
194
266
 
195
267
  - `text`, `html` — template file paths
196
268
 
197
- ### 4. Custom DRF Exception Handler
269
+ ### 5. Custom DRF Exception Handler
198
270
 
199
271
  ```python
200
272
  from djresttoolkit.views import exception_handler
@@ -208,12 +280,12 @@ A DRF exception handler that:
208
280
  - Adds throttling support (defaults to `AnonRateThrottle`).
209
281
  - Returns **429 Too Many Requests** with `retry_after` if throttle limit is exceeded.
210
282
 
211
- #### Parameters
283
+ #### Exception Handler Parameters
212
284
 
213
285
  - `exc`: Exception object.
214
286
  - `context`: DRF context dictionary containing `"request"` and `"view"`.
215
287
 
216
- #### Returns
288
+ #### Returns Type of Exception Handler
217
289
 
218
290
  - `Response` — DRF Response object (with throttling info if applicable), or `None`.
219
291
 
@@ -234,7 +306,7 @@ REST_FRAMEWORK = {
234
306
  - Tracks requests in cache and calculates `retry_after`.
235
307
  - Cleans expired timestamps automatically.
236
308
 
237
- ### 5. Response Time Middleware
309
+ ### 6. Response Time Middleware
238
310
 
239
311
  ```python
240
312
  from djresttoolkit.middlewares import ResponseTimeMiddleware
@@ -281,7 +353,7 @@ X-Response-Time: 0.01234 seconds
281
353
  INFO: Request processed in 0.01234 seconds
282
354
  ```
283
355
 
284
- ### 6. Throttle Utilities
356
+ ### 7. Throttle Utilities
285
357
 
286
358
  #### `ThrottleInfoJSONRenderer`
287
359
 
@@ -337,6 +409,66 @@ ThrottleInspector(
337
409
  - `attach_headers(response: Response, throttle_info: dict | None)`
338
410
  Attaches throttle data to HTTP headers.
339
411
 
412
+ ### 8. AbsoluteUrlFileMixin — API Reference
413
+
414
+ ```python
415
+ from djresttoolkit.serializers.mixins import AbsoluteUrlFileMixin
416
+ ```
417
+
418
+ ### `AbsoluteUrlFileMixin`
419
+
420
+ A **serializer mixin** that converts **FileField** and **ImageField** URLs to **absolute URLs**, ensuring compatibility with cloud storage backends.
421
+
422
+ ---
423
+
424
+ ### Attributes
425
+
426
+ - `file_fields`
427
+ - type: `list[str] | None`
428
+ - default: `None`
429
+ - description: Manual list of file field names for non-model serializers.
430
+
431
+ ### Absolute Url File Mixin Methods
432
+
433
+ #### `to_representation(self, instance: Any) -> dict[str, Any]`
434
+
435
+ - Overrides default serializer `to_representation`.
436
+ - Enhances all file-related fields in the serialized output to **absolute URLs**.
437
+
438
+ #### `enhance_file_fields(self, instance: Any, representation: dict[str, Any], request: Any) -> dict[str, Any]`
439
+
440
+ - Core logic to process each file field.
441
+ - Converts relative URLs to absolute URLs using `request.build_absolute_uri()`.
442
+ - Supports model serializers or manual `file_fields`.
443
+ - Logs warnings if request context is missing or file is not found.
444
+
445
+ #### Exceptions
446
+
447
+ - `MissingRequestContext`: Raised if the request object is missing in serializer context and `DEBUG=True`.
448
+
449
+ ### Absolute Url File Mixin Example
450
+
451
+ ```python
452
+ from rest_framework import serializers
453
+ from djresttoolkit.serializers.mixins import AbsoluteUrlFileMixin
454
+ from myapp.models import Document
455
+
456
+ class DocumentSerializer(AbsoluteUrlFileMixin, serializers.ModelSerializer):
457
+ class Meta:
458
+ model = Document
459
+ fields = ["id", "title", "file"]
460
+
461
+ # Output will convert `file` field to an absolute URL
462
+ serializer = DocumentSerializer(instance, context={"request": request})
463
+ data = serializer.data
464
+ ```
465
+
466
+ #### Notes
467
+
468
+ - Works with both Django model serializers and custom serializers.
469
+ - Relative file paths are automatically converted to absolute URLs.
470
+ - Can manually specify fields via `file_fields` for non-model serializers.
471
+
340
472
  ## 🛠️ Planned Features
341
473
 
342
474
  - Add more utils
@@ -4,7 +4,7 @@
4
4
 
5
5
  [project]
6
6
  name = "djresttoolkit"
7
- version = "0.7.0"
7
+ version = "0.9.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" }
@@ -48,6 +48,8 @@ requires-python = ">=3.13"
48
48
  dependencies = [
49
49
  "faker>=37.5.3",
50
50
  "pydantic>=2.11.7",
51
+ "pydantic-settings>=2.10.1",
52
+ "pyyaml>=6.0.2",
51
53
  ]
52
54
 
53
55
  # CLI scripts entry point configuration
@@ -0,0 +1,3 @@
1
+ from ._env_settings import EnvBaseSettings
2
+
3
+ __all__ = ["EnvBaseSettings"]
@@ -0,0 +1,84 @@
1
+ import warnings
2
+ from pathlib import Path
3
+ from typing import Any, ClassVar
4
+
5
+ import yaml
6
+ from pydantic_settings import BaseSettings, SettingsConfigDict
7
+
8
+
9
+ class EnvBaseSettings[T: "EnvBaseSettings"](BaseSettings):
10
+ """ "
11
+ EnvBaseSettings is a base settings class for managing application configuration
12
+ using both YAML files and environment variables.
13
+ This class is designed to load configuration values from a YAML file first,
14
+ and then override those values with environment variables if present. It supports
15
+ nested configuration using a double underscore (`__`) as the delimiter in
16
+ environment variable names, allowing for hierarchical settings.
17
+
18
+ Class Attributes:
19
+ env_file (str): The default filename for the environment variables file (default: ".env").
20
+ yaml_file (str): The default filename for the YAML configuration file (default: ".environ.yaml").
21
+ model_config (SettingsConfigDict): Configuration for environment variable parsing, including file encoding and nested delimiter.
22
+
23
+ Methods:
24
+ load(cls, *, env_file: str | None = None, ymal_file: str | None = None, warning: bool = True) -> "EnvBaseSettings":
25
+ Loads configuration from a YAML file (if it exists), then overrides with environment variables.
26
+ - env_file: Optional custom path to the .env file.
27
+ - ymal_file: Optional custom path to the YAML file.
28
+ - warning: If True, emits a warning if the YAML file is not found.
29
+ Returns an instance of EnvBaseSettings with the loaded configuration.
30
+
31
+ Usage:
32
+ - Define your settings as subclasses of EnvBaseSettings.
33
+ - Call `YourSettingsClass.load()` to load configuration from files and environment variables.
34
+ - Supports nested configuration via double underscore in environment variable names (e.g., `DATABASE__HOST`).
35
+
36
+ Raises:
37
+ - UserWarning: If the YAML file is not found and `warning` is True.
38
+
39
+ Example:
40
+ ```python
41
+ from djresttoolkit.envconfig import EnvBaseSettings
42
+
43
+ class EnvSettings(EnvBaseSettings):
44
+ debug: bool = False
45
+ database_url: str
46
+
47
+ settings = EnvSettings.load(warning=False)
48
+ ```
49
+
50
+ """
51
+
52
+ env_file: ClassVar[str] = ".env"
53
+ yaml_file: ClassVar[str] = ".environ.yaml"
54
+
55
+ model_config = SettingsConfigDict(
56
+ env_file=env_file,
57
+ env_file_encoding="utf-8",
58
+ env_nested_delimiter="__",
59
+ )
60
+
61
+ @classmethod
62
+ def load(
63
+ cls: type[T],
64
+ *,
65
+ env_file: str | None = None,
66
+ ymal_file: str | None = None,
67
+ warning: bool = True,
68
+ ) -> T:
69
+ """Load from YAML first, then override with .env."""
70
+ if env_file:
71
+ cls.env_file = env_file
72
+ if ymal_file:
73
+ cls.yaml_file = ymal_file
74
+
75
+ config_file = Path(cls.yaml_file)
76
+ yaml_data: dict[str, Any] = {}
77
+ if config_file.exists():
78
+ with config_file.open("r") as f:
79
+ yaml_data = yaml.safe_load(f) or {}
80
+ elif warning:
81
+ msg: str = f"Config file {config_file} not found, using only env vars."
82
+ warnings.warn(msg, UserWarning, stacklevel=1)
83
+
84
+ return cls(**yaml_data)
@@ -0,0 +1,87 @@
1
+ import logging
2
+ from typing import Any, cast
3
+
4
+ from django.core.exceptions import FieldDoesNotExist
5
+ from django.db.models import Model
6
+ from rest_framework.serializers import Field as SerializerField
7
+ from django.db.models import Field as ModelField
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class RecordsCreationMixin:
14
+ """
15
+ A mixin for DRF serializers that supports:
16
+ - Single instance creation with extra context fields
17
+ - Bulk creation from a list of validated_data dicts
18
+ - Updating field error messages with model-specific messages
19
+
20
+ Notes:
21
+ - bulk_create() does not trigger model signals or .save()
22
+ - Meta.model must be defined
23
+ """
24
+
25
+ def create(
26
+ self, validated_data: dict[str, Any] | list[dict[str, Any]]
27
+ ) -> Model | list[Model]:
28
+ logger.debug("Starting creation", extra={"validated_data": validated_data})
29
+
30
+ model: type[Model] | None = getattr(getattr(self, "Meta", None), "model", None)
31
+ if model is None:
32
+ logger.error("Meta.model not defined.")
33
+ raise AttributeError(f"{self.__class__.__name__} missing Meta.model.")
34
+
35
+ # Bulk creation
36
+ if isinstance(validated_data, list):
37
+ instances = [model(**item) for item in validated_data]
38
+ if instances:
39
+ logger.info(
40
+ "Bulk creating instances",
41
+ extra={"count": len(instances), "model": model.__name__},
42
+ )
43
+ return model.objects.bulk_create(instances)
44
+ logger.info("No instances to create.")
45
+ return []
46
+
47
+ # Single instance creation
48
+ if not hasattr(super(), "create"):
49
+ raise NotImplementedError(
50
+ f"{self.__class__.__name__} must be used with a DRF serializer "
51
+ "that implements create()."
52
+ )
53
+
54
+ logger.info("Creating a single instance", extra={"model": model.__name__})
55
+ return super().create({**validated_data}) # type: ignore[misc]
56
+
57
+ def get_fields(self) -> dict[str, SerializerField[Any, Any, Any, Any]]:
58
+ # DRF serializer fields
59
+ fields = cast(
60
+ dict[str, SerializerField[Any, Any, Any, Any]],
61
+ super().get_fields(), # type: ignore
62
+ )
63
+
64
+ meta = getattr(self, "Meta", None)
65
+ model: type[Model] | None = getattr(meta, "model", None)
66
+
67
+ if model is None:
68
+ raise ValueError(f"{self.__class__.__name__}.Meta.model must be defined.")
69
+
70
+ logger.debug("Setting up serializer fields", extra={"model": model.__name__})
71
+
72
+ for field_name, serializer_field in fields.items():
73
+ try:
74
+ # Django model field
75
+ model_field = cast(
76
+ ModelField[Any, Any],
77
+ model._meta.get_field(field_name), # type: ignore
78
+ )
79
+ if hasattr(model_field, "error_messages"):
80
+ serializer_field.error_messages.update(model_field.error_messages)
81
+ except FieldDoesNotExist:
82
+ logger.warning(
83
+ "Skipping serializer field not present on model",
84
+ extra={"field_name": field_name, "model": model.__name__},
85
+ )
86
+
87
+ return fields
@@ -0,0 +1,6 @@
1
+ from ._absolute_url_file_mixin import AbsoluteUrlFileMixin, MissingRequestContext
2
+
3
+ __all__ = [
4
+ "AbsoluteUrlFileMixin",
5
+ "MissingRequestContext",
6
+ ]
@@ -0,0 +1,94 @@
1
+ import logging
2
+ from typing import Any, cast
3
+ from urllib.parse import urlparse
4
+
5
+ from django.conf import settings
6
+ from django.db import models
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class MissingRequestContext(Exception):
12
+ """Custom exception for missing request in serializer context."""
13
+
14
+ ...
15
+
16
+
17
+ class AbsoluteUrlFileMixin:
18
+ """
19
+ A mixin that updates FileField and ImageField URLs in serializer output
20
+ to be absolute URLs, compatible with cloud storage backends.
21
+ """
22
+
23
+ # manually specify file fields for non-model serializers
24
+ file_fields: list[str] | None = None
25
+
26
+ def to_representation(self, instance: Any) -> dict[str, Any]:
27
+ """Extend serializer representation to enhance file field URLs."""
28
+ representation = cast(dict[str, Any], super().to_representation(instance)) # type: ignore[misc]
29
+ request = self.context.get("request") # type: ignore
30
+ return self.enhance_file_fields(instance, representation, request)
31
+
32
+ def enhance_file_fields(
33
+ self,
34
+ instance: Any,
35
+ representation: dict[str, Any],
36
+ request: Any,
37
+ ) -> dict[str, Any]:
38
+ if request is None:
39
+ logger.warning("Request not found in serializer context.")
40
+ if settings.DEBUG:
41
+ raise MissingRequestContext("Request not found in serializer context.")
42
+ return representation
43
+
44
+ # Collect only file-related model fields if available
45
+ model_fields = (
46
+ {
47
+ field.name: field
48
+ for field in instance._meta.get_fields()
49
+ if isinstance(field, (models.FileField, models.ImageField))
50
+ }
51
+ if hasattr(instance, "_meta")
52
+ else {}
53
+ )
54
+
55
+ manual_fields = getattr(self, "file_fields", []) or []
56
+
57
+ for field_name, field_value in representation.items():
58
+ model_field = model_fields.get(field_name)
59
+
60
+ is_file_field = model_field or (field_name in manual_fields)
61
+ if not is_file_field:
62
+ continue
63
+
64
+ try:
65
+ # Get file URL from instance or raw serializer value
66
+ if model_field:
67
+ file_instance = getattr(instance, field_name, None)
68
+ file_url = (
69
+ getattr(file_instance, "url", None) if file_instance else None
70
+ )
71
+ else:
72
+ file_url = field_value
73
+
74
+ if not file_url:
75
+ logger.info("No file found for field: %s", field_name)
76
+ representation[field_name] = None
77
+ continue
78
+
79
+ # Only build absolute URL if it's relative
80
+ parsed_url = urlparse(str(file_url))
81
+ if not parsed_url.netloc: # relative path
82
+ file_url = request.build_absolute_uri(file_url)
83
+
84
+ representation[field_name] = file_url
85
+ logger.debug("Enhanced URL for %s: %s", field_name, file_url)
86
+
87
+ except Exception as error:
88
+ logger.error(
89
+ "Unexpected error processing file field %s: %s",
90
+ field_name,
91
+ error,
92
+ )
93
+
94
+ return representation
File without changes
File without changes