djresttoolkit 0.8.0__py3-none-any.whl → 0.10.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 CHANGED
@@ -30,7 +30,7 @@ djresttoolkit is a collection of utilities and helpers for Django and Django RES
30
30
 
31
31
  ## 📚 All API Reference
32
32
 
33
- ### 1. DB Seed Utilities
33
+ ### 1. DB Seed Utilities — API Reference
34
34
 
35
35
  #### `Generator`
36
36
 
@@ -89,7 +89,7 @@ Here’s a **concise API reference** for your database flush management command
89
89
 
90
90
  ---
91
91
 
92
- ### 2. DB Flush Command
92
+ ### 2. DB Flush Command — API Reference
93
93
 
94
94
  ```python
95
95
  from djresttoolkit.management.commands import flush
@@ -145,7 +145,7 @@ or
145
145
  Flushed 120 records from all models and reset IDs.
146
146
  ```
147
147
 
148
- ### 3. EnvBaseSettings
148
+ ### 3. EnvBaseSettings — API Reference
149
149
 
150
150
  ```python
151
151
  from djresttoolkit.envconfig import EnvBaseSettings
@@ -162,11 +162,18 @@ Supports **nested configuration** using double underscores (`__`) in environment
162
162
 
163
163
  #### Class Attributes
164
164
 
165
- | Attribute | Type | Default | Description |
166
- | -------------- | -------------------- | --------------- | ------------------------------------------------------------------ |
167
- | `env_file` | `str` | `.env` | Environment variable file path. |
168
- | `yaml_file` | `str` | `.environ.yaml` | YAML configuration file path. |
169
- | `model_config` | `SettingsConfigDict` | — | Pydantic settings configuration (file encoding, nested delimiter). |
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).
170
177
 
171
178
  #### Methods
172
179
 
@@ -193,7 +200,7 @@ Loads configuration from **YAML first**, then overrides with **environment varia
193
200
  ```python
194
201
  from djresttoolkit.envconfig import EnvBaseSettings
195
202
 
196
- class EnvSettings(EnvBaseSettings):
203
+ class EnvSettings(EnvBaseSettings["EnvSettings"]):
197
204
  debug: bool = False
198
205
  database_url: str
199
206
 
@@ -210,7 +217,7 @@ print(settings.database_url)
210
217
  - Supports nested keys: `DATABASE__HOST` → `settings.database.host`.
211
218
  - Designed to be subclassed for project-specific settings.
212
219
 
213
- ### 4. EmailSender
220
+ ### 4. EmailSender — API Reference
214
221
 
215
222
  ```python
216
223
  from djresttoolkit.mail import EmailSender, EmailContent, EmailTemplate
@@ -259,7 +266,7 @@ EmailSender(content).send(to=["user@example.com"])
259
266
 
260
267
  - `text`, `html` — template file paths
261
268
 
262
- ### 5. Custom DRF Exception Handler
269
+ ### 5. Custom DRF Exception Handler — API Reference
263
270
 
264
271
  ```python
265
272
  from djresttoolkit.views import exception_handler
