django-environment-config 0.0.2__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.
- django_environment_config-0.0.2/LICENSE +51 -0
- django_environment_config-0.0.2/PKG-INFO +77 -0
- django_environment_config-0.0.2/README.md +42 -0
- django_environment_config-0.0.2/env_config/__init__.py +5 -0
- django_environment_config-0.0.2/env_config/base.py +85 -0
- django_environment_config-0.0.2/env_config/constants.py +18 -0
- django_environment_config-0.0.2/env_config/errors.py +31 -0
- django_environment_config-0.0.2/env_config/py.typed +0 -0
- django_environment_config-0.0.2/env_config/typing.py +59 -0
- django_environment_config-0.0.2/env_config/values.py +379 -0
- django_environment_config-0.0.2/pyproject.toml +314 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Matti Lamppu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---------------- Original Licence for `django-configurations` ----------------
|
|
24
|
+
|
|
25
|
+
Copyright (c) 2012-2023, Jannis Leidel and other contributors.
|
|
26
|
+
All rights reserved.
|
|
27
|
+
|
|
28
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
29
|
+
are permitted provided that the following conditions are met:
|
|
30
|
+
|
|
31
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
32
|
+
this list of conditions and the following disclaimer.
|
|
33
|
+
|
|
34
|
+
2. Redistributions in binary form must reproduce the above copyright
|
|
35
|
+
notice, this list of conditions and the following disclaimer in the
|
|
36
|
+
documentation and/or other materials provided with the distribution.
|
|
37
|
+
|
|
38
|
+
3. Neither the name of django-configurations nor the names of its
|
|
39
|
+
contributors may be used to endorse or promote products derived from
|
|
40
|
+
this software without specific prior written permission.
|
|
41
|
+
|
|
42
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
43
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
44
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
45
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
46
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
47
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
48
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
49
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
50
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
51
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: django-environment-config
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Configure django settings for multiple environments.
|
|
5
|
+
Home-page: https://mrthearman.github.io/django-environment-config
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: django,configuration,settings,environment,env,config
|
|
8
|
+
Author: Matti Lamppu
|
|
9
|
+
Author-email: lamppu.matti.akseli@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Framework :: Django :: 4.2
|
|
13
|
+
Classifier: Framework :: Django :: 5.0
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Provides-Extra: cache
|
|
25
|
+
Provides-Extra: db
|
|
26
|
+
Requires-Dist: Django (>=4.2)
|
|
27
|
+
Requires-Dist: dj-database-url (>=2.1.0,<3.0.0) ; extra == "db"
|
|
28
|
+
Requires-Dist: django-cache-url (>=3.4.5,<4.0.0) ; extra == "cache"
|
|
29
|
+
Requires-Dist: python-dotenv (>=1.0.1)
|
|
30
|
+
Requires-Dist: typing-extensions (>=4.11.0) ; python_version < "3.12"
|
|
31
|
+
Project-URL: Bug Tracker, https://github.com/MrThearMan/django-environment-config/issues
|
|
32
|
+
Project-URL: Repository, https://github.com/MrThearMan/django-environment-config
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# Django Environment Config
|
|
36
|
+
|
|
37
|
+
[![Coverage Status][coverage-badge]][coverage]
|
|
38
|
+
[![GitHub Workflow Status][status-badge]][status]
|
|
39
|
+
[![PyPI][pypi-badge]][pypi]
|
|
40
|
+
[![GitHub][licence-badge]][licence]
|
|
41
|
+
[![GitHub Last Commit][repo-badge]][repo]
|
|
42
|
+
[![GitHub Issues][issues-badge]][issues]
|
|
43
|
+
[![Downloads][downloads-badge]][pypi]
|
|
44
|
+
[![Python Version][version-badge]][pypi]
|
|
45
|
+
|
|
46
|
+
```shell
|
|
47
|
+
pip install django-environment-config
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
**Documentation**: [https://mrthearman.github.io/django-environment-config/](https://mrthearman.github.io/django-environment-config/)
|
|
53
|
+
|
|
54
|
+
**Source Code**: [https://github.com/MrThearMan/django-environment-config/](https://github.com/MrThearMan/django-environment-config/)
|
|
55
|
+
|
|
56
|
+
**Contributing**: [https://github.com/MrThearMan/django-environment-config/blob/main/CONTRIBUTING.md](https://github.com/MrThearMan/django-environment-config/blob/main/CONTRIBUTING.md)
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
Configure django settings for multiple environments.
|
|
61
|
+
|
|
62
|
+
[coverage-badge]: https://coveralls.io/repos/github/MrThearMan/django-environment-config/badge.svg?branch=main
|
|
63
|
+
[status-badge]: https://img.shields.io/github/actions/workflow/status/MrThearMan/django-environment-config/test.yml?branch=main
|
|
64
|
+
[pypi-badge]: https://img.shields.io/pypi/v/django-environment-config
|
|
65
|
+
[licence-badge]: https://img.shields.io/github/license/MrThearMan/django-environment-config
|
|
66
|
+
[repo-badge]: https://img.shields.io/github/last-commit/MrThearMan/django-environment-config
|
|
67
|
+
[issues-badge]: https://img.shields.io/github/issues-raw/MrThearMan/django-environment-config
|
|
68
|
+
[version-badge]: https://img.shields.io/pypi/pyversions/django-environment-config
|
|
69
|
+
[downloads-badge]: https://img.shields.io/pypi/dm/django-environment-config
|
|
70
|
+
|
|
71
|
+
[coverage]: https://coveralls.io/github/MrThearMan/django-environment-config?branch=main
|
|
72
|
+
[status]: https://github.com/MrThearMan/django-environment-config/actions/workflows/test.yml
|
|
73
|
+
[pypi]: https://pypi.org/project/django-environment-config
|
|
74
|
+
[licence]: https://github.com/MrThearMan/django-environment-config/blob/main/LICENSE
|
|
75
|
+
[repo]: https://github.com/MrThearMan/django-environment-config/commits/main
|
|
76
|
+
[issues]: https://github.com/MrThearMan/django-environment-config/issues
|
|
77
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Django Environment Config
|
|
2
|
+
|
|
3
|
+
[![Coverage Status][coverage-badge]][coverage]
|
|
4
|
+
[![GitHub Workflow Status][status-badge]][status]
|
|
5
|
+
[![PyPI][pypi-badge]][pypi]
|
|
6
|
+
[![GitHub][licence-badge]][licence]
|
|
7
|
+
[![GitHub Last Commit][repo-badge]][repo]
|
|
8
|
+
[![GitHub Issues][issues-badge]][issues]
|
|
9
|
+
[![Downloads][downloads-badge]][pypi]
|
|
10
|
+
[![Python Version][version-badge]][pypi]
|
|
11
|
+
|
|
12
|
+
```shell
|
|
13
|
+
pip install django-environment-config
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
**Documentation**: [https://mrthearman.github.io/django-environment-config/](https://mrthearman.github.io/django-environment-config/)
|
|
19
|
+
|
|
20
|
+
**Source Code**: [https://github.com/MrThearMan/django-environment-config/](https://github.com/MrThearMan/django-environment-config/)
|
|
21
|
+
|
|
22
|
+
**Contributing**: [https://github.com/MrThearMan/django-environment-config/blob/main/CONTRIBUTING.md](https://github.com/MrThearMan/django-environment-config/blob/main/CONTRIBUTING.md)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
Configure django settings for multiple environments.
|
|
27
|
+
|
|
28
|
+
[coverage-badge]: https://coveralls.io/repos/github/MrThearMan/django-environment-config/badge.svg?branch=main
|
|
29
|
+
[status-badge]: https://img.shields.io/github/actions/workflow/status/MrThearMan/django-environment-config/test.yml?branch=main
|
|
30
|
+
[pypi-badge]: https://img.shields.io/pypi/v/django-environment-config
|
|
31
|
+
[licence-badge]: https://img.shields.io/github/license/MrThearMan/django-environment-config
|
|
32
|
+
[repo-badge]: https://img.shields.io/github/last-commit/MrThearMan/django-environment-config
|
|
33
|
+
[issues-badge]: https://img.shields.io/github/issues-raw/MrThearMan/django-environment-config
|
|
34
|
+
[version-badge]: https://img.shields.io/pypi/pyversions/django-environment-config
|
|
35
|
+
[downloads-badge]: https://img.shields.io/pypi/dm/django-environment-config
|
|
36
|
+
|
|
37
|
+
[coverage]: https://coveralls.io/github/MrThearMan/django-environment-config?branch=main
|
|
38
|
+
[status]: https://github.com/MrThearMan/django-environment-config/actions/workflows/test.yml
|
|
39
|
+
[pypi]: https://pypi.org/project/django-environment-config
|
|
40
|
+
[licence]: https://github.com/MrThearMan/django-environment-config/blob/main/LICENSE
|
|
41
|
+
[repo]: https://github.com/MrThearMan/django-environment-config/commits/main
|
|
42
|
+
[issues]: https://github.com/MrThearMan/django-environment-config/issues
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from django.utils.functional import classproperty
|
|
8
|
+
from dotenv import dotenv_values
|
|
9
|
+
|
|
10
|
+
from .constants import Undefined
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from dotenv.main import StrPath
|
|
14
|
+
|
|
15
|
+
from .typing import Any
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Environment",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Environment:
|
|
23
|
+
"""Configures a single environment."""
|
|
24
|
+
|
|
25
|
+
def __init_subclass__(
|
|
26
|
+
cls,
|
|
27
|
+
*,
|
|
28
|
+
dotenv_path: StrPath | None | Undefined = Undefined,
|
|
29
|
+
) -> None:
|
|
30
|
+
env: str | None = os.environ.get("DJANGO_SETTINGS_ENVIRONMENT")
|
|
31
|
+
if env is None: # pragma: no cover
|
|
32
|
+
msg = f"Environment variable 'DJANGO_SETTINGS_ENVIRONMENT' is not set before subclassing {cls.__name__!r}"
|
|
33
|
+
raise ValueError(msg)
|
|
34
|
+
|
|
35
|
+
if cls.__name__ != env:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# If not given, set it to `None` so that `python-dotenv` will use
|
|
39
|
+
# `dotenv.main.find_dotenv` to find the `.env` file automatically.
|
|
40
|
+
if dotenv_path is Undefined:
|
|
41
|
+
dotenv_path = None
|
|
42
|
+
# If set to `None` explicitly, do not load a `.env` file.
|
|
43
|
+
elif dotenv_path is None:
|
|
44
|
+
dotenv_path = Undefined
|
|
45
|
+
|
|
46
|
+
dotenv = cls.load_dotenv(dotenv_path=dotenv_path) if dotenv_path is not Undefined else Undefined
|
|
47
|
+
|
|
48
|
+
# Do name mangling to avoid overriding the attribute from a parent class.
|
|
49
|
+
# This way, we can have multiple environments with different `.env` files,
|
|
50
|
+
# and allow using values from a parent `.env` file as defaults (if desired).
|
|
51
|
+
setattr(cls, f"_{cls.__name__}__dotenv", dotenv)
|
|
52
|
+
setattr(cls, f"_{cls.__name__}__dotenv_path", dotenv_path)
|
|
53
|
+
|
|
54
|
+
cls.setup(stack_level=2)
|
|
55
|
+
cls.post_setup()
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def load_dotenv(*, dotenv_path: StrPath | None = None) -> dict[str, str]:
|
|
59
|
+
"""Load the `.env` file and return the values."""
|
|
60
|
+
return dotenv_values(dotenv_path=dotenv_path)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def setup(cls, *, stack_level: int = 1) -> None:
|
|
64
|
+
"""Load settings and set them in the module globals where the environment is defined."""
|
|
65
|
+
settings = cls.load_settings()
|
|
66
|
+
stack = inspect.stack()
|
|
67
|
+
caller_globals: dict[str, Any] = stack[stack_level].frame.f_globals
|
|
68
|
+
caller_globals.update(**settings)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def post_setup(cls) -> None:
|
|
72
|
+
"""Hook for doing additional setup after the settings have been loaded."""
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def load_settings(cls) -> dict[str, Any]:
|
|
76
|
+
"""Load the settings from the environment, validating and returning them."""
|
|
77
|
+
return {name: getattr(cls, name) for name in dir(cls) if name.isupper() and not name.startswith("_")}
|
|
78
|
+
|
|
79
|
+
@classproperty
|
|
80
|
+
def dotenv(cls) -> dict[str, str] | Undefined: # noqa: N805
|
|
81
|
+
return getattr(cls, f"_{cls.__name__}__dotenv", Undefined)
|
|
82
|
+
|
|
83
|
+
@classproperty
|
|
84
|
+
def dotenv_path(cls) -> str | None | Undefined: # noqa: N805
|
|
85
|
+
return getattr(cls, f"_{cls.__name__}__dotenv_path", Undefined)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"Undefined",
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Undefined: # pragma: no cover
|
|
9
|
+
"""Value to represent an undefined value."""
|
|
10
|
+
|
|
11
|
+
def __repr__(self) -> str:
|
|
12
|
+
return self.__class__.__name__
|
|
13
|
+
|
|
14
|
+
def __bool__(self) -> bool:
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Undefined = Undefined()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from env_config.constants import Undefined
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from env_config import Environment
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DjangoEnvConfigError",
|
|
13
|
+
"MissingEnvValueError",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DjangoEnvConfigError(Exception):
|
|
18
|
+
"""Base class for all Django Environment Config errors."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MissingEnvValueError(DjangoEnvConfigError):
|
|
22
|
+
"""Error raised when a value is not found in the .env file, and a default is not provided."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, *, name: str, env: type[Environment]) -> None:
|
|
25
|
+
msg = f"Value {name!r} in environment {env.__name__!r}"
|
|
26
|
+
if env.dotenv_path is not Undefined:
|
|
27
|
+
msg += " not defined in the .env file and value does not have a default"
|
|
28
|
+
else:
|
|
29
|
+
msg += " needs a default value since environment does not define a `dotenv_path`"
|
|
30
|
+
|
|
31
|
+
super().__init__(msg)
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any, Callable, Generator, Generic, Mapping, ParamSpec, Sequence, TypedDict, TypeVar
|
|
5
|
+
|
|
6
|
+
if sys.version_info < (3, 12): # pragma: no cover
|
|
7
|
+
from typing_extensions import Unpack
|
|
8
|
+
else:
|
|
9
|
+
from typing import Unpack
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Any",
|
|
13
|
+
"CacheConfig",
|
|
14
|
+
"Callable",
|
|
15
|
+
"DBConfig",
|
|
16
|
+
"DBConfigExtra",
|
|
17
|
+
"Generator",
|
|
18
|
+
"Generic",
|
|
19
|
+
"Mapping",
|
|
20
|
+
"ParamSpec",
|
|
21
|
+
"Sequence",
|
|
22
|
+
"TypeVar",
|
|
23
|
+
"Unpack",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DBConfig(TypedDict, total=False):
|
|
28
|
+
ATOMIC_REQUESTS: bool
|
|
29
|
+
AUTOCOMMIT: bool
|
|
30
|
+
CONN_MAX_AGE: int | None
|
|
31
|
+
CONN_HEALTH_CHECKS: bool
|
|
32
|
+
DISABLE_SERVER_SIDE_CURSORS: bool
|
|
33
|
+
ENGINE: str
|
|
34
|
+
HOST: str
|
|
35
|
+
NAME: str
|
|
36
|
+
OPTIONS: dict[str, Any] | None
|
|
37
|
+
PASSWORD: str
|
|
38
|
+
PORT: str | int
|
|
39
|
+
TEST: dict[str, Any]
|
|
40
|
+
TIME_ZONE: str
|
|
41
|
+
USER: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DBConfigExtra(TypedDict, total=False):
|
|
45
|
+
engine: str | None
|
|
46
|
+
conn_max_age: int | None
|
|
47
|
+
conn_health_checks: bool
|
|
48
|
+
ssl_require: bool
|
|
49
|
+
test_options: dict | None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CacheConfig(TypedDict, total=False):
|
|
53
|
+
BACKEND: str
|
|
54
|
+
KEY_FUNCTION: str
|
|
55
|
+
KEY_PREFIX: str
|
|
56
|
+
LOCATION: str
|
|
57
|
+
OPTIONS: dict[str, Any]
|
|
58
|
+
TIMEOUT: int
|
|
59
|
+
VERSION: int
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from django.utils.module_loading import import_string
|
|
11
|
+
|
|
12
|
+
from .constants import Undefined
|
|
13
|
+
from .errors import MissingEnvValueError
|
|
14
|
+
from .typing import Any, CacheConfig, DBConfig, DBConfigExtra, Generator, Generic, Mapping, Sequence, TypeVar, Unpack
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .base import Environment
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"BooleanValue",
|
|
22
|
+
"CacheURLValue",
|
|
23
|
+
"DatabaseURLValue",
|
|
24
|
+
"DecimalValue",
|
|
25
|
+
"DictValue",
|
|
26
|
+
"EmailValue",
|
|
27
|
+
"FloatValue",
|
|
28
|
+
"IPValue",
|
|
29
|
+
"ImportStringValue",
|
|
30
|
+
"IntegerValue",
|
|
31
|
+
"JsonValue",
|
|
32
|
+
"ListValue",
|
|
33
|
+
"MappingValue",
|
|
34
|
+
"PathValue",
|
|
35
|
+
"PositiveIntegerValue",
|
|
36
|
+
"RegexValue",
|
|
37
|
+
"SequenceValue",
|
|
38
|
+
"SetValue",
|
|
39
|
+
"StringValue",
|
|
40
|
+
"TupleValue",
|
|
41
|
+
"URLValue",
|
|
42
|
+
"Value",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
T = TypeVar("T")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Value(ABC, Generic[T]):
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
default: T | None = Undefined,
|
|
54
|
+
env_name: str | Undefined = Undefined,
|
|
55
|
+
nullable: bool = False,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Value descriptor for an environment variable.
|
|
59
|
+
|
|
60
|
+
:param default: The default value to use if the environment variable is not set.
|
|
61
|
+
:param env_name: The name of the environment variable to use. If not given, the name of the field is used.
|
|
62
|
+
"""
|
|
63
|
+
self.default: T | None = default
|
|
64
|
+
self.name: str = env_name
|
|
65
|
+
self.nullable: bool = nullable
|
|
66
|
+
|
|
67
|
+
# Use a map to store the value per environment so that we can have
|
|
68
|
+
# different values for environments what inherit from each other.
|
|
69
|
+
self.value_by_environment: dict[type[Environment], Any] = defaultdict(lambda: Undefined)
|
|
70
|
+
super().__init__()
|
|
71
|
+
|
|
72
|
+
def __set_name__(self, env: type[Environment], name: str) -> None:
|
|
73
|
+
"""Called after the owner Environment-class is created with this field as a class attribute."""
|
|
74
|
+
if self.name is Undefined:
|
|
75
|
+
self.name = name
|
|
76
|
+
|
|
77
|
+
def __get__(self, _: Environment | None, env: type[Environment]) -> T:
|
|
78
|
+
"""Called when accessing the field on the class or an instance of the class."""
|
|
79
|
+
if self.value_by_environment[env] is not Undefined:
|
|
80
|
+
return self.value_by_environment[env]
|
|
81
|
+
|
|
82
|
+
self.value_by_environment[env] = self.get_for_environment(env)
|
|
83
|
+
return self.value_by_environment[env]
|
|
84
|
+
|
|
85
|
+
def get_for_environment(self, env: type[Environment]) -> T:
|
|
86
|
+
value = self.default if env.dotenv is Undefined else env.dotenv.get(self.name, self.default)
|
|
87
|
+
if value is Undefined:
|
|
88
|
+
raise MissingEnvValueError(name=self.name, env=env)
|
|
89
|
+
|
|
90
|
+
if value is None:
|
|
91
|
+
if self.nullable:
|
|
92
|
+
return None
|
|
93
|
+
msg = f"Value for {self.name!r} cannot be None"
|
|
94
|
+
raise ValueError(msg)
|
|
95
|
+
|
|
96
|
+
return self.convert(value)
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def convert(self, value: str | T) -> T: # pragma: no cover
|
|
100
|
+
"""Convert the given value into the proper representation."""
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class StringValue(Value[str]):
|
|
105
|
+
"""Parses env variables into a string value."""
|
|
106
|
+
|
|
107
|
+
def convert(self, value: str) -> str:
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class BooleanValue(Value[bool]):
|
|
112
|
+
"""Parses env variables into a boolean value."""
|
|
113
|
+
|
|
114
|
+
def convert(self, value: str | bool) -> bool: # noqa: FBT001
|
|
115
|
+
if isinstance(value, bool):
|
|
116
|
+
return value
|
|
117
|
+
normalized_value = value.strip().lower()
|
|
118
|
+
if normalized_value in ("yes", "y", "true", "1"):
|
|
119
|
+
return True
|
|
120
|
+
if normalized_value in ("no", "n", "false", "0", ""):
|
|
121
|
+
return False
|
|
122
|
+
msg = f"Cannot interpret {value!r} as a boolean value"
|
|
123
|
+
raise ValueError(msg)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class IntegerValue(Value[int]):
|
|
127
|
+
"""Parses env variables into an integer value."""
|
|
128
|
+
|
|
129
|
+
def convert(self, value: str | int) -> int:
|
|
130
|
+
return int(value)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class PositiveIntegerValue(IntegerValue):
|
|
134
|
+
"""Parses env variables into an integer value, and validates that the value is positive."""
|
|
135
|
+
|
|
136
|
+
def convert(self, value: str | int) -> int:
|
|
137
|
+
val = super().convert(value)
|
|
138
|
+
if val < 0:
|
|
139
|
+
msg = f"Value must be positive, got {val}"
|
|
140
|
+
raise ValueError(msg)
|
|
141
|
+
return val
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class FloatValue(Value[float]):
|
|
145
|
+
"""Parses env variables into a float value."""
|
|
146
|
+
|
|
147
|
+
def convert(self, value: str | float) -> float:
|
|
148
|
+
return float(value)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class DecimalValue(Value[Decimal], Decimal):
|
|
152
|
+
"""Parses env variables into a Decimal value."""
|
|
153
|
+
|
|
154
|
+
def convert(self, value: str | Decimal) -> Decimal:
|
|
155
|
+
return Decimal(value)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ImportStringValue(Value[str]):
|
|
159
|
+
"""Parses env variables into a string value, and validates that the value is an importable string."""
|
|
160
|
+
|
|
161
|
+
def convert(self, value: str) -> str:
|
|
162
|
+
import_string(value)
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SequenceValue(Value, ABC, Generic[T]):
|
|
167
|
+
def __init__( # noqa: PLR0913
|
|
168
|
+
self,
|
|
169
|
+
child: Value[T] | None = None,
|
|
170
|
+
*,
|
|
171
|
+
default: Sequence[T] | None = Undefined,
|
|
172
|
+
env_name: str | Undefined = Undefined,
|
|
173
|
+
nullable: bool = False,
|
|
174
|
+
delimiter: str = ",",
|
|
175
|
+
) -> None:
|
|
176
|
+
self.child = child or StringValue()
|
|
177
|
+
self.delimiter = delimiter
|
|
178
|
+
super().__init__(default=default, env_name=env_name, nullable=nullable)
|
|
179
|
+
|
|
180
|
+
def iterate(self, value: str | Sequence[Any]) -> Generator[T, None, None]:
|
|
181
|
+
seq = value.split(self.delimiter) if isinstance(value, str) else value
|
|
182
|
+
for item in seq:
|
|
183
|
+
if not item:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
yield self.child.convert(item.strip())
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ListValue(SequenceValue):
|
|
190
|
+
"""Parses env variables like `item1,item2,item3` into a list."""
|
|
191
|
+
|
|
192
|
+
def convert(self, value: str | list[Any]) -> list[Any]:
|
|
193
|
+
return list(self.iterate(value))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class TupleValue(SequenceValue):
|
|
197
|
+
"""Parses env variables like `item1,item2,item3` into a tuple."""
|
|
198
|
+
|
|
199
|
+
def convert(self, value: str | tuple[Any, ...]) -> tuple[Any, ...]:
|
|
200
|
+
return tuple(self.iterate(value))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class SetValue(SequenceValue):
|
|
204
|
+
"""Parses env variables like `item1,item2,item3` into a set."""
|
|
205
|
+
|
|
206
|
+
def convert(self, value: str | set[str]) -> set[Any]:
|
|
207
|
+
return set(self.iterate(value))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class MappingValue(Value, ABC, Generic[T]):
|
|
211
|
+
def __init__( # noqa: PLR0913
|
|
212
|
+
self,
|
|
213
|
+
child: Value[T] | None = None,
|
|
214
|
+
*,
|
|
215
|
+
default: Mapping[str, T] | None = Undefined,
|
|
216
|
+
env_name: str | Undefined = Undefined,
|
|
217
|
+
nullable: bool = False,
|
|
218
|
+
kv_delimiter: str = "=",
|
|
219
|
+
item_delimiter: str = ";",
|
|
220
|
+
) -> None:
|
|
221
|
+
self.child = child or StringValue()
|
|
222
|
+
self.kv_delimiter = kv_delimiter
|
|
223
|
+
self.item_delimiter = item_delimiter
|
|
224
|
+
super().__init__(default=default, env_name=env_name, nullable=nullable)
|
|
225
|
+
|
|
226
|
+
def iterate(self, value: str | Mapping[str, Any]) -> Generator[tuple[str, Any], None, None]:
|
|
227
|
+
seq = value.split(self.item_delimiter) if isinstance(value, str) else value.items()
|
|
228
|
+
|
|
229
|
+
for item in seq:
|
|
230
|
+
if not item:
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
if isinstance(item, str):
|
|
234
|
+
kv = item.split(self.kv_delimiter, 1)
|
|
235
|
+
if len(kv) != 2: # noqa: PLR2004
|
|
236
|
+
msg = f"Cannot split key-value pair from {item!r}"
|
|
237
|
+
raise ValueError(msg)
|
|
238
|
+
else:
|
|
239
|
+
kv = item
|
|
240
|
+
|
|
241
|
+
yield kv[0].strip(), self.child.convert(kv[1].strip())
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class DictValue(MappingValue):
|
|
245
|
+
"""Parses env variables like `key1=value1;key2=value2` into a dict."""
|
|
246
|
+
|
|
247
|
+
def convert(self, value: str | dict[str, Any]) -> dict[str, Any]:
|
|
248
|
+
return dict(self.iterate(value))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class JsonValue(Value[dict | list]):
|
|
252
|
+
"""Parses env variables from a json string to a python list or dict."""
|
|
253
|
+
|
|
254
|
+
def convert(self, value: str | list | dict) -> list | dict:
|
|
255
|
+
if isinstance(value, (list, dict)):
|
|
256
|
+
return value
|
|
257
|
+
return json.loads(value)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class EmailValue(StringValue):
|
|
261
|
+
"""Parses env variables into a string value, and validates that it's a valid email address."""
|
|
262
|
+
|
|
263
|
+
def convert(self, value: str) -> str:
|
|
264
|
+
from django.core.validators import validate_email
|
|
265
|
+
|
|
266
|
+
validate_email(value)
|
|
267
|
+
return value
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class URLValue(StringValue):
|
|
271
|
+
"""Parses env variables into a string value, and validates that it's a valid URL."""
|
|
272
|
+
|
|
273
|
+
def convert(self, value: str) -> str:
|
|
274
|
+
from django.core.validators import URLValidator
|
|
275
|
+
|
|
276
|
+
URLValidator()(value)
|
|
277
|
+
return value
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class IPValue(StringValue):
|
|
281
|
+
"""Parses env variables into a string value, and validates that it's a valid IP address."""
|
|
282
|
+
|
|
283
|
+
def convert(self, value: str) -> str:
|
|
284
|
+
from django.core.validators import validate_ipv46_address
|
|
285
|
+
|
|
286
|
+
validate_ipv46_address(value)
|
|
287
|
+
return value
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class RegexValue(StringValue):
|
|
291
|
+
"""Parses env variables into a string value, and validates that it matches the given regex."""
|
|
292
|
+
|
|
293
|
+
def __init__(
|
|
294
|
+
self,
|
|
295
|
+
*,
|
|
296
|
+
regex: str,
|
|
297
|
+
default: str | None = Undefined,
|
|
298
|
+
nullable: bool = False,
|
|
299
|
+
env_name: str | Undefined = Undefined,
|
|
300
|
+
) -> None:
|
|
301
|
+
self.regex = regex
|
|
302
|
+
super().__init__(default=default, env_name=env_name, nullable=nullable)
|
|
303
|
+
|
|
304
|
+
def convert(self, value: str) -> str:
|
|
305
|
+
from django.core.validators import RegexValidator
|
|
306
|
+
|
|
307
|
+
RegexValidator(regex=self.regex)(value)
|
|
308
|
+
return value
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class PathValue(StringValue):
|
|
312
|
+
"""Parses env variable into a string value, and can optionally validate that the path exists."""
|
|
313
|
+
|
|
314
|
+
def __init__(
|
|
315
|
+
self,
|
|
316
|
+
*,
|
|
317
|
+
default: str | None = Undefined,
|
|
318
|
+
env_name: str | Undefined = Undefined,
|
|
319
|
+
nullable: bool = False,
|
|
320
|
+
check_exists: bool = True,
|
|
321
|
+
) -> None:
|
|
322
|
+
self.check_exists = check_exists
|
|
323
|
+
super().__init__(default=default, env_name=env_name, nullable=nullable)
|
|
324
|
+
|
|
325
|
+
def convert(self, value: str) -> str:
|
|
326
|
+
path = Path(value).absolute()
|
|
327
|
+
if self.check_exists and not path.exists():
|
|
328
|
+
msg = f"Path '{path}' does not exist"
|
|
329
|
+
raise ValueError(msg)
|
|
330
|
+
|
|
331
|
+
return str(path)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class DatabaseURLValue(Value[DBConfig | str]):
|
|
335
|
+
"""Load a database configuration from a URL."""
|
|
336
|
+
|
|
337
|
+
def __init__(
|
|
338
|
+
self,
|
|
339
|
+
*,
|
|
340
|
+
db_alias: str = "default",
|
|
341
|
+
default: DBConfig | str = Undefined,
|
|
342
|
+
env_name: str = "DATABASE_URL",
|
|
343
|
+
**params: Unpack[DBConfigExtra],
|
|
344
|
+
) -> None:
|
|
345
|
+
self.db_alias = db_alias
|
|
346
|
+
self.params = params
|
|
347
|
+
super().__init__(default=default, env_name=env_name)
|
|
348
|
+
|
|
349
|
+
def convert(self, value: str | DBConfig) -> dict[str, DBConfig]:
|
|
350
|
+
if not isinstance(value, str):
|
|
351
|
+
return {self.db_alias: value}
|
|
352
|
+
|
|
353
|
+
from dj_database_url import parse
|
|
354
|
+
|
|
355
|
+
config = parse(value, **self.params)
|
|
356
|
+
return {self.db_alias: config}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class CacheURLValue(Value[CacheConfig | str]):
|
|
360
|
+
"""Load a cache configuration from a URL."""
|
|
361
|
+
|
|
362
|
+
def __init__(
|
|
363
|
+
self,
|
|
364
|
+
*,
|
|
365
|
+
cache_alias: str = "default",
|
|
366
|
+
default: CacheConfig | str = Undefined,
|
|
367
|
+
env_name: str = "CACHE_URL",
|
|
368
|
+
) -> None:
|
|
369
|
+
self.cache_alias = cache_alias
|
|
370
|
+
super().__init__(default=default, env_name=env_name)
|
|
371
|
+
|
|
372
|
+
def convert(self, value: str | CacheConfig) -> dict[str, CacheConfig]:
|
|
373
|
+
if not isinstance(value, str):
|
|
374
|
+
return {self.cache_alias: value}
|
|
375
|
+
|
|
376
|
+
from django_cache_url import parse
|
|
377
|
+
|
|
378
|
+
config = parse(value)
|
|
379
|
+
return {self.cache_alias: config}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "django-environment-config"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "Configure django settings for multiple environments."
|
|
5
|
+
authors = [
|
|
6
|
+
"Matti Lamppu <lamppu.matti.akseli@gmail.com>",
|
|
7
|
+
]
|
|
8
|
+
packages = [
|
|
9
|
+
{ include = "env_config" },
|
|
10
|
+
]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
homepage = "https://mrthearman.github.io/django-environment-config"
|
|
14
|
+
repository = "https://github.com/MrThearMan/django-environment-config"
|
|
15
|
+
keywords = [
|
|
16
|
+
"django",
|
|
17
|
+
"configuration",
|
|
18
|
+
"settings",
|
|
19
|
+
"environment",
|
|
20
|
+
"env",
|
|
21
|
+
"config",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Environment :: Web Environment",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"Natural Language :: English",
|
|
28
|
+
"Programming Language :: Python",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Framework :: Django :: 4.2",
|
|
35
|
+
"Framework :: Django :: 5.0",
|
|
36
|
+
]
|
|
37
|
+
include = [
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
]
|
|
41
|
+
exclude = [
|
|
42
|
+
"tests",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.poetry.urls]
|
|
46
|
+
"Bug Tracker" = "https://github.com/MrThearMan/django-environment-config/issues"
|
|
47
|
+
|
|
48
|
+
[tool.poetry.dependencies]
|
|
49
|
+
python = ">=3.10,<4"
|
|
50
|
+
Django = ">=4.2"
|
|
51
|
+
python-dotenv = ">=1.0.1"
|
|
52
|
+
typing-extensions = { version = ">=4.11.0", python = "<3.12" }
|
|
53
|
+
dj-database-url = { version = "^2.1.0", optional = true }
|
|
54
|
+
django-cache-url = { version = "^3.4.5", optional = true }
|
|
55
|
+
|
|
56
|
+
[tool.poetry.group.test.dependencies]
|
|
57
|
+
pytest = "8.2.0"
|
|
58
|
+
coverage = "7.5.0"
|
|
59
|
+
pytest-django = "4.8.0"
|
|
60
|
+
pre-commit = "3.7.0"
|
|
61
|
+
tox = "4.15.0"
|
|
62
|
+
tox-gh-actions = "3.2.0"
|
|
63
|
+
|
|
64
|
+
[tool.poetry.group.docs.dependencies]
|
|
65
|
+
mkdocs = "1.6.0"
|
|
66
|
+
pymdown-extensions = "10.8.1"
|
|
67
|
+
mkdocs-mermaid2-plugin = "1.1.1"
|
|
68
|
+
|
|
69
|
+
[tool.poetry.group.lint.dependencies]
|
|
70
|
+
mypy = "1.10.0"
|
|
71
|
+
django-stubs = "4.2.7"
|
|
72
|
+
|
|
73
|
+
[tool.poetry.extras]
|
|
74
|
+
db = ["dj-database-url"]
|
|
75
|
+
cache = ["django-cache-url"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff]
|
|
78
|
+
fix = true
|
|
79
|
+
unsafe-fixes = true
|
|
80
|
+
line-length = 120
|
|
81
|
+
extend-exclude = [
|
|
82
|
+
"tests/*",
|
|
83
|
+
]
|
|
84
|
+
lint.typing-modules = [
|
|
85
|
+
"env_config.typing",
|
|
86
|
+
]
|
|
87
|
+
lint.explicit-preview-rules = true
|
|
88
|
+
lint.preview = true
|
|
89
|
+
lint.select = [
|
|
90
|
+
"A", # flake8-builtins
|
|
91
|
+
"ANN", # flake8-annotations
|
|
92
|
+
"ARG", # flake8-unused-arguments
|
|
93
|
+
"B", # flake8-bugbear
|
|
94
|
+
"BLE", # flake8-blind-except
|
|
95
|
+
"C90", # mccabe
|
|
96
|
+
"C4", # flake8-comprehensions
|
|
97
|
+
"COM", # flake8-commas
|
|
98
|
+
"D200", # pydocstyle: One-line docstring should fit on one line
|
|
99
|
+
"D201", # pydocstyle: No blank lines allowed before function docstring (found {num_lines})
|
|
100
|
+
"D202", # pydocstyle: No blank lines allowed after function docstring (found {num_lines})
|
|
101
|
+
"D209", # pydocstyle: Multi-line docstring closing quotes should be on a separate line
|
|
102
|
+
"D213", # pydocstyle: Multi-line docstring summary should start at the second line
|
|
103
|
+
"DJ", # flake8-django
|
|
104
|
+
"DTZ", # flake8-datetimez
|
|
105
|
+
"E", # pycodestyle errors
|
|
106
|
+
"EM", # flake8-errmsg
|
|
107
|
+
"F", # pyflakes
|
|
108
|
+
"FA", # flake8-future-annotations
|
|
109
|
+
"FBT", # flake8-boolean-trap
|
|
110
|
+
"FLY", # flynt
|
|
111
|
+
"G", # flake8-logging-format
|
|
112
|
+
"I", # isort
|
|
113
|
+
"ICN", # flake8-import-conventions
|
|
114
|
+
"INP", # flake8-no-pep420
|
|
115
|
+
"INT", # flake8-gettext
|
|
116
|
+
"ISC", # flake8-implicit-str-concat
|
|
117
|
+
"LOG", # flake8-logging
|
|
118
|
+
"N", # pep8-naming
|
|
119
|
+
"PERF", # perflint
|
|
120
|
+
"PGH", # pygrep-hooks
|
|
121
|
+
"PIE", # flake8-pie
|
|
122
|
+
"PL", # pylint
|
|
123
|
+
"PT", # flake8-pytest-style
|
|
124
|
+
"PTH", # flake8-use-pathlib
|
|
125
|
+
"PYI", # flake8-pyi
|
|
126
|
+
"Q", # flake8-quotes
|
|
127
|
+
"R", # Refactor
|
|
128
|
+
"RET", # flake8-return
|
|
129
|
+
"RSE", # flake8-raise
|
|
130
|
+
"RUF", # ruff-specific rules
|
|
131
|
+
"S", # flake8-bandit
|
|
132
|
+
"SIM", # flake8-simplify
|
|
133
|
+
"SLF", # flake8-self
|
|
134
|
+
"T20", # flake8-print
|
|
135
|
+
"TCH", # flake8-type-checking
|
|
136
|
+
"TID", # flake8-tidy-imports
|
|
137
|
+
"TRY", # tryceratops
|
|
138
|
+
"UP", # pyupgrade
|
|
139
|
+
"W", # pycodestyle warnings
|
|
140
|
+
]
|
|
141
|
+
# Preview rules
|
|
142
|
+
lint.extend-select = [
|
|
143
|
+
"B909", # loop-iterator-mutation
|
|
144
|
+
"FURB110", # if-exp-instead-of-or-operator
|
|
145
|
+
"FURB142", # for-loop-set-mutations
|
|
146
|
+
"FURB145", # slice-copy
|
|
147
|
+
"FURB171", # single-item-membership-test
|
|
148
|
+
"FURB187", # list-reverse-copy
|
|
149
|
+
"PLE0307", # invalid-str-return-type
|
|
150
|
+
"PLR0916", # too-many-boolean-expressions
|
|
151
|
+
"PLR1730", # if-stmt-min-max
|
|
152
|
+
"PLR1733", # unnecessary-dict-index-lookup
|
|
153
|
+
"PLR1736", # unnecessary-list-index-lookup
|
|
154
|
+
"PLR6104", # non-augmented-assignment
|
|
155
|
+
"PLW0211", # bad-staticmethod-argument
|
|
156
|
+
"PLW0642", # self-or-cls-assignment
|
|
157
|
+
"RUF021", # parenthesize-chained-operators
|
|
158
|
+
"RUF022", # unsorted-dunder-all
|
|
159
|
+
"UP042", # replace-str-enum
|
|
160
|
+
]
|
|
161
|
+
lint.ignore = [
|
|
162
|
+
"ANN101", # Missing type annotation for `self` in method
|
|
163
|
+
"ANN102", # Missing type annotation for `cls` in method
|
|
164
|
+
"ANN401", # Any-typing allowed
|
|
165
|
+
"ARG001", # Unused function argument
|
|
166
|
+
"ARG002", # Unused method argument
|
|
167
|
+
"ARG003", # Unused class method argument
|
|
168
|
+
"RUF012", # No need for ClassVar
|
|
169
|
+
"G004", # Logging statement uses f-string
|
|
170
|
+
#
|
|
171
|
+
# Conflicting with ruff-format
|
|
172
|
+
#
|
|
173
|
+
"COM812", # missing-trailing-comma
|
|
174
|
+
"COM819", # prohibited-trailing-comma
|
|
175
|
+
"D206", # indent-with-spaces
|
|
176
|
+
"D300", # triple-single-quotes
|
|
177
|
+
"E111", # indentation-with-invalid-multiple
|
|
178
|
+
"E114", # indentation-with-invalid-multiple-comment
|
|
179
|
+
"E117", # over-indented
|
|
180
|
+
"ISC001", # single-line-implicit-string-concatenation
|
|
181
|
+
"ISC002", # multi-line-implicit-string-concatenation
|
|
182
|
+
"Q000", # bad-quotes-inline-string
|
|
183
|
+
"Q001", # bad-quotes-multiline-string
|
|
184
|
+
"Q002", # bad-quotes-docstring
|
|
185
|
+
"Q003", # avoidable-escaped-quote
|
|
186
|
+
"W191", # tab-indentation
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
190
|
+
"test_*.py" = [
|
|
191
|
+
"ANN", # No need to annotate tests
|
|
192
|
+
"N801", # Allow whatever class names in tests
|
|
193
|
+
"N802", # Allow whatever function names in tests
|
|
194
|
+
"N815", # Allow whatever variable names in tests
|
|
195
|
+
"PLR0915", # Can have as many statements as needed
|
|
196
|
+
"PLR2004", # Magic value comparisons are allowed in tests
|
|
197
|
+
"S101", # Assert is fine
|
|
198
|
+
"S105", # Hardcoded passwords are fine in tests
|
|
199
|
+
"S106", # Hardcoded passwords are fine in tests
|
|
200
|
+
"S108", # No need to care about insecure temp file usages in tests
|
|
201
|
+
"S311", # Pseudo-random generators are fine here
|
|
202
|
+
"SLF", # Allow accessing private members in tests
|
|
203
|
+
"UP", # No upgrade rules
|
|
204
|
+
]
|
|
205
|
+
"conftest.py" = [
|
|
206
|
+
"ARG", # Fixtures can be unused
|
|
207
|
+
"ANN", # No need to annotate tests
|
|
208
|
+
"UP", # No upgrade rules
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
[tool.ruff.lint.isort]
|
|
212
|
+
combine-as-imports = false
|
|
213
|
+
split-on-trailing-comma = false
|
|
214
|
+
known-third-party = [
|
|
215
|
+
"django",
|
|
216
|
+
]
|
|
217
|
+
known-first-party = [
|
|
218
|
+
"env_config",
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
[tool.ruff.lint.flake8-import-conventions]
|
|
222
|
+
banned-from = [
|
|
223
|
+
"base64",
|
|
224
|
+
"csv",
|
|
225
|
+
"datetime",
|
|
226
|
+
"hashlib",
|
|
227
|
+
"hmac",
|
|
228
|
+
"json",
|
|
229
|
+
"logging",
|
|
230
|
+
"math",
|
|
231
|
+
"os",
|
|
232
|
+
"pickle",
|
|
233
|
+
"random",
|
|
234
|
+
"re",
|
|
235
|
+
"shutil",
|
|
236
|
+
"subprocess",
|
|
237
|
+
"sys",
|
|
238
|
+
"tempfile",
|
|
239
|
+
"time",
|
|
240
|
+
"uuid",
|
|
241
|
+
"xml",
|
|
242
|
+
"yaml",
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
[tool.mypy]
|
|
246
|
+
python_version = "3.12"
|
|
247
|
+
warn_return_any = "True"
|
|
248
|
+
warn_unused_configs = "True"
|
|
249
|
+
ignore_missing_imports = "True"
|
|
250
|
+
plugins = [
|
|
251
|
+
"mypy_django_plugin.main",
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
[tool.django-stubs]
|
|
255
|
+
django_settings_module = "tests.project.settings"
|
|
256
|
+
|
|
257
|
+
[tool.coverage.run]
|
|
258
|
+
relative_files = true
|
|
259
|
+
|
|
260
|
+
[tool.coverage.report]
|
|
261
|
+
omit = [
|
|
262
|
+
"tests/*",
|
|
263
|
+
"docs/*",
|
|
264
|
+
".venv/*",
|
|
265
|
+
".tox/*",
|
|
266
|
+
]
|
|
267
|
+
exclude_lines = [
|
|
268
|
+
"if TYPE_CHECKING:",
|
|
269
|
+
"except ImportError:",
|
|
270
|
+
"pragma: no cover",
|
|
271
|
+
"pass",
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
[tool.pytest.ini_options]
|
|
275
|
+
DJANGO_SETTINGS_MODULE = "tests.project.settings"
|
|
276
|
+
addopts = "-vv -s --disable-warnings"
|
|
277
|
+
|
|
278
|
+
[tool.tox]
|
|
279
|
+
legacy_tox_ini = """
|
|
280
|
+
[tox]
|
|
281
|
+
envlist = py{310, 311, 312}-django{42, 50}
|
|
282
|
+
isolated_build = true
|
|
283
|
+
|
|
284
|
+
[gh-actions]
|
|
285
|
+
python =
|
|
286
|
+
3.10: py310
|
|
287
|
+
3.11: py311
|
|
288
|
+
3.12: py312
|
|
289
|
+
|
|
290
|
+
[testenv]
|
|
291
|
+
allowlist_externals =
|
|
292
|
+
poetry
|
|
293
|
+
setenv =
|
|
294
|
+
PYTHONPATH = {toxinidir}
|
|
295
|
+
DJANGO_SETTINGS_MODULE = tests.project.settings
|
|
296
|
+
deps =
|
|
297
|
+
django42: Django >=4.2,<4.3
|
|
298
|
+
django50: Django >=5.0,<5.1
|
|
299
|
+
|
|
300
|
+
python-dotenv
|
|
301
|
+
dj-database-url
|
|
302
|
+
django-cache-url
|
|
303
|
+
typing-extensions
|
|
304
|
+
|
|
305
|
+
pytest
|
|
306
|
+
coverage
|
|
307
|
+
pytest-django
|
|
308
|
+
commands =
|
|
309
|
+
coverage run -m pytest -vv -s --disable-warnings
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
[build-system]
|
|
313
|
+
requires = ["poetry-core>=1.7.0"]
|
|
314
|
+
build-backend = "poetry.core.masonry.api"
|