djresttoolkit 0.17.5__py3-none-any.whl → 1.1.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.
@@ -1,3 +1,4 @@
1
- from ._env_settings import EnvBaseSettings
1
+ from ._base_env_config import BaseEnvConfig
2
+ from dotenv import load_dotenv
2
3
 
3
- __all__ = ["EnvBaseSettings"]
4
+ __all__ = ["load_dotenv", "BaseEnvConfig"]
@@ -0,0 +1,84 @@
1
+ import threading
2
+ import json
3
+ from os import getenv
4
+ from typing import Any, Self, get_type_hints
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class BaseEnvConfig:
11
+ """Production-ready environment loader."""
12
+
13
+ _instance_lock = threading.Lock()
14
+ _instance = None
15
+
16
+ def __new__(cls, *args: Any, **kwargs: Any) -> Self:
17
+ if cls._instance is None:
18
+ with cls._instance_lock:
19
+ if cls._instance is None:
20
+ cls._instance = super().__new__(cls)
21
+ return cls._instance
22
+
23
+ def __init__(self) -> None:
24
+ if getattr(self, "_initialized", False):
25
+ return
26
+
27
+ self._env_cache: dict[str, Any] = {}
28
+ self._sync_env()
29
+ self._initialized = True
30
+
31
+ def _sync_env(self) -> None:
32
+ hints = get_type_hints(self.__class__)
33
+ for field, _ in hints.items():
34
+ raw = getenv(field)
35
+
36
+ if raw is None:
37
+ if hasattr(self, field):
38
+ value = getattr(self, field)
39
+ logger.info(f"{field} not set, using default: {value}")
40
+ else:
41
+ raise EnvironmentError(
42
+ f"Missing required environment variable: {field}"
43
+ )
44
+ else:
45
+ if field in self._env_cache:
46
+ value = self._env_cache[field]
47
+ else:
48
+ value = self._parse_env_value(raw)
49
+ self._env_cache[field] = value
50
+
51
+ setattr(self, field, value)
52
+
53
+ def _parse_env_value(self, raw: str) -> Any:
54
+ """Parse string from environment."""
55
+ lowered = raw.lower()
56
+
57
+ # Boolean parsing
58
+ if lowered == "true":
59
+ return True
60
+ if lowered == "false":
61
+ return False
62
+
63
+ # JSON parsing
64
+ try:
65
+ return json.loads(raw)
66
+ except json.JSONDecodeError:
67
+ pass
68
+
69
+ # Numeric parsing
70
+ if raw.isdigit():
71
+ return int(raw)
72
+ try:
73
+ return float(raw)
74
+ except ValueError:
75
+ pass
76
+
77
+ # Fallback: plain string
78
+ return raw
79
+
80
+ def reload(self) -> None:
81
+ """Reload environment variables at runtime."""
82
+ self._env_cache.clear()
83
+ self._sync_env()
84
+ logger.info("Environment variables reloaded.")
@@ -1,6 +1,3 @@
1
- from ._retrieve_object_mixin import RetrieveObjectMixin, QuerysetNotDefinedError
1
+ from ._retrieve_object_mixin import RetrieveObjectMixin
2
2
 
3
- __all__ = [
4
- "RetrieveObjectMixin",
5
- "QuerysetNotDefinedError",
6
- ]
3
+ __all__ = ["RetrieveObjectMixin"]
@@ -1,19 +1,20 @@
1
1
  from typing import Any
2
- from django.db.models import Model, QuerySet
3
-
4
-
5
- class QuerysetNotDefinedError(Exception):
6
- """Exception raised when the `queryset` attribute is not set in the class."""
7
2
 
8
- pass
3
+ from django.core.exceptions import ImproperlyConfigured
4
+ from django.db.models import Model, QuerySet
5
+ from django.http import Http404
9
6
 
10
7
 
11
8
  class RetrieveObjectMixin[T: Model]:
