djresttoolkit 0.8.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.8.0 → djresttoolkit-0.9.0}/PKG-INFO +73 -6
  2. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/README.md +72 -5
  3. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/pyproject.toml +1 -1
  4. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/envconfig/_env_settings.py +3 -3
  5. djresttoolkit-0.9.0/src/djresttoolkit/serializers/__init__.py +0 -0
  6. djresttoolkit-0.9.0/src/djresttoolkit/serializers/_serializer_create_mixin.py +87 -0
  7. djresttoolkit-0.9.0/src/djresttoolkit/serializers/mixins/__init__.py +6 -0
  8. djresttoolkit-0.9.0/src/djresttoolkit/serializers/mixins/_absolute_url_file_mixin.py +94 -0
  9. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/.gitignore +0 -0
  10. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/LICENSE +0 -0
  11. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/demo/staticfiles/admin/img/LICENSE +0 -0
  12. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/__init__.py +0 -0
  13. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/admin.py +0 -0
  14. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/apps.py +0 -0
  15. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/dbseed/__init__.py +0 -0
  16. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/dbseed/models/__init__.py +0 -0
  17. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/dbseed/models/_choice_field.py +0 -0
  18. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/dbseed/models/_gen.py +0 -0
  19. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/dbseed/models/_seed_model.py +0 -0
  20. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/envconfig/__init__.py +0 -0
  21. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/mail/__init__.py +0 -0
  22. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/mail/_email_sender.py +0 -0
  23. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/mail/_models.py +0 -0
  24. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/mail/_types.py +0 -0
  25. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/management/__init__.py +0 -0
  26. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/management/commands/__init__.py +0 -0
  27. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/management/commands/dbflush.py +0 -0
  28. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/management/commands/dbseed.py +0 -0
  29. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/middlewares/__init__.py +0 -0
  30. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/middlewares/_response_time_middleware.py +0 -0
  31. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/migrations/__init__.py +0 -0
  32. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/models/__init__.py +0 -0
  33. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/py.typed +0 -0
  34. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/renderers/__init__.py +0 -0
  35. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/renderers/_throttle_info_json_renderer.py +0 -0
  36. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/throttling/__init__.py +0 -0
  37. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/throttling/_throttle_inspector.py +0 -0
  38. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/views/__init__.py +0 -0
  39. {djresttoolkit-0.8.0 → djresttoolkit-0.9.0}/src/djresttoolkit/views/_exceptions/__init__.py +0 -0
  40. {djresttoolkit-0.8.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.8.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
@@ -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
 
@@ -460,6 +467,66 @@ 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
+
463
530
  ## 🛠️ Planned Features
464
531
 
465
532
  - Add more utils
@@ -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
 
@@ -402,6 +409,66 @@ 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
+
405
472
  ## 🛠️ Planned Features
406
473
 
407
474
  - Add more utils
@@ -4,7 +4,7 @@
4
4
 
5
5
  [project]
6
6
  name = "djresttoolkit"
7
- version = "0.8.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" }
@@ -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
@@ -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