@@ -299,7 +306,7 @@ REST_FRAMEWORK = {
299
306
  - Tracks requests in cache and calculates `retry_after`.
300
307
  - Cleans expired timestamps automatically.
301
308
 
302
- ### 6. Response Time Middleware
309
+ ### 6. Response Time Middleware — API Reference
303
310
 
304
311
  ```python
305
312
  from djresttoolkit.middlewares import ResponseTimeMiddleware
@@ -346,7 +353,7 @@ X-Response-Time: 0.01234 seconds
346
353
  INFO: Request processed in 0.01234 seconds
347
354
  ```
348
355
 
349
- ### 7. Throttle Utilities
356
+ ### 7. Throttle Utilities — API Reference
350
357
 
351
358
  #### `ThrottleInfoJSONRenderer`
352
359
 
@@ -402,6 +409,144 @@ ThrottleInspector(
402
409
  - `attach_headers(response: Response, throttle_info: dict | None)`
403
410
  Attaches throttle data to HTTP headers.
404
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
+
472
+ ### 9. BulkCreateMixin — API Reference
473
+
474
+ ```python
475
+ from djresttoolkit.serializers.mixins import BulkCreateMixin
476
+ ```
477
+
478
+ #### `BulkCreateMixin`
479
+
480
+ A **DRF serializer mixin** that adds support for:
481
+
482
+ - **Single instance creation** with extra context fields
483
+ - **Bulk creation** from a list of validated data dictionaries
484
+ - **Updating serializer field error messages** with model-specific messages
485
+
486
+ #### Bulk Create Mixin Notes
487
+
488
+ - `bulk_create()` does **not trigger model signals** or call `.save()` on instances.
489
+ - `Meta.model` **must** be defined in the serializer.
490
+
491
+ #### Bulk Create Mixin Methods
492
+
493
+ #### `create(self, validated_data: dict[str, Any] | list[dict[str, Any]]) -> Model | list[Model]`
494
+
495
+ - Creates single or multiple model instances.
496
+ - **Parameters:**
497
+ - `validated_data`: dict for single instance or list of dicts for bulk creation.
498
+
499
+ - **Returns:**
500
+ - Single model instance or list of instances.
501
+
502
+ - **Raises:**
503
+ - `AttributeError` if `Meta.model` is not defined.
504
+ - `NotImplementedError` if used with a serializer that does not implement `create()`.
505
+
506
+ #### `get_fields(self) -> dict[str, SerializerField]`
507
+
508
+ - Extends DRF serializer `get_fields()` to update **error messages** using model field definitions.
509
+ - **Returns:**
510
+ - Dictionary of serializer fields.
511
+
512
+ - **Warning:**
513
+ - Logs a warning if a serializer field is not present on the model.
514
+
515
+ ### Bulk Create Mixin Example
516
+
517
+ ```python
518
+ from rest_framework import serializers
519
+ from djresttoolkit.serializers.mixins import BulkCreateMixin
520
+ from myapp.models import Product
521
+
522
+ class ProductSerializer(BulkCreateMixin, serializers.ModelSerializer):
523
+ class Meta:
524
+ model = Product
525
+ fields = ["id", "name", "price"]
526
+
527
+ # Single creation
528
+ serializer = ProductSerializer(data={"name": "Item1", "price": 10})
529
+ serializer.is_valid(raise_exception=True)
530
+ product = serializer.save()
531
+
532
+ # Bulk creation
533
+ serializer = ProductSerializer(
534
+ data=[
535
+ {"name": "Item2", "price": 20},
536
+ {"name": "Item3", "price": 30},
537
+ ],
538
+ many=True
539
+ )
540
+ serializer.is_valid(raise_exception=True)
541
+ products = serializer.save()
542
+ ```
543
+
544
+ #### Bulk Create Mixin Features
545
+
546
+ - Works seamlessly with DRF `ModelSerializer`.
547
+ - Automatically updates field error messages based on Django model definitions.
548
+ - Bulk creation is optimized using `model.objects.bulk_create()` for efficiency.
549
+
405
550
  ## 🛠️ Planned Features
406
551
 
407
552
  - Add more utils
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djresttoolkit
3
- Version: 0.8.0
3
+ Version: 0.10.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
@@ -88,7 +88,7 @@ djresttoolkit is a collection of utilities and helpers for Django and Django RES
88
88
 
89
89
  ## 📚 All API Reference
90
90
 
91
- ### 1. DB Seed Utilities
91
+ ### 1. DB Seed Utilities — API Reference
92
92
 
93
93
  #### `Generator`
94
94
 
@@ -147,7 +147,7 @@ Here’s a **concise API reference** for your database flush management command
147
147
 
148
148
  ---
149
149
 
150
- ### 2. DB Flush Command
150
+ ### 2. DB Flush Command — API Reference
151
151
 
152
152
  ```python
153
153
  from djresttoolkit.management.commands import flush
@@ -203,7 +203,7 @@ or
203
203
  Flushed 120 records from all models and reset IDs.
204
204
  ```
205
205
 
206
- ### 3. EnvBaseSettings
206
+ ### 3. EnvBaseSettings — API Reference
207
207
 
208
208
  ```python
209
209
  from djresttoolkit.envconfig import EnvBaseSettings
@@ -220,11 +220,18 @@ Supports **nested configuration** using double underscores (`__`) in environment
220
220
 
221
221
  #### Class Attributes
222
222
 
223
- | Attribute | Type | Default | Description |
224
- | -------------- | -------------------- | --------------- | ------------------------------------------------------------------ |
225
- | `env_file` | `str` | `.env` | Environment variable file path. |
226
- | `yaml_file` | `str` | `.environ.yaml` | YAML configuration file path. |
227
- | `model_config` | `SettingsConfigDict` | — | Pydantic settings configuration (file encoding, nested delimiter). |
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).
228
235
 
229
236
  #### Methods
230
237
 
@@ -251,7 +258,7 @@ Loads configuration from **YAML first**, then overrides with **environment varia
251
258
  ```python
252
259
  from djresttoolkit.envconfig import EnvBaseSettings
253
260
 
254
- class EnvSettings(EnvBaseSettings):
261
+ class EnvSettings(EnvBaseSettings["EnvSettings"]):
255
262
  debug: bool = False
256
263
  database_url: str
257
264
 
@@ -268,7 +275,7 @@ print(settings.database_url)
268
275
  - Supports nested keys: `DATABASE__HOST` → `settings.database.host`.
269
276
  - Designed to be subclassed for project-specific settings.
270
277
 
271
- ### 4. EmailSender
278
+ ### 4. EmailSender — API Reference
272
279
 
273
280
  ```python
274
281
  from djresttoolkit.mail import EmailSender, EmailContent, EmailTemplate
@@ -317,7 +324,7 @@ EmailSender(content).send(to=["user@example.com"])
317
324
 
318
325
  - `text`, `html` — template file paths
319
326
 
320
- ### 5. Custom DRF Exception Handler
327
+ ### 5. Custom DRF Exception Handler — API Reference
321
328
 
322
329
  ```python
323
330
  from djresttoolkit.views import exception_handler
@@ -357,7 +364,7 @@ REST_FRAMEWORK = {
357
364
  - Tracks requests in cache and calculates `retry_after`.
358
365
  - Cleans expired timestamps automatically.
359
366
 
360
- ### 6. Response Time Middleware
367
+ ### 6. Response Time Middleware — API Reference
361
368
 
362
369
  ```python
363
370
  from djresttoolkit.middlewares import ResponseTimeMiddleware
@@ -404,7 +411,7 @@ X-Response-Time: 0.01234 seconds
404
411
  INFO: Request processed in 0.01234 seconds
405
412
  ```
406
413
 
407
- ### 7. Throttle Utilities
414
+ ### 7. Throttle Utilities — API Reference
408
415
 
409
416
  #### `ThrottleInfoJSONRenderer`
410
417
 
@@ -460,6 +467,144 @@ ThrottleInspector(
460
467
  - `attach_headers(response: Response, throttle_info: dict | None)`
461
468
  Attaches throttle data to HTTP headers.
462
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
+
530
+ ### 9. BulkCreateMixin — API Reference
531
+
532
+ ```python
533
+ from djresttoolkit.serializers.mixins import BulkCreateMixin
534
+ ```
535
+
536
+ #### `BulkCreateMixin`
537
+
538
+ A **DRF serializer mixin** that adds support for:
539
+
540
+ - **Single instance creation** with extra context fields
541
+ - **Bulk creation** from a list of validated data dictionaries
542
+ - **Updating serializer field error messages** with model-specific messages
543
+
544
+ #### Bulk Create Mixin Notes
545
+
546
+ - `bulk_create()` does **not trigger model signals** or call `.save()` on instances.
547
+ - `Meta.model` **must** be defined in the serializer.
548
+
549
+ #### Bulk Create Mixin Methods
550
+
551
+ #### `create(self, validated_data: dict[str, Any] | list[dict[str, Any]]) -> Model | list[Model]`
552
+
553
+ - Creates single or multiple model instances.
554
+ - **Parameters:**
555
+ - `validated_data`: dict for single instance or list of dicts for bulk creation.
556
+
557
+ - **Returns:**
558
+ - Single model instance or list of instances.
559
+
560
+ - **Raises:**
561
+ - `AttributeError` if `Meta.model` is not defined.
562
+ - `NotImplementedError` if used with a serializer that does not implement `create()`.
563
+
564
+ #### `get_fields(self) -> dict[str, SerializerField]`
565
+
566
+ - Extends DRF serializer `get_fields()` to update **error messages** using model field definitions.
567
+ - **Returns:**
568
+ - Dictionary of serializer fields.
569
+
570
+ - **Warning:**
571
+ - Logs a warning if a serializer field is not present on the model.
572
+
573
+ ### Bulk Create Mixin Example
574
+
575
+ ```python
576
+ from rest_framework import serializers
577
+ from djresttoolkit.serializers.mixins import BulkCreateMixin
578
+ from myapp.models import Product
579
+
580
+ class ProductSerializer(BulkCreateMixin, serializers.ModelSerializer):
581
+ class Meta:
582
+ model = Product
583
+ fields = ["id", "name", "price"]
584
+
585
+ # Single creation
586
+ serializer = ProductSerializer(data={"name": "Item1", "price": 10})
587
+ serializer.is_valid(raise_exception=True)
588
+ product = serializer.save()
589
+
590
+ # Bulk creation
591
+ serializer = ProductSerializer(
592
+ data=[
593
+ {"name": "Item2", "price": 20},
594
+ {"name": "Item3", "price": 30},
595
+ ],
596
+ many=True
597
+ )
598
+ serializer.is_valid(raise_exception=True)
599
+ products = serializer.save()
600
+ ```
601
+
602
+ #### Bulk Create Mixin Features
603
+
604
+ - Works seamlessly with DRF `ModelSerializer`.
605
+ - Automatically updates field error messages based on Django model definitions.
606
+ - Bulk creation is optimized using `model.objects.bulk_create()` for efficiency.
607
+
463
608
  ## 🛠️ Planned Features
464
609
 
465
610
  - Add more utils
@@ -1,5 +1,5 @@
1
1
  LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
2
- README.md,sha256=9S8AiRr4U5uA-YrgtQbUZiYpbg4rT_UejlDGhJ2WYzM,10558
2
+ README.md,sha256=OzvflId2K79hvwsZr4VIf393Fvm1PhTExUHAAnBvpzo,14651
3
3
  demo/staticfiles/admin/img/LICENSE,sha256=0RT6_zSIwWwxmzI13EH5AjnT1j2YU3MwM9j3U19cAAQ,1081
4
4
  src/djresttoolkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  src/djresttoolkit/admin.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -11,7 +11,7 @@ src/djresttoolkit/dbseed/models/_choice_field.py,sha256=T7LAzbyXqlYp2mtCAKL8E1Da
11
11
  src/djresttoolkit/dbseed/models/_gen.py,sha256=qBPQaLvh1rcEam0YmE4JBJqpa-Vv5IFlIIagkEMHDVw,206
12
12
  src/djresttoolkit/dbseed/models/_seed_model.py,sha256=0cmbi0VNKjmJbwhjeCFsvb3iKYjok6TJOk6Y2MF_3N4,2443
13
13
  src/djresttoolkit/envconfig/__init__.py,sha256=PcLaPaVfQmz3-4m6SwoOQF2W4U7F0agBGJ4Qjqbcyfw,74
14
- src/djresttoolkit/envconfig/_env_settings.py,sha256=cKGDLo7WU0wzQGkWSwgrUxGCb0sT111JUt4HGlDVSxo,3221
14
+ src/djresttoolkit/envconfig/_env_settings.py,sha256=X-pgHqNtUNQLBHiGpbYB4ViCuY14n6aff5HZqYL4Tlc,3236
15
15
  src/djresttoolkit/mail/__init__.py,sha256=tB9SdMlhfWQ640q4aobZ0H1c7fTWalpDL2I-onkr2VI,268
16
16
  src/djresttoolkit/mail/_email_sender.py,sha256=bPMqgD5HibJcOZgO6xxHOhdK9HEhnGNC6BoMPpo-h7k,3096
17
17
  src/djresttoolkit/mail/_models.py,sha256=of5KsLGvsN2OWgDYgdtLEijulg817TXgsLKuUdsnDQc,1447
@@ -26,13 +26,17 @@ src/djresttoolkit/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
26
26
  src/djresttoolkit/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  src/djresttoolkit/renderers/__init__.py,sha256=kmFMPRiMfD8CuJTN1_-6Z_Hqil3x8GBM0IN1roZESm0,107
28
28
  src/djresttoolkit/renderers/_throttle_info_json_renderer.py,sha256=aP2cN4cB_Imcpy732zsPBQrMQqcKEs5R3dld5Y_4AMU,1089
29
+ src/djresttoolkit/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ src/djresttoolkit/serializers/mixins/__init__.py,sha256=dRT0kXDckOkZo1RQHrT1gXbGFMIv5M8TBHGF2uF-81Q,225
31
+ src/djresttoolkit/serializers/mixins/_absolute_url_file_mixin.py,sha256=5ewael0_RsJZ9b36IfXacxjb-Vx1eQ9Dk6dWuj5D_dc,3261
32
+ src/djresttoolkit/serializers/mixins/_bulk_create_mixin.py,sha256=9ZWm2MNaZOhmhKlWOu6VECtlDbUtaPeceGHmivDYwYQ,3248
29
33
  src/djresttoolkit/throttling/__init__.py,sha256=01sjMymjx8XjqnAw3bEBLc-JtfhCDrp5dGxSNXMvPpU,84
30
34
  src/djresttoolkit/throttling/_throttle_inspector.py,sha256=Kss6ZxKy-EXq9UGaGprGDhpSuJ5992bmEYZSWmUVBHo,6480
31
35
  src/djresttoolkit/views/__init__.py,sha256=XrxBrs6sH4HmUzp41omcmy_y94pSaXAVn01ttQ022-4,76
32
36
  src/djresttoolkit/views/_exceptions/__init__.py,sha256=DrCUxuPNyBR4WhzNutn5HDxLa--q51ykIxSG7_bFsOI,83
33
37
  src/djresttoolkit/views/_exceptions/_exception_handler.py,sha256=_o7If47bzWLl57LeSXSWsIDsJGo2RIpwYAwNQ-hsHVY,2839
34
- djresttoolkit-0.8.0.dist-info/METADATA,sha256=qCwxDQnmpbQygUL7gytkkO826sOs9Zm8Xy_NXmlLvQY,13567
35
- djresttoolkit-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
- djresttoolkit-0.8.0.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
37
- djresttoolkit-0.8.0.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
38
- djresttoolkit-0.8.0.dist-info/RECORD,,
38
+ djresttoolkit-0.10.0.dist-info/METADATA,sha256=A91Vw-rZ7cni31PQzgw568wk5tGRLROJkBe9mLXYa14,17661
39
+ djresttoolkit-0.10.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
40
+ djresttoolkit-0.10.0.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
41
+ djresttoolkit-0.10.0.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
42
+ djresttoolkit-0.10.0.dist-info/RECORD,,
@@ -6,7 +6,7 @@ import yaml
6
6
  from pydantic_settings import BaseSettings, SettingsConfigDict
7
7
 
8
8
 
9
- class EnvBaseSettings(BaseSettings):
9
+ class EnvBaseSettings[T: "EnvBaseSettings"](BaseSettings):
10
10
  """ "
11
11
  EnvBaseSettings is a base settings class for managing application configuration
12
12
  using both YAML files and environment variables.
@@ -60,12 +60,12 @@ class EnvBaseSettings(BaseSettings):
60
60
 
61
61
  @classmethod
62
62
  def load(
63
- cls,
63
+ cls: type[T],
64
64
  *,
65
65
  env_file: str | None = None,
66
66
  ymal_file: str | None = None,
67
67
  warning: bool = True,
68
- ) -> "EnvBaseSettings":
68
+ ) -> T:
69
69
  """Load from YAML first, then override with .env."""
70
70
  if env_file:
71
71
  cls.env_file = env_file
File without changes
@@ -0,0 +1,8 @@
1
+ from ._absolute_url_file_mixin import AbsoluteUrlFileMixin, MissingRequestContext
2
+ from ._bulk_create_mixin import BulkCreateMixin
3
+
4
+ __all__ = [
5
+ "AbsoluteUrlFileMixin",
6
+ "MissingRequestContext",
7
+ "BulkCreateMixin",
8
+ ]
@@ -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
@@ -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 BulkCreateMixin:
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