12
9
  """
13
- Mixin to provide a method for retrieving a single model object by filters.
10
+ Retrieve a single model object by filters.
14
11
 
15
12
  Requires the `queryset` attribute to be set in the class that inherits this mixin.
16
13
 
14
+ Raises `Http404` when the object is missing.
15
+
16
+ This works in both Django views and DRF views.
17
+
17
18
  Example:
18
19
  ```
19
20
  class MyView(RetrieveModelMixin[Book], APIView):
@@ -27,15 +28,32 @@ class RetrieveObjectMixin[T: Model]:
27
28
 
28
29
  queryset: QuerySet[T] | None = None
29
30
 
30
- def get_object(self, **filters: Any) -> T | None:
31
+ def get_object(self, **filters: Any) -> T:
31
32
  """Retrieve a model object based on provided filters."""
32
33
 
33
34
  if self.queryset is None:
34
- raise QuerysetNotDefinedError(
35
+ raise ImproperlyConfigured(
35
36
  "Queryset attribute is not set in the class.",
36
37
  )
37
38
 
38
39
  try:
39
40
  return self.queryset.get(**filters)
40
41
  except self.queryset.model.DoesNotExist:
41
- return None
42
+ raise Http404(self.not_found_detail())
43
+
44
+ def not_found_detail(self) -> dict[str, str] | str:
45
+ """
46
+ Hook for customizing the 404 message.
47
+ Can be overridden per view.
48
+ """
49
+
50
+ if self.queryset is None:
51
+ raise ImproperlyConfigured(
52
+ "Queryset attribute is not set in the class.",
53
+ )
54
+
55
+ verbose_name = self.queryset.model._meta.verbose_name
56
+ model_name = (
57
+ verbose_name.title() if verbose_name else self.queryset.model.__name__
58
+ )
59
+ return f"The requested {model_name} was not found."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djresttoolkit
3
- Version: 0.17.5
3
+ Version: 1.1.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,9 +47,7 @@ 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
51
50
  Requires-Dist: pydantic>=2.11.7
52
- Requires-Dist: pyyaml>=6.0.2
53
51
  Provides-Extra: dev
54
52
  Requires-Dist: mypy; extra == 'dev'
55
53
  Requires-Dist: pytest; extra == 'dev'
@@ -72,8 +70,8 @@ djresttoolkit is a collection of utilities and helpers for Django and Django RES
72
70
  - **DB Flush Command (`dbflush`)**
73
71
  Management command to flush all models or a specific model, resetting auto-increment IDs safely with transaction support.
74
72
 
75
- - **EnvBaseSettings**
76
- Typed settings loader using **YAML + .env**, supports nested keys and overrides. Great for structured configuration management.
73
+ - **BaseEnvConfig**
74
+ Singleton environment loader that reads and parses environment variables.
77
75
 
78
76
  - **EmailSender**
79
77
  Custom class to send templated emails (`text` and `html`) with context. Supports error handling and logging.
@@ -186,10 +184,6 @@ python manage.py dbseed --count 10
186
184
  python manage.py dbseed --model User --seed 42
187
185
  ```
188
186
 
189
- Here’s a **concise API reference** for your database flush management command for `djresttoolkit`:
190
-
191
- ---
192
-
193
187
  ### 2. DB Flush Command — API Reference
194
188
 
195
189
  ```python
@@ -246,77 +240,54 @@ or
246
240
  Flushed 120 records from all models and reset IDs.
247
241
  ```
248
242
 
249
- ### 3. EnvBaseSettings — API Reference
243
+ ### 3. BaseEnvConfig — API Reference
250
244
 
251
245
  ```python
252
- from djresttoolkit.envconfig import EnvBaseSettings
246
+ from djresttoolkit.envconfig import BaseEnvConfig
253
247
  ```
254
248
 
255
- #### `EnvBaseSettings`
256
-
257
- A **base settings class** for managing application configuration using:
258
-
259
- - YAML files (default `.environ.yaml`)
260
- - Environment variables (default `.env`)
261
-
262
- Supports **nested configuration** using double underscores (`__`) in environment variable names.
263
-
264
- #### Class Attributes
265
-
266
- - Attributes
267
- - `env_file`
268
- - Type: `str`
269
- - Default: `.env`
270
- - Description: Environment variable file path.
271
- - `yaml_file`
272
- - Type: `str`
273
- - Default: `.environ.yaml`
274
- - Description: YAML configuration file path.
275
- - `model_config`
276
- - Type: `SettingsConfigDict`
277
- - Description: Pydantic settings configuration (file encoding, nested delimiter).
278
-
279
- #### Methods
249
+ #### `BaseEnvConfig`
280
250
 
281
- #### `load(cls, *, env_file: str | None = None, ymal_file: str | None = None, warning: bool = True) -> EnvBaseSettings`
251
+ Singleton environment loader that reads and parses environment variables with support for booleans, numbers, and JSON.
282
252
 
283
- Loads configuration from **YAML first**, then overrides with **environment variables**.
253
+ > ⚠️ **Note:** Note: If you are using a .env file, load it first with python-dotenv:
254
+ >
255
+ > ```python
256
+ > from dotenv import load_dotenv
257
+ > load_dotenv()
258
+ > ```
284
259
 
285
- #### Parameters
286
-
287
- - `env_file` — Optional custom `.env` file path.
288
- - `ymal_file` Optional custom YAML file path.
289
- - `warning` — Emit a warning if YAML file is missing (default `True`).
290
-
291
- #### Returns
292
-
293
- - Instance of `EnvBaseSettings` (or subclass) with loaded configuration.
294
-
295
- #### Raises
296
-
297
- - `UserWarning` if YAML file not found and `warning=True`.
260
+ or
261
+ >
262
+ > ```python
263
+ > from djresttoolkit.envconfig import load_dotenv
264
+ > load_dotenv()
265
+ > ```
298
266
 
299
- ### Usage Example
267
+ #### Usage Example
300
268
 
301
269
  ```python
302
- from djresttoolkit.envconfig import EnvBaseSettings
270
+ from djresttoolkit.envconfig import load_dotenv, BaseEnvConfig
303
271
 
304
- class EnvSettings(EnvBaseSettings["EnvSettings"]):
305
- debug: bool = False
306
- database_url: str
272
+ load_dotenv()
307
273
 
308
- # Load settings
309
- settings = EnvSettings.load(warning=False)
274
+ class EnvConfig(BaseEnvConfig):
275
+ DEBUG: bool = False
276
+ DATABASE_URL: str
310
277
 
311
- print(settings.debug)
312
- print(settings.database_url)
278
+ config = EnvConfig()
279
+ print(config.DEBUG)
280
+ print(config.DATABASE_URL)
281
+
282
+ config.reload()
313
283
  ```
314
284
 
315
285
  #### Features
316
286
 
317
- - Prioritizes `.env` variables over YAML.
318
- - Supports nested keys: `DATABASE__HOST`:- `settings.database.host`.
319
- - Designed to be subclassed for project-specific settings.
287
+ - Thread-safe singleton.
288
+ - Automatic type parsing for environment variables.
289
+ - Supports default values and runtime reloading.
290
+ - Designed for subclassing with project-specific settings.
320
291
 
321
292
  ### 4. EmailSender — API Reference
322
293
 
@@ -830,6 +801,7 @@ Retrieve a single model object using the provided filter criteria.
830
801
 
831
802
  ```python
832
803
  from rest_framework.views import APIView
804
+ from rest_framework.response import Respone
833
805
  from django.http import JsonResponse
834
806
  from myapp.models import Book
835
807
  from djresttoolkit.mixins import RetrieveObjectMixin
@@ -837,17 +809,18 @@ from djresttoolkit.mixins import RetrieveObjectMixin
837
809
  class BookDetailView(RetrieveObjectMixin[Book], APIView):
838
810
  queryset = Book.objects.all()
839
811
 
840
- def get(self, request, *args, **kwargs):
812
+ def not_found_detail(self) -> dict[str, str] | str:
813
+ return "The requested Book was not found."
814
+
815
+ def get(self, request, *args, **kwargs) -> Respone:
841
816
  book = self.get_object(id=kwargs["id"])
842
- if book:
843
- return JsonResponse({"title": book.title, "author": book.author})
844
- return JsonResponse({"detail": "Not found"}, status=404)
817
+ return Respone({"title": book.title, "author": book.author})
845
818
  ```
846
819
 
847
820
  #### Features of Retrieve Object Mixin
848
821
 
849
822
  - Simplifies object retrieval in class-based views or DRF views.
850
- - Returns `None` instead of raising `DoesNotExist`, making error handling easier.
823
+ - Raise `http404` if requested resource does not extst.
851
824
  - Works with any Django model and queryset.
852
825
 
853
826
  ### 13. build_absolute_uri — API Reference
@@ -14,8 +14,8 @@ djresttoolkit/dbseed/models/__init__.py,sha256=uuynQIcfVqEaZN9hF_caI24zm8az23JdX
14
14
  djresttoolkit/dbseed/models/_choice_field.py,sha256=T7LAzbyXqlYp2mtCAKL8E1Da_MEh9RzgLZrFJ7fa4gM,446
15
15
  djresttoolkit/dbseed/models/_gen.py,sha256=qBPQaLvh1rcEam0YmE4JBJqpa-Vv5IFlIIagkEMHDVw,206
16
16
  djresttoolkit/dbseed/models/_seed_model.py,sha256=0cmbi0VNKjmJbwhjeCFsvb3iKYjok6TJOk6Y2MF_3N4,2443
17
- djresttoolkit/envconfig/__init__.py,sha256=PcLaPaVfQmz3-4m6SwoOQF2W4U7F0agBGJ4Qjqbcyfw,74
18
- djresttoolkit/envconfig/_env_settings.py,sha256=X-pgHqNtUNQLBHiGpbYB4ViCuY14n6aff5HZqYL4Tlc,3236
17
+ djresttoolkit/envconfig/__init__.py,sha256=ABYwK1rsH15txGsbh5myqerF_ls6RafUlGhLyvaF2t8,119
18
+ djresttoolkit/envconfig/_base_env_config.py,sha256=7buy7khKDR1PLag9w4rtjRhk9EUtIQvqcyqn_LehuV4,2340
19
19
  djresttoolkit/mail/__init__.py,sha256=tB9SdMlhfWQ640q4aobZ0H1c7fTWalpDL2I-onkr2VI,268
20
20
  djresttoolkit/mail/_email_sender.py,sha256=nXnH1JSiiu51IFQSKw8W6pzaGjDPg2GUKEq0XQ2gSJM,3207
21
21
  djresttoolkit/mail/_models.py,sha256=of5KsLGvsN2OWgDYgdtLEijulg817TXgsLKuUdsnDQc,1447
@@ -48,10 +48,10 @@ djresttoolkit/views/_apiviews/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
48
48
  djresttoolkit/views/_apiviews/_choice_fields_apiview.py,sha256=zABPgqxMVaWd814B_sC64bWL61fDJkyYQZmJXQCa6Xc,1395
49
49
  djresttoolkit/views/_exceptions/__init__.py,sha256=DrCUxuPNyBR4WhzNutn5HDxLa--q51ykIxSG7_bFsOI,83
50
50
  djresttoolkit/views/_exceptions/_exception_handler.py,sha256=_o7If47bzWLl57LeSXSWsIDsJGo2RIpwYAwNQ-hsHVY,2839
51
- djresttoolkit/views/mixins/__init__.py,sha256=K-1tk5d8tCVViMynw5DdffJ3Oo5uHpEx32E3_4X2UxM,154
52
- djresttoolkit/views/mixins/_retrieve_object_mixin.py,sha256=Q9znYPb07YXXUhsL7VIrk3BC-zDwjOhwLJKe2GPJ-k0,1155
53
- djresttoolkit-0.17.5.dist-info/METADATA,sha256=3sSysAflkxpff1N8jlZ7_5lv4CJSWZ9be8_6ZQFUp5w,32929
54
- djresttoolkit-0.17.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
55
- djresttoolkit-0.17.5.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
56
- djresttoolkit-0.17.5.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
57
- djresttoolkit-0.17.5.dist-info/RECORD,,
51
+ djresttoolkit/views/mixins/__init__.py,sha256=mHD49OUxuJ9v81tGfM0hLnUJuJlYi7E-5cTVdplh-vs,91
52
+ djresttoolkit/views/mixins/_retrieve_object_mixin.py,sha256=v7CQDUkRWjtevFZnAYRBdDl7wcfYWF3evWoKWHAcckA,1749
53
+ djresttoolkit-1.1.0.dist-info/METADATA,sha256=0Ln7HqEDc0-fHHiJZhZv0uipTH5TC9IsoWH9dWCswvI,31878
54
+ djresttoolkit-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
55
+ djresttoolkit-1.1.0.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
56
+ djresttoolkit-1.1.0.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
57
+ djresttoolkit-1.1.0.dist-info/RECORD,,
@@ -1,84 +0,0 @@
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)