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.
- djresttoolkit/envconfig/__init__.py +3 -2
- djresttoolkit/envconfig/_base_env_config.py +84 -0
- djresttoolkit/views/mixins/__init__.py +2 -5
- djresttoolkit/views/mixins/_retrieve_object_mixin.py +28 -10
- {djresttoolkit-0.17.5.dist-info → djresttoolkit-1.1.0.dist-info}/METADATA +41 -68
- {djresttoolkit-0.17.5.dist-info → djresttoolkit-1.1.0.dist-info}/RECORD +9 -9
- djresttoolkit/envconfig/_env_settings.py +0 -84
- {djresttoolkit-0.17.5.dist-info → djresttoolkit-1.1.0.dist-info}/WHEEL +0 -0
- {djresttoolkit-0.17.5.dist-info → djresttoolkit-1.1.0.dist-info}/entry_points.txt +0 -0
- {djresttoolkit-0.17.5.dist-info → djresttoolkit-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,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
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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:
|
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
|
-
- **
|
76
|
-
|
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.
|
243
|
+
### 3. BaseEnvConfig — API Reference
|
250
244
|
|
251
245
|
```python
|
252
|
-
from djresttoolkit.envconfig import
|
246
|
+
from djresttoolkit.envconfig import BaseEnvConfig
|
253
247
|
```
|
254
248
|
|
255
|
-
#### `
|
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
|
-
|
251
|
+
Singleton environment loader that reads and parses environment variables with support for booleans, numbers, and JSON.
|
282
252
|
|
283
|
-
|
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
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
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
|
-
|
267
|
+
#### Usage Example
|
300
268
|
|
301
269
|
```python
|
302
|
-
from djresttoolkit.envconfig import
|
270
|
+
from djresttoolkit.envconfig import load_dotenv, BaseEnvConfig
|
303
271
|
|
304
|
-
|
305
|
-
debug: bool = False
|
306
|
-
database_url: str
|
272
|
+
load_dotenv()
|
307
273
|
|
308
|
-
|
309
|
-
|
274
|
+
class EnvConfig(BaseEnvConfig):
|
275
|
+
DEBUG: bool = False
|
276
|
+
DATABASE_URL: str
|
310
277
|
|
311
|
-
|
312
|
-
print(
|
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
|
-
-
|
318
|
-
-
|
319
|
-
-
|
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
|
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
|
-
|
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
|
-
-
|
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=
|
18
|
-
djresttoolkit/envconfig/
|
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=
|
52
|
-
djresttoolkit/views/mixins/_retrieve_object_mixin.py,sha256=
|
53
|
-
djresttoolkit-
|
54
|
-
djresttoolkit-
|
55
|
-
djresttoolkit-
|
56
|
-
djresttoolkit-
|
57
|
-
djresttoolkit-
|
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)
|
File without changes
|
File without changes
|
File without changes
|