pydantic-settings-manager 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.
- pydantic_settings_manager-0.1.0/LICENSE +21 -0
- pydantic_settings_manager-0.1.0/PKG-INFO +118 -0
- pydantic_settings_manager-0.1.0/README.md +90 -0
- pydantic_settings_manager-0.1.0/pydantic_settings_manager/__init__.py +37 -0
- pydantic_settings_manager-0.1.0/pydantic_settings_manager/base.py +52 -0
- pydantic_settings_manager-0.1.0/pydantic_settings_manager/mapped.py +282 -0
- pydantic_settings_manager-0.1.0/pydantic_settings_manager/single.py +96 -0
- pydantic_settings_manager-0.1.0/pydantic_settings_manager/types.py +6 -0
- pydantic_settings_manager-0.1.0/pydantic_settings_manager/utils.py +50 -0
- pydantic_settings_manager-0.1.0/pyproject.toml +71 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Aki
|
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,118 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: pydantic-settings-manager
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A library for managing Pydantic settings objects
|
5
|
+
Home-page: https://github.com/kiarina/pydantic-settings-manager
|
6
|
+
License: MIT
|
7
|
+
Keywords: pydantic,settings,configuration
|
8
|
+
Author: kiarina
|
9
|
+
Author-email: kiarinadawa@gmail.com
|
10
|
+
Requires-Python: >=3.8,<4.0
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
12
|
+
Classifier: Intended Audience :: Developers
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
14
|
+
Classifier: Operating System :: OS Independent
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
22
|
+
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
23
|
+
Requires-Dist: pydantic-settings (>=2.0.0,<3.0.0)
|
24
|
+
Project-URL: Documentation, https://github.com/kiarina/pydantic-settings-manager
|
25
|
+
Project-URL: Repository, https://github.com/kiarina/pydantic-settings-manager
|
26
|
+
Description-Content-Type: text/markdown
|
27
|
+
|
28
|
+
# pydantic-settings-manager
|
29
|
+
|
30
|
+
A library for managing Pydantic settings objects.
|
31
|
+
|
32
|
+
## Features
|
33
|
+
|
34
|
+
- Two types of settings managers:
|
35
|
+
- `SingleSettingsManager`: For managing a single settings object
|
36
|
+
- `MappedSettingsManager`: For managing multiple settings objects mapped to keys
|
37
|
+
- Support for loading settings from multiple sources
|
38
|
+
- Command line argument overrides
|
39
|
+
- Settings validation through Pydantic
|
40
|
+
- Type hints and documentation
|
41
|
+
|
42
|
+
## Installation
|
43
|
+
|
44
|
+
```bash
|
45
|
+
pip install pydantic-settings-manager
|
46
|
+
```
|
47
|
+
|
48
|
+
## Quick Start
|
49
|
+
|
50
|
+
### Single Settings Manager
|
51
|
+
|
52
|
+
```python
|
53
|
+
from pydantic_settings import BaseSettings
|
54
|
+
from pydantic_settings_manager import SingleSettingsManager
|
55
|
+
|
56
|
+
class MySettings(BaseSettings):
|
57
|
+
name: str = "default"
|
58
|
+
value: int = 0
|
59
|
+
|
60
|
+
# Create a settings manager
|
61
|
+
manager = SingleSettingsManager(MySettings)
|
62
|
+
|
63
|
+
# Update settings from a configuration file
|
64
|
+
manager.user_config = {"name": "from_file", "value": 42}
|
65
|
+
|
66
|
+
# Update settings from command line arguments
|
67
|
+
manager.cli_args = {"value": 100}
|
68
|
+
|
69
|
+
# Get the current settings (combines both sources)
|
70
|
+
settings = manager.settings
|
71
|
+
assert settings.name == "from_file" # from user_config
|
72
|
+
assert settings.value == 100 # from cli_args (overrides user_config)
|
73
|
+
```
|
74
|
+
|
75
|
+
### Mapped Settings Manager
|
76
|
+
|
77
|
+
```python
|
78
|
+
from pydantic_settings import BaseSettings
|
79
|
+
from pydantic_settings_manager import MappedSettingsManager
|
80
|
+
|
81
|
+
class MySettings(BaseSettings):
|
82
|
+
name: str = "default"
|
83
|
+
value: int = 0
|
84
|
+
|
85
|
+
# Create a settings manager
|
86
|
+
manager = MappedSettingsManager(MySettings)
|
87
|
+
|
88
|
+
# Set up multiple configurations
|
89
|
+
manager.user_config = {
|
90
|
+
"map": {
|
91
|
+
"dev": {"name": "development", "value": 42},
|
92
|
+
"prod": {"name": "production", "value": 100}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
# Select which configuration to use
|
97
|
+
manager.set_cli_args("dev")
|
98
|
+
|
99
|
+
# Get the current settings
|
100
|
+
settings = manager.settings
|
101
|
+
assert settings.name == "development"
|
102
|
+
assert settings.value == 42
|
103
|
+
|
104
|
+
# Switch to a different configuration
|
105
|
+
manager.set_cli_args("prod")
|
106
|
+
settings = manager.settings
|
107
|
+
assert settings.name == "production"
|
108
|
+
assert settings.value == 100
|
109
|
+
```
|
110
|
+
|
111
|
+
## Documentation
|
112
|
+
|
113
|
+
For more detailed documentation, please see the [GitHub repository](https://github.com/kiarina/pydantic-settings-manager).
|
114
|
+
|
115
|
+
## License
|
116
|
+
|
117
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
118
|
+
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# pydantic-settings-manager
|
2
|
+
|
3
|
+
A library for managing Pydantic settings objects.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- Two types of settings managers:
|
8
|
+
- `SingleSettingsManager`: For managing a single settings object
|
9
|
+
- `MappedSettingsManager`: For managing multiple settings objects mapped to keys
|
10
|
+
- Support for loading settings from multiple sources
|
11
|
+
- Command line argument overrides
|
12
|
+
- Settings validation through Pydantic
|
13
|
+
- Type hints and documentation
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
```bash
|
18
|
+
pip install pydantic-settings-manager
|
19
|
+
```
|
20
|
+
|
21
|
+
## Quick Start
|
22
|
+
|
23
|
+
### Single Settings Manager
|
24
|
+
|
25
|
+
```python
|
26
|
+
from pydantic_settings import BaseSettings
|
27
|
+
from pydantic_settings_manager import SingleSettingsManager
|
28
|
+
|
29
|
+
class MySettings(BaseSettings):
|
30
|
+
name: str = "default"
|
31
|
+
value: int = 0
|
32
|
+
|
33
|
+
# Create a settings manager
|
34
|
+
manager = SingleSettingsManager(MySettings)
|
35
|
+
|
36
|
+
# Update settings from a configuration file
|
37
|
+
manager.user_config = {"name": "from_file", "value": 42}
|
38
|
+
|
39
|
+
# Update settings from command line arguments
|
40
|
+
manager.cli_args = {"value": 100}
|
41
|
+
|
42
|
+
# Get the current settings (combines both sources)
|
43
|
+
settings = manager.settings
|
44
|
+
assert settings.name == "from_file" # from user_config
|
45
|
+
assert settings.value == 100 # from cli_args (overrides user_config)
|
46
|
+
```
|
47
|
+
|
48
|
+
### Mapped Settings Manager
|
49
|
+
|
50
|
+
```python
|
51
|
+
from pydantic_settings import BaseSettings
|
52
|
+
from pydantic_settings_manager import MappedSettingsManager
|
53
|
+
|
54
|
+
class MySettings(BaseSettings):
|
55
|
+
name: str = "default"
|
56
|
+
value: int = 0
|
57
|
+
|
58
|
+
# Create a settings manager
|
59
|
+
manager = MappedSettingsManager(MySettings)
|
60
|
+
|
61
|
+
# Set up multiple configurations
|
62
|
+
manager.user_config = {
|
63
|
+
"map": {
|
64
|
+
"dev": {"name": "development", "value": 42},
|
65
|
+
"prod": {"name": "production", "value": 100}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
# Select which configuration to use
|
70
|
+
manager.set_cli_args("dev")
|
71
|
+
|
72
|
+
# Get the current settings
|
73
|
+
settings = manager.settings
|
74
|
+
assert settings.name == "development"
|
75
|
+
assert settings.value == 42
|
76
|
+
|
77
|
+
# Switch to a different configuration
|
78
|
+
manager.set_cli_args("prod")
|
79
|
+
settings = manager.settings
|
80
|
+
assert settings.name == "production"
|
81
|
+
assert settings.value == 100
|
82
|
+
```
|
83
|
+
|
84
|
+
## Documentation
|
85
|
+
|
86
|
+
For more detailed documentation, please see the [GitHub repository](https://github.com/kiarina/pydantic-settings-manager).
|
87
|
+
|
88
|
+
## License
|
89
|
+
|
90
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
"""
|
2
|
+
pydantic-settings-manager
|
3
|
+
========================
|
4
|
+
|
5
|
+
A library for managing Pydantic settings objects.
|
6
|
+
|
7
|
+
This library provides two types of settings managers:
|
8
|
+
|
9
|
+
1. SingleSettingsManager: For managing a single settings object
|
10
|
+
2. MappedSettingsManager: For managing multiple settings objects mapped to keys
|
11
|
+
|
12
|
+
Both managers support:
|
13
|
+
- Loading settings from multiple sources
|
14
|
+
- Command line argument overrides
|
15
|
+
- Settings validation through Pydantic
|
16
|
+
"""
|
17
|
+
|
18
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
19
|
+
|
20
|
+
from .base import BaseSettingsManager
|
21
|
+
from .mapped import MappedSettingsManager, SettingsMap
|
22
|
+
from .single import SingleSettingsManager
|
23
|
+
|
24
|
+
__version__ = "0.1.0"
|
25
|
+
|
26
|
+
__all__ = [
|
27
|
+
# Re-exports from pydantic_settings
|
28
|
+
"BaseSettings",
|
29
|
+
"SettingsConfigDict",
|
30
|
+
# Base manager
|
31
|
+
"BaseSettingsManager",
|
32
|
+
# Single settings manager
|
33
|
+
"SingleSettingsManager",
|
34
|
+
# Mapped settings manager
|
35
|
+
"MappedSettingsManager",
|
36
|
+
"SettingsMap",
|
37
|
+
]
|
@@ -0,0 +1,52 @@
|
|
1
|
+
"""
|
2
|
+
Base classes for settings managers.
|
3
|
+
"""
|
4
|
+
from abc import ABC, abstractmethod
|
5
|
+
from typing import Any, Generic, Type, TypeVar
|
6
|
+
|
7
|
+
from pydantic_settings import BaseSettings
|
8
|
+
|
9
|
+
T = TypeVar("T", bound=BaseSettings)
|
10
|
+
|
11
|
+
|
12
|
+
class BaseSettingsManager(ABC, Generic[T]):
|
13
|
+
"""
|
14
|
+
Base class for settings managers.
|
15
|
+
|
16
|
+
This abstract class defines the interface that all settings managers must implement.
|
17
|
+
It provides a generic way to manage Pydantic settings objects.
|
18
|
+
|
19
|
+
Type Parameters:
|
20
|
+
T: A type that inherits from BaseSettings
|
21
|
+
"""
|
22
|
+
|
23
|
+
@abstractmethod
|
24
|
+
def __init__(self, settings_cls: Type[T]):
|
25
|
+
"""
|
26
|
+
Initialize the settings manager.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
settings_cls: The settings class to manage
|
30
|
+
"""
|
31
|
+
self.settings_cls = settings_cls
|
32
|
+
"""The settings class being managed"""
|
33
|
+
|
34
|
+
self.user_config: dict[str, Any] = {}
|
35
|
+
"""User configuration dictionary"""
|
36
|
+
|
37
|
+
@property
|
38
|
+
@abstractmethod
|
39
|
+
def settings(self) -> T:
|
40
|
+
"""
|
41
|
+
Get the current settings.
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
The current settings object
|
45
|
+
"""
|
46
|
+
|
47
|
+
@abstractmethod
|
48
|
+
def clear(self):
|
49
|
+
"""
|
50
|
+
Clear the current settings.
|
51
|
+
This typically involves clearing any cached settings.
|
52
|
+
"""
|
@@ -0,0 +1,282 @@
|
|
1
|
+
"""
|
2
|
+
Mapped settings manager implementation.
|
3
|
+
"""
|
4
|
+
from __future__ import annotations
|
5
|
+
|
6
|
+
from typing import Any, Dict, Generic, Type
|
7
|
+
|
8
|
+
from pydantic import BaseModel, Field
|
9
|
+
from pydantic.main import create_model
|
10
|
+
|
11
|
+
from .base import BaseSettingsManager, T
|
12
|
+
from .utils import diff_dict, update_dict
|
13
|
+
|
14
|
+
|
15
|
+
class SettingsMap(BaseModel, Generic[T]):
|
16
|
+
"""
|
17
|
+
A model that maps keys to settings objects.
|
18
|
+
|
19
|
+
This model is used by MappedSettingsManager to store multiple settings objects
|
20
|
+
and track which one is currently active.
|
21
|
+
|
22
|
+
Type Parameters:
|
23
|
+
T: A type that inherits from BaseSettings
|
24
|
+
|
25
|
+
Attributes:
|
26
|
+
key: The key of the currently active settings
|
27
|
+
map: A dictionary mapping keys to settings objects
|
28
|
+
"""
|
29
|
+
|
30
|
+
key: str = ""
|
31
|
+
"""The key of the currently active settings"""
|
32
|
+
|
33
|
+
map: Dict[str, T] = Field(default_factory=dict)
|
34
|
+
"""A dictionary mapping keys to settings objects"""
|
35
|
+
|
36
|
+
|
37
|
+
class MappedSettingsManager(BaseSettingsManager[T]):
|
38
|
+
"""
|
39
|
+
A settings manager that manages multiple settings objects mapped to keys.
|
40
|
+
|
41
|
+
This manager is useful when you need to switch between different configurations
|
42
|
+
at runtime. Each configuration is identified by a unique key.
|
43
|
+
|
44
|
+
Type Parameters:
|
45
|
+
T: A type that inherits from BaseSettings
|
46
|
+
|
47
|
+
Example:
|
48
|
+
```python
|
49
|
+
from pydantic_settings import BaseSettings
|
50
|
+
from pydantic_settings_manager import MappedSettingsManager
|
51
|
+
|
52
|
+
class MySettings(BaseSettings):
|
53
|
+
name: str = "default"
|
54
|
+
value: int = 0
|
55
|
+
|
56
|
+
# Create a settings manager
|
57
|
+
manager = MappedSettingsManager(MySettings)
|
58
|
+
|
59
|
+
# Set up multiple configurations
|
60
|
+
manager.user_config = {
|
61
|
+
"map": {
|
62
|
+
"dev": {"name": "development", "value": 42},
|
63
|
+
"prod": {"name": "production", "value": 100}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
# Select which configuration to use
|
68
|
+
manager.set_cli_args("dev")
|
69
|
+
|
70
|
+
# Get the current settings
|
71
|
+
settings = manager.settings
|
72
|
+
assert settings.name == "development"
|
73
|
+
assert settings.value == 42
|
74
|
+
|
75
|
+
# Switch to a different configuration
|
76
|
+
manager.set_cli_args("prod")
|
77
|
+
settings = manager.settings
|
78
|
+
assert settings.name == "production"
|
79
|
+
assert settings.value == 100
|
80
|
+
```
|
81
|
+
"""
|
82
|
+
|
83
|
+
def __init__(self, settings_cls: Type[T]):
|
84
|
+
"""
|
85
|
+
Initialize the settings manager.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
settings_cls: The settings class to manage
|
89
|
+
"""
|
90
|
+
self.settings_cls: Type[T] = settings_cls
|
91
|
+
"""The settings class being managed"""
|
92
|
+
|
93
|
+
self.cli_args: SettingsMap[T] = SettingsMap[T]()
|
94
|
+
"""Command line arguments"""
|
95
|
+
|
96
|
+
self.user_config: Dict[str, Any] = {}
|
97
|
+
"""User configuration"""
|
98
|
+
|
99
|
+
self.system_settings: T = settings_cls()
|
100
|
+
"""System settings"""
|
101
|
+
|
102
|
+
self._map_settings: SettingsMap[T] | None = None
|
103
|
+
"""Cached settings map"""
|
104
|
+
|
105
|
+
@property
|
106
|
+
def settings_cls_name(self) -> str:
|
107
|
+
"""
|
108
|
+
Get the name of the settings class.
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
The name of the settings class
|
112
|
+
"""
|
113
|
+
return self.settings_cls.__name__
|
114
|
+
|
115
|
+
def set_cli_args(self, key: str, value: T | None = None):
|
116
|
+
"""
|
117
|
+
Set the command line arguments.
|
118
|
+
|
119
|
+
Args:
|
120
|
+
key: The key to make active
|
121
|
+
value: Optional settings to associate with the key
|
122
|
+
"""
|
123
|
+
self.cli_args.key = key
|
124
|
+
|
125
|
+
if value:
|
126
|
+
self.cli_args.map[key] = value
|
127
|
+
|
128
|
+
self.clear()
|
129
|
+
|
130
|
+
@property
|
131
|
+
def _cli_args_config(self) -> Dict[str, Any]:
|
132
|
+
"""
|
133
|
+
Get the command line arguments as a dictionary.
|
134
|
+
|
135
|
+
Returns:
|
136
|
+
The command line arguments
|
137
|
+
"""
|
138
|
+
vanilla = SettingsMap[T]()
|
139
|
+
|
140
|
+
for k, _ in self.cli_args.map.items():
|
141
|
+
vanilla.map[k] = self.settings_cls()
|
142
|
+
|
143
|
+
base = vanilla.model_dump()
|
144
|
+
target = self.cli_args.model_dump()
|
145
|
+
|
146
|
+
return diff_dict(base, target)
|
147
|
+
|
148
|
+
@property
|
149
|
+
def _system_settings_config(self) -> Dict[str, Any]:
|
150
|
+
"""
|
151
|
+
Get the system settings as a dictionary.
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
The system settings
|
155
|
+
"""
|
156
|
+
base = self.settings_cls().model_dump()
|
157
|
+
target = self.system_settings.model_dump()
|
158
|
+
return diff_dict(base, target)
|
159
|
+
|
160
|
+
@property
|
161
|
+
def map_settings(self) -> SettingsMap[T]:
|
162
|
+
"""
|
163
|
+
Get the settings map.
|
164
|
+
|
165
|
+
Returns:
|
166
|
+
The settings map
|
167
|
+
"""
|
168
|
+
if not self._map_settings:
|
169
|
+
self._map_settings = self._get_map_settings()
|
170
|
+
|
171
|
+
return self._map_settings
|
172
|
+
|
173
|
+
def _get_map_settings(self) -> SettingsMap[T]:
|
174
|
+
"""
|
175
|
+
Create a new settings map.
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
A new settings map
|
179
|
+
"""
|
180
|
+
DynamicMapSettings = create_model(
|
181
|
+
"DynamicMapSettings",
|
182
|
+
key=(str, ""),
|
183
|
+
map=(Dict[str, self.settings_cls], {}), # type: ignore[name-defined]
|
184
|
+
__base__=SettingsMap[T],
|
185
|
+
)
|
186
|
+
|
187
|
+
# Merge user configuration and command line arguments
|
188
|
+
config = update_dict(self.user_config, self._cli_args_config)
|
189
|
+
|
190
|
+
# Merge system settings into each configuration
|
191
|
+
if "map" in config:
|
192
|
+
for key, value in config["map"].items():
|
193
|
+
config["map"][key] = update_dict(value, self._system_settings_config)
|
194
|
+
|
195
|
+
return DynamicMapSettings(**config)
|
196
|
+
|
197
|
+
def clear(self):
|
198
|
+
"""
|
199
|
+
Clear the cached settings map.
|
200
|
+
"""
|
201
|
+
self._map_settings = None
|
202
|
+
|
203
|
+
@property
|
204
|
+
def settings(self) -> T:
|
205
|
+
"""
|
206
|
+
Get the current settings.
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
The current settings object
|
210
|
+
|
211
|
+
Raises:
|
212
|
+
ValueError: If the active key does not exist in the settings map
|
213
|
+
"""
|
214
|
+
if not self.map_settings.key:
|
215
|
+
if not self.map_settings.map:
|
216
|
+
settings = self.settings_cls(**self.system_settings.model_dump())
|
217
|
+
else:
|
218
|
+
key = list(self.map_settings.map.keys())[0]
|
219
|
+
settings = self.map_settings.map[key]
|
220
|
+
else:
|
221
|
+
if self.map_settings.key not in self.map_settings.map:
|
222
|
+
raise ValueError(
|
223
|
+
f"Key does not exist in settings map: "
|
224
|
+
f"{self.settings_cls_name}, {self.map_settings.key}"
|
225
|
+
)
|
226
|
+
else:
|
227
|
+
settings = self.map_settings.map[self.map_settings.key]
|
228
|
+
|
229
|
+
return settings
|
230
|
+
|
231
|
+
def has_key(self, key: str) -> bool:
|
232
|
+
"""
|
233
|
+
Check if a key exists in the settings map.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
key: The key to check
|
237
|
+
|
238
|
+
Returns:
|
239
|
+
True if the key exists, False otherwise
|
240
|
+
"""
|
241
|
+
return key in self.map_settings.map
|
242
|
+
|
243
|
+
def get_settings_by_key(self, key: str = "") -> T:
|
244
|
+
"""
|
245
|
+
Get settings by key.
|
246
|
+
|
247
|
+
Args:
|
248
|
+
key: The key to get settings for. If empty, returns the active settings.
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
The settings object for the specified key
|
252
|
+
|
253
|
+
Raises:
|
254
|
+
ValueError: If the specified key does not exist in the settings map
|
255
|
+
"""
|
256
|
+
if not key:
|
257
|
+
return self.settings
|
258
|
+
|
259
|
+
if not self.has_key(key):
|
260
|
+
raise ValueError(f"Key does not exist in settings map: {key}")
|
261
|
+
|
262
|
+
return self.map_settings.map[key]
|
263
|
+
|
264
|
+
@property
|
265
|
+
def active_key(self) -> str:
|
266
|
+
"""
|
267
|
+
Get the active key.
|
268
|
+
|
269
|
+
Returns:
|
270
|
+
The active key
|
271
|
+
"""
|
272
|
+
return self.map_settings.key
|
273
|
+
|
274
|
+
@property
|
275
|
+
def all_settings(self) -> Dict[str, T]:
|
276
|
+
"""
|
277
|
+
Get all settings.
|
278
|
+
|
279
|
+
Returns:
|
280
|
+
A dictionary mapping keys to settings objects
|
281
|
+
"""
|
282
|
+
return self.map_settings.map
|
@@ -0,0 +1,96 @@
|
|
1
|
+
"""
|
2
|
+
Single settings manager implementation.
|
3
|
+
"""
|
4
|
+
from typing import Any, Dict, Type
|
5
|
+
|
6
|
+
from .base import BaseSettingsManager, T
|
7
|
+
from .utils import NestedDict, nested_dict, update_dict
|
8
|
+
|
9
|
+
|
10
|
+
class SingleSettingsManager(BaseSettingsManager[T]):
|
11
|
+
"""
|
12
|
+
A settings manager that manages a single settings object.
|
13
|
+
|
14
|
+
This manager is useful when you need to manage a single configuration that can be
|
15
|
+
updated from multiple sources (e.g., command line arguments, configuration files).
|
16
|
+
|
17
|
+
Type Parameters:
|
18
|
+
T: A type that inherits from BaseSettings
|
19
|
+
|
20
|
+
Example:
|
21
|
+
```python
|
22
|
+
from pydantic_settings import BaseSettings
|
23
|
+
from pydantic_settings_manager import SingleSettingsManager
|
24
|
+
|
25
|
+
class MySettings(BaseSettings):
|
26
|
+
name: str = "default"
|
27
|
+
value: int = 0
|
28
|
+
|
29
|
+
# Create a settings manager
|
30
|
+
manager = SingleSettingsManager(MySettings)
|
31
|
+
|
32
|
+
# Update settings from a configuration file
|
33
|
+
manager.user_config = {"name": "from_file", "value": 42}
|
34
|
+
|
35
|
+
# Update settings from command line arguments
|
36
|
+
manager.cli_args = {"value": 100}
|
37
|
+
|
38
|
+
# Get the current settings (combines both sources)
|
39
|
+
settings = manager.settings
|
40
|
+
assert settings.name == "from_file" # from user_config
|
41
|
+
assert settings.value == 100 # from cli_args (overrides user_config)
|
42
|
+
```
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(self, settings_cls: Type[T]):
|
46
|
+
"""
|
47
|
+
Initialize the settings manager.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
settings_cls: The settings class to manage
|
51
|
+
"""
|
52
|
+
self.settings_cls: Type[T] = settings_cls
|
53
|
+
"""The settings class being managed"""
|
54
|
+
|
55
|
+
self.cli_args: NestedDict = nested_dict()
|
56
|
+
"""Command line arguments"""
|
57
|
+
|
58
|
+
self.user_config: Dict[str, Any] = {}
|
59
|
+
"""User configuration"""
|
60
|
+
|
61
|
+
self._settings: T | None = None
|
62
|
+
"""Cached settings"""
|
63
|
+
|
64
|
+
@property
|
65
|
+
def settings(self) -> T:
|
66
|
+
"""
|
67
|
+
Get the current settings.
|
68
|
+
|
69
|
+
The settings are created by combining the user configuration and command line
|
70
|
+
arguments, with command line arguments taking precedence.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
The current settings object
|
74
|
+
"""
|
75
|
+
if not self._settings:
|
76
|
+
self._settings = self._get_settings()
|
77
|
+
|
78
|
+
return self._settings
|
79
|
+
|
80
|
+
def _get_settings(self) -> T:
|
81
|
+
"""
|
82
|
+
Create a new settings object.
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
A new settings object
|
86
|
+
"""
|
87
|
+
config = update_dict(self.user_config, self.cli_args)
|
88
|
+
return self.settings_cls(**config)
|
89
|
+
|
90
|
+
def clear(self):
|
91
|
+
"""
|
92
|
+
Clear the cached settings.
|
93
|
+
|
94
|
+
This forces the next access to settings to create a new settings object.
|
95
|
+
"""
|
96
|
+
self._settings = None
|
@@ -0,0 +1,50 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions for dictionary operations.
|
3
|
+
"""
|
4
|
+
from collections import defaultdict
|
5
|
+
from typing import Any, Dict
|
6
|
+
|
7
|
+
from .types import NestedDict
|
8
|
+
|
9
|
+
|
10
|
+
def diff_dict(base: Dict[str, Any], target: Dict[str, Any]) -> Dict[str, Any]:
|
11
|
+
"""
|
12
|
+
Get the difference between two dictionaries.
|
13
|
+
Only includes keys where the values are different.
|
14
|
+
"""
|
15
|
+
result = {}
|
16
|
+
|
17
|
+
for key in target:
|
18
|
+
if key not in base:
|
19
|
+
result[key] = target[key]
|
20
|
+
elif isinstance(target[key], dict) and isinstance(base[key], dict):
|
21
|
+
nested = diff_dict(base[key], target[key])
|
22
|
+
if nested:
|
23
|
+
result[key] = nested
|
24
|
+
elif target[key] != base[key]:
|
25
|
+
result[key] = target[key]
|
26
|
+
|
27
|
+
return result
|
28
|
+
|
29
|
+
|
30
|
+
def update_dict(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
|
31
|
+
"""
|
32
|
+
Update a dictionary with another dictionary.
|
33
|
+
Performs a deep update.
|
34
|
+
"""
|
35
|
+
result = base.copy()
|
36
|
+
|
37
|
+
for key, value in update.items():
|
38
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
39
|
+
result[key] = update_dict(result[key], value)
|
40
|
+
else:
|
41
|
+
result[key] = value
|
42
|
+
|
43
|
+
return result
|
44
|
+
|
45
|
+
|
46
|
+
def nested_dict() -> NestedDict:
|
47
|
+
"""
|
48
|
+
ネストした dict を作成する
|
49
|
+
"""
|
50
|
+
return defaultdict(nested_dict)
|
@@ -0,0 +1,71 @@
|
|
1
|
+
[tool.poetry]
|
2
|
+
name = "pydantic-settings-manager"
|
3
|
+
version = "0.1.0"
|
4
|
+
description = "A library for managing Pydantic settings objects"
|
5
|
+
authors = ["kiarina <kiarinadawa@gmail.com>"]
|
6
|
+
license = "MIT"
|
7
|
+
readme = "README.md"
|
8
|
+
homepage = "https://github.com/kiarina/pydantic-settings-manager"
|
9
|
+
repository = "https://github.com/kiarina/pydantic-settings-manager"
|
10
|
+
documentation = "https://github.com/kiarina/pydantic-settings-manager"
|
11
|
+
keywords = ["pydantic", "settings", "configuration"]
|
12
|
+
classifiers = [
|
13
|
+
"Development Status :: 4 - Beta",
|
14
|
+
"Intended Audience :: Developers",
|
15
|
+
"License :: OSI Approved :: MIT License",
|
16
|
+
"Operating System :: OS Independent",
|
17
|
+
"Programming Language :: Python :: 3",
|
18
|
+
"Programming Language :: Python :: 3.8",
|
19
|
+
"Programming Language :: Python :: 3.9",
|
20
|
+
"Programming Language :: Python :: 3.10",
|
21
|
+
"Programming Language :: Python :: 3.11",
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
23
|
+
]
|
24
|
+
packages = [
|
25
|
+
{ include = "pydantic_settings_manager" }
|
26
|
+
]
|
27
|
+
|
28
|
+
[tool.poetry.dependencies]
|
29
|
+
python = "^3.8"
|
30
|
+
pydantic = "^2.0.0"
|
31
|
+
pydantic-settings = "^2.0.0"
|
32
|
+
|
33
|
+
[tool.poetry.group.dev.dependencies]
|
34
|
+
pytest = "^7.4.0"
|
35
|
+
pytest-cov = "^4.1.0"
|
36
|
+
black = "^23.7.0"
|
37
|
+
isort = "^5.12.0"
|
38
|
+
mypy = "^1.5.0"
|
39
|
+
ruff = "^0.0.284"
|
40
|
+
|
41
|
+
[build-system]
|
42
|
+
requires = ["poetry-core"]
|
43
|
+
build-backend = "poetry.core.masonry.api"
|
44
|
+
|
45
|
+
[tool.black]
|
46
|
+
line-length = 100
|
47
|
+
target-version = ["py38"]
|
48
|
+
|
49
|
+
[tool.isort]
|
50
|
+
profile = "black"
|
51
|
+
line_length = 100
|
52
|
+
multi_line_output = 3
|
53
|
+
|
54
|
+
[tool.mypy]
|
55
|
+
python_version = "3.8"
|
56
|
+
warn_return_any = true
|
57
|
+
warn_unused_configs = true
|
58
|
+
disallow_untyped_defs = true
|
59
|
+
disallow_incomplete_defs = true
|
60
|
+
check_untyped_defs = true
|
61
|
+
disallow_untyped_decorators = true
|
62
|
+
no_implicit_optional = true
|
63
|
+
warn_redundant_casts = true
|
64
|
+
warn_unused_ignores = true
|
65
|
+
warn_no_return = true
|
66
|
+
warn_unreachable = true
|
67
|
+
|
68
|
+
[tool.ruff]
|
69
|
+
line-length = 100
|
70
|
+
target-version = "py38"
|
71
|
+
select = ["E", "F", "B", "I"]
|