ScoloConfig 0.1.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.
- scoloconfig-0.1.0/LICENSE +21 -0
- scoloconfig-0.1.0/PKG-INFO +126 -0
- scoloconfig-0.1.0/README.md +94 -0
- scoloconfig-0.1.0/pyproject.toml +59 -0
- scoloconfig-0.1.0/setup.cfg +4 -0
- scoloconfig-0.1.0/src/ScoloConfig.egg-info/PKG-INFO +126 -0
- scoloconfig-0.1.0/src/ScoloConfig.egg-info/SOURCES.txt +12 -0
- scoloconfig-0.1.0/src/ScoloConfig.egg-info/dependency_links.txt +1 -0
- scoloconfig-0.1.0/src/ScoloConfig.egg-info/requires.txt +7 -0
- scoloconfig-0.1.0/src/ScoloConfig.egg-info/top_level.txt +1 -0
- scoloconfig-0.1.0/src/scoloconfig/__init__.py +23 -0
- scoloconfig-0.1.0/src/scoloconfig/core.py +209 -0
- scoloconfig-0.1.0/src/scoloconfig/py.typed +0 -0
- scoloconfig-0.1.0/tests/test_scoloconfig.py +99 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 G3tFun
|
|
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.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ScoloConfig
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Safe typed configuration for Python bots and services.
|
|
5
|
+
Author: G3tFun
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/G3tFun/ScoloConfig
|
|
8
|
+
Project-URL: Repository, https://github.com/G3tFun/ScoloConfig
|
|
9
|
+
Project-URL: Documentation, https://github.com/G3tFun/ScoloConfig#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/G3tFun/ScoloConfig/issues
|
|
11
|
+
Keywords: configuration,settings,dotenv,secrets,pydantic,telegram-bot
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pydantic-settings<3,>=2.4
|
|
26
|
+
Requires-Dist: ScoloLogger<0.2,>=0.1
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# ScoloConfig
|
|
34
|
+
|
|
35
|
+
ScoloConfig provides safe typed configuration for Python bots and services. It uses `pydantic-settings` for validation, reads process environment and optional `.env` files, masks secrets by default, and emits safe startup events through ScoloLogger.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install ScoloConfig
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from pydantic import SecretStr
|
|
47
|
+
from scoloconfig import ScoloSettings, SettingsConfig
|
|
48
|
+
|
|
49
|
+
class AppConfig(ScoloSettings):
|
|
50
|
+
model_config = SettingsConfig(
|
|
51
|
+
env_file='.env',
|
|
52
|
+
env_prefix='BOT_',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
token: SecretStr
|
|
56
|
+
debug: bool = False
|
|
57
|
+
request_timeout: float = 35.0
|
|
58
|
+
|
|
59
|
+
config = AppConfig.load()
|
|
60
|
+
config.log_startup()
|
|
61
|
+
|
|
62
|
+
bot_token = config.token.get_secret_value()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
With this model, `BOT_TOKEN`, `BOT_DEBUG`, and `BOT_REQUEST_TIMEOUT` are read from the process environment. Values in the process environment always override `.env`.
|
|
66
|
+
|
|
67
|
+
## `.env`
|
|
68
|
+
|
|
69
|
+
```dotenv
|
|
70
|
+
BOT_TOKEN=123456:replace-with-a-real-token-locally
|
|
71
|
+
BOT_DEBUG=false
|
|
72
|
+
BOT_REQUEST_TIMEOUT=35
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Commit `.env.example`, not `.env`. Production deployments should inject settings through the process environment or a dedicated secret manager.
|
|
76
|
+
|
|
77
|
+
## Secret safety
|
|
78
|
+
|
|
79
|
+
Use `SecretStr` or `SecretBytes` for credentials. ScoloConfig also masks fields whose names include `token`, `secret`, `password`, `api_key`, `private_key`, `authorization`, or `cookie`.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
config.redacted_dump()
|
|
83
|
+
# {
|
|
84
|
+
# 'token': '***REDACTED***',
|
|
85
|
+
# 'debug': False,
|
|
86
|
+
# 'request_timeout': 35.0,
|
|
87
|
+
# }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Secrets are not included in `repr`, `redacted_json()`, `source_report()`, `log_startup()`, or safe validation errors. Reveal a secret only at the narrow point where a client requires it:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
token = config.token.get_secret_value()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Explicit environment names
|
|
97
|
+
|
|
98
|
+
Use `env()` when a field should map to an exact environment variable instead of a prefix-derived name.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from pydantic import SecretStr
|
|
102
|
+
from scoloconfig import ScoloSettings, env
|
|
103
|
+
|
|
104
|
+
class LegacyConfig(ScoloSettings):
|
|
105
|
+
telegram_token: SecretStr = env('TELEGRAM_BOT_TOKEN')
|
|
106
|
+
retries: int = env('RETRIES', default=3)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Safe startup report
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from scolologger import configure
|
|
113
|
+
|
|
114
|
+
configure(level='INFO', json_output=True)
|
|
115
|
+
report = config.log_startup()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The report includes only the settings class name, source categories, secret field names, and number of validated fields. It never includes configuration values.
|
|
119
|
+
|
|
120
|
+
## Limits
|
|
121
|
+
|
|
122
|
+
ScoloConfig 0.1.0 supports process environment and `.env` files. It does not fetch remote secrets, watch files, mutate a running configuration object, or replace a dedicated secrets manager.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# ScoloConfig
|
|
2
|
+
|
|
3
|
+
ScoloConfig provides safe typed configuration for Python bots and services. It uses `pydantic-settings` for validation, reads process environment and optional `.env` files, masks secrets by default, and emits safe startup events through ScoloLogger.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ScoloConfig
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from pydantic import SecretStr
|
|
15
|
+
from scoloconfig import ScoloSettings, SettingsConfig
|
|
16
|
+
|
|
17
|
+
class AppConfig(ScoloSettings):
|
|
18
|
+
model_config = SettingsConfig(
|
|
19
|
+
env_file='.env',
|
|
20
|
+
env_prefix='BOT_',
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
token: SecretStr
|
|
24
|
+
debug: bool = False
|
|
25
|
+
request_timeout: float = 35.0
|
|
26
|
+
|
|
27
|
+
config = AppConfig.load()
|
|
28
|
+
config.log_startup()
|
|
29
|
+
|
|
30
|
+
bot_token = config.token.get_secret_value()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
With this model, `BOT_TOKEN`, `BOT_DEBUG`, and `BOT_REQUEST_TIMEOUT` are read from the process environment. Values in the process environment always override `.env`.
|
|
34
|
+
|
|
35
|
+
## `.env`
|
|
36
|
+
|
|
37
|
+
```dotenv
|
|
38
|
+
BOT_TOKEN=123456:replace-with-a-real-token-locally
|
|
39
|
+
BOT_DEBUG=false
|
|
40
|
+
BOT_REQUEST_TIMEOUT=35
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Commit `.env.example`, not `.env`. Production deployments should inject settings through the process environment or a dedicated secret manager.
|
|
44
|
+
|
|
45
|
+
## Secret safety
|
|
46
|
+
|
|
47
|
+
Use `SecretStr` or `SecretBytes` for credentials. ScoloConfig also masks fields whose names include `token`, `secret`, `password`, `api_key`, `private_key`, `authorization`, or `cookie`.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
config.redacted_dump()
|
|
51
|
+
# {
|
|
52
|
+
# 'token': '***REDACTED***',
|
|
53
|
+
# 'debug': False,
|
|
54
|
+
# 'request_timeout': 35.0,
|
|
55
|
+
# }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Secrets are not included in `repr`, `redacted_json()`, `source_report()`, `log_startup()`, or safe validation errors. Reveal a secret only at the narrow point where a client requires it:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
token = config.token.get_secret_value()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Explicit environment names
|
|
65
|
+
|
|
66
|
+
Use `env()` when a field should map to an exact environment variable instead of a prefix-derived name.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from pydantic import SecretStr
|
|
70
|
+
from scoloconfig import ScoloSettings, env
|
|
71
|
+
|
|
72
|
+
class LegacyConfig(ScoloSettings):
|
|
73
|
+
telegram_token: SecretStr = env('TELEGRAM_BOT_TOKEN')
|
|
74
|
+
retries: int = env('RETRIES', default=3)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Safe startup report
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from scolologger import configure
|
|
81
|
+
|
|
82
|
+
configure(level='INFO', json_output=True)
|
|
83
|
+
report = config.log_startup()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The report includes only the settings class name, source categories, secret field names, and number of validated fields. It never includes configuration values.
|
|
87
|
+
|
|
88
|
+
## Limits
|
|
89
|
+
|
|
90
|
+
ScoloConfig 0.1.0 supports process environment and `.env` files. It does not fetch remote secrets, watch files, mutate a running configuration object, or replace a dedicated secrets manager.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ScoloConfig"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Safe typed configuration for Python bots and services."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pydantic-settings>=2.4,<3",
|
|
13
|
+
"ScoloLogger>=0.1,<0.2",
|
|
14
|
+
]
|
|
15
|
+
license = "MIT"
|
|
16
|
+
authors = [{ name = "G3tFun" }]
|
|
17
|
+
keywords = ["configuration", "settings", "dotenv", "secrets", "pydantic", "telegram-bot"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Software Development :: Libraries",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["build>=1.2", "pytest>=8.0", "ruff>=0.6"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/G3tFun/ScoloConfig"
|
|
36
|
+
Repository = "https://github.com/G3tFun/ScoloConfig"
|
|
37
|
+
Documentation = "https://github.com/G3tFun/ScoloConfig#readme"
|
|
38
|
+
Issues = "https://github.com/G3tFun/ScoloConfig/issues"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools]
|
|
41
|
+
package-dir = {"" = "src"}
|
|
42
|
+
include-package-data = true
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.package-data]
|
|
48
|
+
scoloconfig = ["py.typed"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 110
|
|
55
|
+
target-version = "py310"
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
59
|
+
ignore = ["E501", "UP037", "SIM108"]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ScoloConfig
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Safe typed configuration for Python bots and services.
|
|
5
|
+
Author: G3tFun
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/G3tFun/ScoloConfig
|
|
8
|
+
Project-URL: Repository, https://github.com/G3tFun/ScoloConfig
|
|
9
|
+
Project-URL: Documentation, https://github.com/G3tFun/ScoloConfig#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/G3tFun/ScoloConfig/issues
|
|
11
|
+
Keywords: configuration,settings,dotenv,secrets,pydantic,telegram-bot
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pydantic-settings<3,>=2.4
|
|
26
|
+
Requires-Dist: ScoloLogger<0.2,>=0.1
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# ScoloConfig
|
|
34
|
+
|
|
35
|
+
ScoloConfig provides safe typed configuration for Python bots and services. It uses `pydantic-settings` for validation, reads process environment and optional `.env` files, masks secrets by default, and emits safe startup events through ScoloLogger.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install ScoloConfig
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from pydantic import SecretStr
|
|
47
|
+
from scoloconfig import ScoloSettings, SettingsConfig
|
|
48
|
+
|
|
49
|
+
class AppConfig(ScoloSettings):
|
|
50
|
+
model_config = SettingsConfig(
|
|
51
|
+
env_file='.env',
|
|
52
|
+
env_prefix='BOT_',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
token: SecretStr
|
|
56
|
+
debug: bool = False
|
|
57
|
+
request_timeout: float = 35.0
|
|
58
|
+
|
|
59
|
+
config = AppConfig.load()
|
|
60
|
+
config.log_startup()
|
|
61
|
+
|
|
62
|
+
bot_token = config.token.get_secret_value()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
With this model, `BOT_TOKEN`, `BOT_DEBUG`, and `BOT_REQUEST_TIMEOUT` are read from the process environment. Values in the process environment always override `.env`.
|
|
66
|
+
|
|
67
|
+
## `.env`
|
|
68
|
+
|
|
69
|
+
```dotenv
|
|
70
|
+
BOT_TOKEN=123456:replace-with-a-real-token-locally
|
|
71
|
+
BOT_DEBUG=false
|
|
72
|
+
BOT_REQUEST_TIMEOUT=35
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Commit `.env.example`, not `.env`. Production deployments should inject settings through the process environment or a dedicated secret manager.
|
|
76
|
+
|
|
77
|
+
## Secret safety
|
|
78
|
+
|
|
79
|
+
Use `SecretStr` or `SecretBytes` for credentials. ScoloConfig also masks fields whose names include `token`, `secret`, `password`, `api_key`, `private_key`, `authorization`, or `cookie`.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
config.redacted_dump()
|
|
83
|
+
# {
|
|
84
|
+
# 'token': '***REDACTED***',
|
|
85
|
+
# 'debug': False,
|
|
86
|
+
# 'request_timeout': 35.0,
|
|
87
|
+
# }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Secrets are not included in `repr`, `redacted_json()`, `source_report()`, `log_startup()`, or safe validation errors. Reveal a secret only at the narrow point where a client requires it:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
token = config.token.get_secret_value()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Explicit environment names
|
|
97
|
+
|
|
98
|
+
Use `env()` when a field should map to an exact environment variable instead of a prefix-derived name.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from pydantic import SecretStr
|
|
102
|
+
from scoloconfig import ScoloSettings, env
|
|
103
|
+
|
|
104
|
+
class LegacyConfig(ScoloSettings):
|
|
105
|
+
telegram_token: SecretStr = env('TELEGRAM_BOT_TOKEN')
|
|
106
|
+
retries: int = env('RETRIES', default=3)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Safe startup report
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from scolologger import configure
|
|
113
|
+
|
|
114
|
+
configure(level='INFO', json_output=True)
|
|
115
|
+
report = config.log_startup()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The report includes only the settings class name, source categories, secret field names, and number of validated fields. It never includes configuration values.
|
|
119
|
+
|
|
120
|
+
## Limits
|
|
121
|
+
|
|
122
|
+
ScoloConfig 0.1.0 supports process environment and `.env` files. It does not fetch remote secrets, watch files, mutate a running configuration object, or replace a dedicated secrets manager.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/ScoloConfig.egg-info/PKG-INFO
|
|
5
|
+
src/ScoloConfig.egg-info/SOURCES.txt
|
|
6
|
+
src/ScoloConfig.egg-info/dependency_links.txt
|
|
7
|
+
src/ScoloConfig.egg-info/requires.txt
|
|
8
|
+
src/ScoloConfig.egg-info/top_level.txt
|
|
9
|
+
src/scoloconfig/__init__.py
|
|
10
|
+
src/scoloconfig/core.py
|
|
11
|
+
src/scoloconfig/py.typed
|
|
12
|
+
tests/test_scoloconfig.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
scoloconfig
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""ScoloConfig: safe typed configuration for Python bots and services."""
|
|
2
|
+
|
|
3
|
+
from .core import (
|
|
4
|
+
REDACTED,
|
|
5
|
+
ConfigIssue,
|
|
6
|
+
ConfigurationError,
|
|
7
|
+
ScoloSettings,
|
|
8
|
+
SettingsConfig,
|
|
9
|
+
SourceReport,
|
|
10
|
+
env,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__version__ = '0.1.0'
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
'REDACTED',
|
|
17
|
+
'ConfigIssue',
|
|
18
|
+
'ConfigurationError',
|
|
19
|
+
'ScoloSettings',
|
|
20
|
+
'SettingsConfig',
|
|
21
|
+
'SourceReport',
|
|
22
|
+
'env',
|
|
23
|
+
]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Typed, safe configuration primitives built on pydantic-settings."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from dataclasses import asdict, dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field, SecretBytes, SecretStr, ValidationError
|
|
12
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
|
+
from scolologger import get_logger
|
|
14
|
+
|
|
15
|
+
_LOG = get_logger('scoloconfig')
|
|
16
|
+
REDACTED = '***REDACTED***'
|
|
17
|
+
_SENSITIVE_PARTS = ('token', 'secret', 'password', 'api_key', 'private_key', 'authorization', 'cookie')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class ConfigIssue:
|
|
22
|
+
"""A validation issue without the rejected input value."""
|
|
23
|
+
|
|
24
|
+
location: str
|
|
25
|
+
message: str
|
|
26
|
+
error_type: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConfigurationError(ValueError):
|
|
30
|
+
"""Safe validation error that never includes a raw configuration value."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, settings_name: str, issues: Sequence[ConfigIssue]) -> None:
|
|
33
|
+
self.settings_name = settings_name
|
|
34
|
+
self.issues = tuple(issues)
|
|
35
|
+
details = '; '.join(f'{issue.location}: {issue.message}' for issue in self.issues)
|
|
36
|
+
super().__init__(f'Invalid configuration for {settings_name}: {details}')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class SourceReport:
|
|
41
|
+
"""Safe summary of available configuration sources and field classification."""
|
|
42
|
+
|
|
43
|
+
settings: str
|
|
44
|
+
sources: tuple[str, ...]
|
|
45
|
+
secret_fields: tuple[str, ...]
|
|
46
|
+
validated_fields: int
|
|
47
|
+
|
|
48
|
+
def as_dict(self) -> dict[str, Any]:
|
|
49
|
+
return asdict(self)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def SettingsConfig(
|
|
53
|
+
*,
|
|
54
|
+
env_file: str | Path | Sequence[str | Path] | None = None,
|
|
55
|
+
env_prefix: str = '',
|
|
56
|
+
case_sensitive: bool = False,
|
|
57
|
+
env_nested_delimiter: str | None = '__',
|
|
58
|
+
extra: str = 'ignore',
|
|
59
|
+
) -> SettingsConfigDict:
|
|
60
|
+
"""Return a concise settings configuration accepted by ``ScoloSettings``.
|
|
61
|
+
|
|
62
|
+
Process environment always takes priority over the optional ``.env`` file.
|
|
63
|
+
The default nested delimiter maps ``DATABASE__HOST`` to ``database.host``.
|
|
64
|
+
"""
|
|
65
|
+
return SettingsConfigDict(
|
|
66
|
+
env_file=env_file,
|
|
67
|
+
env_prefix=env_prefix,
|
|
68
|
+
case_sensitive=case_sensitive,
|
|
69
|
+
env_nested_delimiter=env_nested_delimiter,
|
|
70
|
+
extra=extra,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def env(name: str, default: Any = ...) -> Any:
|
|
75
|
+
"""Bind one field to an explicit environment variable name."""
|
|
76
|
+
if not isinstance(name, str) or not name:
|
|
77
|
+
raise ValueError('environment variable name must be a non-empty string')
|
|
78
|
+
if default is ...:
|
|
79
|
+
return Field(validation_alias=name)
|
|
80
|
+
return Field(default=default, validation_alias=name)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_sensitive_name(name: str) -> bool:
|
|
84
|
+
normalized = name.casefold().replace('-', '_')
|
|
85
|
+
return any(part in normalized for part in _SENSITIVE_PARTS)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _is_secret(value: Any) -> bool:
|
|
89
|
+
return isinstance(value, (SecretStr, SecretBytes))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _redact(value: Any, *, name: str = '') -> Any:
|
|
93
|
+
if _is_secret(value):
|
|
94
|
+
return REDACTED
|
|
95
|
+
if _is_sensitive_name(name):
|
|
96
|
+
return REDACTED
|
|
97
|
+
if isinstance(value, BaseModel):
|
|
98
|
+
return _redact(value.model_dump(mode='python'), name=name)
|
|
99
|
+
if isinstance(value, Mapping):
|
|
100
|
+
return {str(key): _redact(item, name=str(key)) for key, item in value.items()}
|
|
101
|
+
if isinstance(value, list):
|
|
102
|
+
return [_redact(item, name=name) for item in value]
|
|
103
|
+
if isinstance(value, tuple):
|
|
104
|
+
return tuple(_redact(item, name=name) for item in value)
|
|
105
|
+
if isinstance(value, set):
|
|
106
|
+
return {_redact(item, name=name) for item in value}
|
|
107
|
+
if isinstance(value, bytes):
|
|
108
|
+
return REDACTED if _is_sensitive_name(name) else f'<bytes:{len(value)}>'
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ScoloSettings(BaseSettings):
|
|
113
|
+
"""Base settings class with safe rendering, reports, and startup logging.
|
|
114
|
+
|
|
115
|
+
Subclasses declare ordinary Pydantic fields and set ``model_config`` with
|
|
116
|
+
:func:`SettingsConfig`. Call ``MySettings.load()`` at process startup to obtain
|
|
117
|
+
a safe validation error instead of an error that can display rejected secrets.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
model_config = SettingsConfigDict(extra='ignore')
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def load(cls, **overrides: Any) -> ScoloSettings:
|
|
124
|
+
"""Load and validate settings while removing raw values from validation errors."""
|
|
125
|
+
try:
|
|
126
|
+
settings = cls(**overrides)
|
|
127
|
+
except ValidationError as error:
|
|
128
|
+
issues = tuple(
|
|
129
|
+
ConfigIssue(
|
|
130
|
+
location='.'.join(str(part) for part in item['loc']),
|
|
131
|
+
message=str(item['msg']),
|
|
132
|
+
error_type=str(item['type']),
|
|
133
|
+
)
|
|
134
|
+
for item in error.errors(include_input=False)
|
|
135
|
+
)
|
|
136
|
+
_LOG.error('config.validation_failed', settings=cls.__name__, issue_count=len(issues))
|
|
137
|
+
raise ConfigurationError(cls.__name__, issues) from None
|
|
138
|
+
return settings
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def _field_environment_names(cls, field_name: str) -> tuple[str, ...]:
|
|
142
|
+
field = cls.model_fields[field_name]
|
|
143
|
+
prefix = str(cls.model_config.get('env_prefix') or '')
|
|
144
|
+
case_sensitive = bool(cls.model_config.get('case_sensitive'))
|
|
145
|
+
alias = field.validation_alias or field.alias or field_name
|
|
146
|
+
names = (str(alias),) if field.validation_alias is not None else (f'{prefix}{alias}',)
|
|
147
|
+
return names if case_sensitive else tuple(name.upper() for name in names)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def _configured_env_files(cls) -> tuple[Path, ...]:
|
|
151
|
+
raw = cls.model_config.get('env_file')
|
|
152
|
+
if raw is None:
|
|
153
|
+
return ()
|
|
154
|
+
values = raw if isinstance(raw, Sequence) and not isinstance(raw, (str, bytes, Path)) else (raw,)
|
|
155
|
+
return tuple(Path(value) for value in values)
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def _field_is_sensitive(cls, field_name: str, value: Any) -> bool:
|
|
159
|
+
return _is_sensitive_name(field_name) or _is_secret(value)
|
|
160
|
+
|
|
161
|
+
def redacted_dump(self, *, by_alias: bool = False, exclude_none: bool = False) -> dict[str, Any]:
|
|
162
|
+
"""Return a JSON-compatible configuration mapping with sensitive values hidden."""
|
|
163
|
+
raw = self.model_dump(mode='python', by_alias=by_alias, exclude_none=exclude_none)
|
|
164
|
+
return {name: _redact(value, name=name) for name, value in raw.items()}
|
|
165
|
+
|
|
166
|
+
def redacted_json(self, *, by_alias: bool = False, exclude_none: bool = False) -> str:
|
|
167
|
+
"""Serialise :meth:`redacted_dump` without exposing secret values."""
|
|
168
|
+
return json.dumps(
|
|
169
|
+
self.redacted_dump(by_alias=by_alias, exclude_none=exclude_none),
|
|
170
|
+
ensure_ascii=False,
|
|
171
|
+
separators=(',', ':'),
|
|
172
|
+
default=str,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def source_report(self) -> SourceReport:
|
|
176
|
+
"""Describe available sources and sensitive field names without reading values."""
|
|
177
|
+
sources: list[str] = []
|
|
178
|
+
environment_names = {
|
|
179
|
+
name
|
|
180
|
+
for field_name in type(self).model_fields
|
|
181
|
+
for name in self._field_environment_names(field_name)
|
|
182
|
+
}
|
|
183
|
+
environment = os.environ if bool(self.model_config.get('case_sensitive')) else {
|
|
184
|
+
key.upper(): value for key, value in os.environ.items()
|
|
185
|
+
}
|
|
186
|
+
if any(name in environment for name in environment_names):
|
|
187
|
+
sources.append('environment')
|
|
188
|
+
if any(path.is_file() for path in self._configured_env_files()):
|
|
189
|
+
sources.append('dotenv')
|
|
190
|
+
if not sources:
|
|
191
|
+
sources.append('defaults-or-overrides')
|
|
192
|
+
secret_fields = tuple(
|
|
193
|
+
name for name in type(self).model_fields if self._field_is_sensitive(name, getattr(self, name))
|
|
194
|
+
)
|
|
195
|
+
return SourceReport(
|
|
196
|
+
settings=self.__class__.__name__,
|
|
197
|
+
sources=tuple(sources),
|
|
198
|
+
secret_fields=secret_fields,
|
|
199
|
+
validated_fields=len(type(self).model_fields),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def log_startup(self) -> SourceReport:
|
|
203
|
+
"""Emit one safe structured startup event and return its report."""
|
|
204
|
+
report = self.source_report()
|
|
205
|
+
_LOG.info('config.loaded', **report.as_dict())
|
|
206
|
+
return report
|
|
207
|
+
|
|
208
|
+
def __repr_args__(self) -> Any:
|
|
209
|
+
return list(self.redacted_dump().items())
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from pydantic import SecretStr
|
|
7
|
+
|
|
8
|
+
from scoloconfig import ConfigurationError, ScoloSettings, SettingsConfig, env
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_environment_overrides_dotenv_and_values_are_typed(monkeypatch, tmp_path):
|
|
12
|
+
dotenv = tmp_path / '.env'
|
|
13
|
+
dotenv.write_text('BOT_TOKEN=dotenv-token\nBOT_DEBUG=false\nBOT_TIMEOUT=12.5\n', encoding='utf-8')
|
|
14
|
+
monkeypatch.setenv('BOT_TOKEN', 'environment-token')
|
|
15
|
+
monkeypatch.setenv('BOT_DEBUG', 'true')
|
|
16
|
+
|
|
17
|
+
class AppSettings(ScoloSettings):
|
|
18
|
+
model_config = SettingsConfig(env_file=dotenv, env_prefix='BOT_')
|
|
19
|
+
|
|
20
|
+
token: SecretStr
|
|
21
|
+
debug: bool = False
|
|
22
|
+
timeout: float = 5.0
|
|
23
|
+
|
|
24
|
+
settings = AppSettings.load()
|
|
25
|
+
|
|
26
|
+
assert settings.token.get_secret_value() == 'environment-token'
|
|
27
|
+
assert settings.debug is True
|
|
28
|
+
assert settings.timeout == 12.5
|
|
29
|
+
assert settings.source_report().sources == ('environment', 'dotenv')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_secrets_are_never_present_in_repr_or_redacted_serialization(monkeypatch):
|
|
33
|
+
monkeypatch.setenv('APP_TOKEN', '123456:real-token')
|
|
34
|
+
monkeypatch.setenv('APP_API_KEY', 'real-api-key')
|
|
35
|
+
|
|
36
|
+
class AppSettings(ScoloSettings):
|
|
37
|
+
model_config = SettingsConfig(env_prefix='APP_')
|
|
38
|
+
|
|
39
|
+
token: SecretStr
|
|
40
|
+
api_key: str
|
|
41
|
+
plain: str = 'safe'
|
|
42
|
+
|
|
43
|
+
settings = AppSettings.load()
|
|
44
|
+
rendered = repr(settings)
|
|
45
|
+
redacted = settings.redacted_dump()
|
|
46
|
+
|
|
47
|
+
assert '123456:real-token' not in rendered
|
|
48
|
+
assert 'real-api-key' not in rendered
|
|
49
|
+
assert '123456:real-token' not in settings.redacted_json()
|
|
50
|
+
assert 'real-api-key' not in settings.redacted_json()
|
|
51
|
+
assert redacted == {'token': '***REDACTED***', 'api_key': '***REDACTED***', 'plain': 'safe'}
|
|
52
|
+
assert settings.source_report().secret_fields == ('token', 'api_key')
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_explicit_environment_alias_and_defaults(monkeypatch):
|
|
56
|
+
monkeypatch.setenv('TELEGRAM_BOT_TOKEN', 'token-from-explicit-name')
|
|
57
|
+
|
|
58
|
+
class AppSettings(ScoloSettings):
|
|
59
|
+
token: SecretStr = env('TELEGRAM_BOT_TOKEN')
|
|
60
|
+
retries: int = env('RETRIES', default=3)
|
|
61
|
+
|
|
62
|
+
settings = AppSettings.load()
|
|
63
|
+
|
|
64
|
+
assert settings.token.get_secret_value() == 'token-from-explicit-name'
|
|
65
|
+
assert settings.retries == 3
|
|
66
|
+
assert settings.source_report().sources == ('environment',)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_safe_validation_error_excludes_rejected_secret(monkeypatch):
|
|
70
|
+
raw_secret = 'not-an-integer-and-must-not-leak'
|
|
71
|
+
monkeypatch.setenv('APP_PORT', raw_secret)
|
|
72
|
+
|
|
73
|
+
class AppSettings(ScoloSettings):
|
|
74
|
+
model_config = SettingsConfig(env_prefix='APP_')
|
|
75
|
+
|
|
76
|
+
port: int
|
|
77
|
+
|
|
78
|
+
with pytest.raises(ConfigurationError) as captured:
|
|
79
|
+
AppSettings.load()
|
|
80
|
+
|
|
81
|
+
error = captured.value
|
|
82
|
+
assert raw_secret not in str(error)
|
|
83
|
+
assert error.settings_name == 'AppSettings'
|
|
84
|
+
assert error.issues[0].location == 'port'
|
|
85
|
+
assert error.issues[0].error_type == 'int_parsing'
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_missing_required_setting_is_safe_and_env_is_not_mutated(monkeypatch):
|
|
89
|
+
monkeypatch.delenv('REQUIRED_TOKEN', raising=False)
|
|
90
|
+
before = dict(os.environ)
|
|
91
|
+
|
|
92
|
+
class AppSettings(ScoloSettings):
|
|
93
|
+
token: SecretStr = env('REQUIRED_TOKEN')
|
|
94
|
+
|
|
95
|
+
with pytest.raises(ConfigurationError) as captured:
|
|
96
|
+
AppSettings.load()
|
|
97
|
+
|
|
98
|
+
assert 'REQUIRED_TOKEN' in str(captured.value)
|
|
99
|
+
assert dict(os.environ) == before
|