appsettings2 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- appsettings2/Configuration.py +113 -0
- appsettings2/ConfigurationBuilder.py +32 -0
- appsettings2/ConfigurationException.py +7 -0
- appsettings2/__init__.py +9 -0
- appsettings2/providers/CommandLineConfigurationProvider.py +39 -0
- appsettings2/providers/ConfigurationProvider.py +12 -0
- appsettings2/providers/EnvironmentConfigurationProvider.py +13 -0
- appsettings2/providers/JsonConfigurationProvider.py +47 -0
- appsettings2/providers/TomlConfigurationProvider.py +47 -0
- appsettings2/providers/YamlConfigurationProvider.py +50 -0
- appsettings2/providers/__init__.py +9 -0
- appsettings2-0.0.1.dist-info/LICENSE +21 -0
- appsettings2-0.0.1.dist-info/METADATA +235 -0
- appsettings2-0.0.1.dist-info/RECORD +15 -0
- appsettings2-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from . import ConfigurationException
|
|
5
|
+
import json
|
|
6
|
+
import types
|
|
7
|
+
from typing import Any, TypeAlias, get_type_hints
|
|
8
|
+
|
|
9
|
+
any:TypeAlias = Any
|
|
10
|
+
|
|
11
|
+
class Configuration:
|
|
12
|
+
"""A configuration class which creates a layer of indirection between configuration providers and configuration consumers."""
|
|
13
|
+
|
|
14
|
+
__normalize:bool
|
|
15
|
+
|
|
16
|
+
def __init__(self, *, normalize:bool = False):
|
|
17
|
+
"""By default keys are ingest as-is, pass `True` for `normalize` to normalize kets to lower case."""
|
|
18
|
+
self.__normalize = normalize
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
d:dict = self.toDictionary()
|
|
22
|
+
return json.dumps(d)
|
|
23
|
+
|
|
24
|
+
def __recursiveBind(self, target:object, source:'Configuration') -> any:
|
|
25
|
+
targetTypeHints = get_type_hints(target)
|
|
26
|
+
for aname in dir(target):
|
|
27
|
+
if aname.startswith('_'):
|
|
28
|
+
continue
|
|
29
|
+
lval = getattr(target, aname)
|
|
30
|
+
if isinstance(lval, types.FunctionType) or isinstance(lval, types.MethodType):
|
|
31
|
+
continue
|
|
32
|
+
ahint = targetTypeHints.get(aname)
|
|
33
|
+
ahintstr = str(ahint)
|
|
34
|
+
rval = source.get(aname)
|
|
35
|
+
if isinstance(rval, Configuration):
|
|
36
|
+
if ahint is dict or ahintstr.startswith('dict['):
|
|
37
|
+
lval = rval.toDictionary()
|
|
38
|
+
setattr(target, aname, lval)
|
|
39
|
+
elif ahint is list or ahintstr.startswith('list['):
|
|
40
|
+
# naive behavior
|
|
41
|
+
setattr(target, aname, rval)
|
|
42
|
+
else:
|
|
43
|
+
if lval == None:
|
|
44
|
+
lval = ahint()
|
|
45
|
+
setattr(target, aname, lval)
|
|
46
|
+
self.__recursiveBind(lval, rval)
|
|
47
|
+
elif rval != None:
|
|
48
|
+
if ahint is float:
|
|
49
|
+
v = float(rval)
|
|
50
|
+
setattr(target, aname, v)
|
|
51
|
+
elif ahint is int:
|
|
52
|
+
v = int(rval)
|
|
53
|
+
setattr(target, aname, v)
|
|
54
|
+
elif ahint is str:
|
|
55
|
+
v = str(rval)
|
|
56
|
+
setattr(target, aname, v)
|
|
57
|
+
else:
|
|
58
|
+
# naive behavior
|
|
59
|
+
setattr(target, aname, rval)
|
|
60
|
+
else:
|
|
61
|
+
setattr(target, aname, None)
|
|
62
|
+
return target
|
|
63
|
+
|
|
64
|
+
def bind(self, target:object, key:str|None = None) -> any:
|
|
65
|
+
"""Binds the configuration values into the target object.
|
|
66
|
+
|
|
67
|
+
Can optionally specify a configuration key to bind from."""
|
|
68
|
+
if not target:
|
|
69
|
+
raise ConfigurationException('Missing required argument: target')
|
|
70
|
+
if key == None:
|
|
71
|
+
return self.__recursiveBind(target, self)
|
|
72
|
+
else:
|
|
73
|
+
return self.__recursiveBind(target, self.get(key))
|
|
74
|
+
|
|
75
|
+
def get(self, key:str, default:any = None) -> any:
|
|
76
|
+
"""Gets the configuration element associated with the specified key."""
|
|
77
|
+
if self.__normalize:
|
|
78
|
+
key = key.lower()
|
|
79
|
+
parts = key.replace('__', '.').replace(':', '.').split('.')
|
|
80
|
+
o = self
|
|
81
|
+
for part in parts:
|
|
82
|
+
if not hasattr(o, part):
|
|
83
|
+
return default
|
|
84
|
+
o = getattr(o, part)
|
|
85
|
+
return o
|
|
86
|
+
|
|
87
|
+
def set(self, key:str, value:any) -> None:
|
|
88
|
+
"""Sets the configuration element for the specified key."""
|
|
89
|
+
if self.__normalize:
|
|
90
|
+
key = key.lower()
|
|
91
|
+
parts = key.replace('__', '.').replace(':', '.').split('.')
|
|
92
|
+
o = self
|
|
93
|
+
for i in range(len(parts) - 1):
|
|
94
|
+
if not hasattr(o, parts[i]):
|
|
95
|
+
setattr(o, parts[i], Configuration(normalize=self.__normalize))
|
|
96
|
+
o = getattr(o, parts[i])
|
|
97
|
+
if hasattr(o, parts[-1]):
|
|
98
|
+
delattr(o, parts[-1])
|
|
99
|
+
setattr(o, parts[-1], value)
|
|
100
|
+
|
|
101
|
+
def toDictionary(self) -> dict:
|
|
102
|
+
"""Projects a dictionary from the configuration object."""
|
|
103
|
+
result = {}
|
|
104
|
+
for aname in dir(self):
|
|
105
|
+
if aname.startswith('_'):
|
|
106
|
+
continue
|
|
107
|
+
aval = getattr(self, aname)
|
|
108
|
+
if isinstance(aval, types.FunctionType) or isinstance(aval, types.MethodType):
|
|
109
|
+
continue
|
|
110
|
+
if isinstance(aval, Configuration):
|
|
111
|
+
aval = aval.toDictionary()
|
|
112
|
+
result[aname] = aval
|
|
113
|
+
return result
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from . import Configuration, ConfigurationException
|
|
5
|
+
from .providers import ConfigurationProvider
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
any: typing.TypeAlias = typing.Any
|
|
9
|
+
|
|
10
|
+
class ConfigurationBuilder:
|
|
11
|
+
"""A class which builds a Configuration object from one ore more ConfigurationProvider instances."""
|
|
12
|
+
|
|
13
|
+
__normalize:bool
|
|
14
|
+
__providers:list[ConfigurationProvider]
|
|
15
|
+
|
|
16
|
+
def __init__(self, *, normalize:bool = False):
|
|
17
|
+
self.__normalize = normalize
|
|
18
|
+
self.__providers = []
|
|
19
|
+
|
|
20
|
+
def addProvider(self, provider:ConfigurationProvider) -> 'ConfigurationBuilder':
|
|
21
|
+
"""Adds the specified provider to this builder instance."""
|
|
22
|
+
if not provider:
|
|
23
|
+
raise ConfigurationException('Missing required argument: provider')
|
|
24
|
+
self.__providers.append(provider)
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
def build(self) -> Configuration:
|
|
28
|
+
"""Builds a Configuration object from all providers which have been added to this builder."""
|
|
29
|
+
configuration = Configuration(normalize=self.__normalize)
|
|
30
|
+
for provider in self.__providers:
|
|
31
|
+
provider.populateConfiguration(configuration)
|
|
32
|
+
return configuration
|
appsettings2/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from .providers import *
|
|
5
|
+
from .Configuration import Configuration
|
|
6
|
+
from .ConfigurationBuilder import ConfigurationBuilder
|
|
7
|
+
from .ConfigurationException import ConfigurationException
|
|
8
|
+
|
|
9
|
+
__version__ = '0.0.1'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from . import ConfigurationProvider
|
|
5
|
+
from .. import Configuration
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
class CommandLineConfigurationProvider(ConfigurationProvider):
|
|
9
|
+
"""Processes the command-line and provides args as Configuration data."""
|
|
10
|
+
|
|
11
|
+
__argv:list[str]
|
|
12
|
+
|
|
13
|
+
def __init__(self, argv:list[str] = None):
|
|
14
|
+
self.__argv = argv if argv != None else sys.argv
|
|
15
|
+
|
|
16
|
+
def populateConfiguration(self, configuration:Configuration):
|
|
17
|
+
if self.__argv == None:
|
|
18
|
+
return
|
|
19
|
+
pending_key = None
|
|
20
|
+
for arg in self.__argv:
|
|
21
|
+
safe_arg = arg.lstrip('-')
|
|
22
|
+
eqidx = safe_arg.find('=')
|
|
23
|
+
if pending_key:
|
|
24
|
+
if (not arg.startswith('-')) and (eqidx < 1):
|
|
25
|
+
k = pending_key
|
|
26
|
+
v = arg.lstrip('=')
|
|
27
|
+
configuration.set(k, v)
|
|
28
|
+
continue
|
|
29
|
+
else:
|
|
30
|
+
configuration.set(pending_key, True)
|
|
31
|
+
pending_key = None
|
|
32
|
+
if eqidx > 0:
|
|
33
|
+
k = safe_arg[0:eqidx]
|
|
34
|
+
v = safe_arg[eqidx+1:len(safe_arg)]
|
|
35
|
+
configuration.set(k, v)
|
|
36
|
+
elif arg.startswith('--'):
|
|
37
|
+
pending_key = safe_arg
|
|
38
|
+
if pending_key != None:
|
|
39
|
+
configuration.set(pending_key, True)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from .. import Configuration
|
|
5
|
+
from abc import ABC as abstract, abstractmethod
|
|
6
|
+
|
|
7
|
+
class ConfigurationProvider(abstract):
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def populateConfiguration(self, configuration:Configuration) -> None:
|
|
11
|
+
"""The ConfigurationProvider will populate the provided Configuration instance."""
|
|
12
|
+
pass
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from . import ConfigurationProvider
|
|
5
|
+
from .. import Configuration
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
class EnvironmentConfigurationProvider(ConfigurationProvider):
|
|
9
|
+
"""Processes environment variables and provides them as Configuration data."""
|
|
10
|
+
|
|
11
|
+
def populateConfiguration(self, configuration:Configuration):
|
|
12
|
+
for k in os.environ:
|
|
13
|
+
configuration.set(k, os.environ[k])
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from . import ConfigurationProvider
|
|
5
|
+
from .. import Configuration, ConfigurationException
|
|
6
|
+
import json as _json
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, TypeAlias
|
|
9
|
+
|
|
10
|
+
FileDescriptor: TypeAlias = int
|
|
11
|
+
any: TypeAlias = Any
|
|
12
|
+
|
|
13
|
+
class JsonConfigurationProvider(ConfigurationProvider):
|
|
14
|
+
"""Processes a json string, json file, or json stream and provides the content as Configuration data."""
|
|
15
|
+
|
|
16
|
+
__obj:dict
|
|
17
|
+
|
|
18
|
+
def __init__(self, filepath:str = None, *, json:str = None, fd:FileDescriptor = None, required:bool = True):
|
|
19
|
+
if filepath:
|
|
20
|
+
if os.path.isfile(filepath):
|
|
21
|
+
with open(filepath, 'rt') as file:
|
|
22
|
+
json = file.read()
|
|
23
|
+
elif required:
|
|
24
|
+
raise ConfigurationException(f'Missing required file: {filepath}')
|
|
25
|
+
elif fd:
|
|
26
|
+
with open(fd, 'rt') as file:
|
|
27
|
+
json = file.read()
|
|
28
|
+
if json:
|
|
29
|
+
self.__obj = _json.loads(json)
|
|
30
|
+
else:
|
|
31
|
+
self.__obj = None
|
|
32
|
+
|
|
33
|
+
def __populateRecursive(self, configuration:Configuration, prefix:str, o:dict):
|
|
34
|
+
for kvp in o.items():
|
|
35
|
+
if isinstance(kvp[1], dict):
|
|
36
|
+
self.__populateRecursive(configuration, f'{prefix}__{kvp[0]}', kvp[1])
|
|
37
|
+
else:
|
|
38
|
+
configuration.set(f'{prefix}__{kvp[0]}', kvp[1])
|
|
39
|
+
|
|
40
|
+
def populateConfiguration(self, configuration:Configuration):
|
|
41
|
+
if self.__obj == None:
|
|
42
|
+
return
|
|
43
|
+
for kvp in self.__obj.items():
|
|
44
|
+
if isinstance(kvp[1], dict):
|
|
45
|
+
self.__populateRecursive(configuration, kvp[0], kvp[1])
|
|
46
|
+
else:
|
|
47
|
+
configuration.set(f'{kvp[0]}', kvp[1])
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from . import ConfigurationProvider
|
|
5
|
+
from .. import Configuration, ConfigurationException
|
|
6
|
+
import os
|
|
7
|
+
import tomllib as _toml
|
|
8
|
+
from typing import Any, TypeAlias
|
|
9
|
+
|
|
10
|
+
FileDescriptor: TypeAlias = int
|
|
11
|
+
any: TypeAlias = Any
|
|
12
|
+
|
|
13
|
+
class TomlConfigurationProvider(ConfigurationProvider):
|
|
14
|
+
"""Processes a toml string, toml file, or toml stream and provides the content as Configuration data."""
|
|
15
|
+
|
|
16
|
+
__obj:dict
|
|
17
|
+
|
|
18
|
+
def __init__(self, filepath:str = None, *, toml:str = None, fd:FileDescriptor = None, required:bool = True):
|
|
19
|
+
if filepath:
|
|
20
|
+
if os.path.isfile(filepath):
|
|
21
|
+
with open(filepath, 'rt') as file:
|
|
22
|
+
toml = file.read()
|
|
23
|
+
elif required:
|
|
24
|
+
raise ConfigurationException(f'Missing required file: {filepath}')
|
|
25
|
+
elif fd:
|
|
26
|
+
with open(fd, 'rt') as file:
|
|
27
|
+
toml = file.read()
|
|
28
|
+
if toml:
|
|
29
|
+
self.__obj = _toml.loads(toml)
|
|
30
|
+
else:
|
|
31
|
+
self.__obj = None
|
|
32
|
+
|
|
33
|
+
def __populateRecursive(self, configuration:Configuration, prefix:str, o:dict):
|
|
34
|
+
for kvp in o.items():
|
|
35
|
+
if isinstance(kvp[1], dict):
|
|
36
|
+
self.__populateRecursive(configuration, f'{prefix}__{kvp[0]}', kvp[1])
|
|
37
|
+
else:
|
|
38
|
+
configuration.set(f'{prefix}__{kvp[0]}', kvp[1])
|
|
39
|
+
|
|
40
|
+
def populateConfiguration(self, configuration:Configuration):
|
|
41
|
+
if self.__obj == None:
|
|
42
|
+
return
|
|
43
|
+
for kvp in self.__obj.items():
|
|
44
|
+
if isinstance(kvp[1], dict):
|
|
45
|
+
self.__populateRecursive(configuration, kvp[0], kvp[1])
|
|
46
|
+
else:
|
|
47
|
+
configuration.set(f'{kvp[0]}', kvp[1])
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from . import ConfigurationProvider
|
|
5
|
+
from .. import Configuration, ConfigurationException
|
|
6
|
+
from io import StringIO
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, TypeAlias
|
|
9
|
+
import yaml as _yaml
|
|
10
|
+
|
|
11
|
+
FileDescriptor: TypeAlias = int
|
|
12
|
+
any: TypeAlias = Any
|
|
13
|
+
|
|
14
|
+
class YamlConfigurationProvider(ConfigurationProvider):
|
|
15
|
+
"""Processes a yaml string, yaml file, or yaml stream and provides the content as Configuration data."""
|
|
16
|
+
|
|
17
|
+
__obj:dict
|
|
18
|
+
|
|
19
|
+
def __init__(self, filepath:str = None, *, yaml:str = None, fd:FileDescriptor = None, required:bool = True):
|
|
20
|
+
if filepath:
|
|
21
|
+
if os.path.isfile(filepath):
|
|
22
|
+
with open(filepath, 'rt') as file:
|
|
23
|
+
self.__obj = _yaml.safe_load(file)
|
|
24
|
+
elif required:
|
|
25
|
+
raise ConfigurationException(f'Missing required file: {filepath}')
|
|
26
|
+
elif fd:
|
|
27
|
+
with open(fd, 'rt') as file:
|
|
28
|
+
self.__obj = _yaml.safe_load(file)
|
|
29
|
+
elif yaml:
|
|
30
|
+
stream = StringIO(yaml)
|
|
31
|
+
self.__obj = _yaml.safe_load(stream)
|
|
32
|
+
stream.close()
|
|
33
|
+
else:
|
|
34
|
+
self.__obj = None
|
|
35
|
+
|
|
36
|
+
def __populateRecursive(self, configuration:Configuration, prefix:str, o:dict):
|
|
37
|
+
for kvp in o.items():
|
|
38
|
+
if isinstance(kvp[1], dict):
|
|
39
|
+
self.__populateRecursive(configuration, f'{prefix}__{kvp[0]}', kvp[1])
|
|
40
|
+
else:
|
|
41
|
+
configuration.set(f'{prefix}__{kvp[0]}', kvp[1])
|
|
42
|
+
|
|
43
|
+
def populateConfiguration(self, configuration:Configuration):
|
|
44
|
+
if self.__obj == None:
|
|
45
|
+
return
|
|
46
|
+
for kvp in self.__obj.items():
|
|
47
|
+
if isinstance(kvp[1], dict):
|
|
48
|
+
self.__populateRecursive(configuration, kvp[0], kvp[1])
|
|
49
|
+
else:
|
|
50
|
+
configuration.set(f'{kvp[0]}', kvp[1])
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from .ConfigurationProvider import ConfigurationProvider
|
|
5
|
+
from .CommandLineConfigurationProvider import CommandLineConfigurationProvider
|
|
6
|
+
from .EnvironmentConfigurationProvider import EnvironmentConfigurationProvider
|
|
7
|
+
from .JsonConfigurationProvider import JsonConfigurationProvider
|
|
8
|
+
from .TomlConfigurationProvider import TomlConfigurationProvider
|
|
9
|
+
from .YamlConfigurationProvider import YamlConfigurationProvider
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Shaun Wilson
|
|
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,235 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: appsettings2
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A python library that unifies configuration sources into a Configuration object that can be bound to complex types, or accessed directly for configuration data.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Shaun Wilson
|
|
7
|
+
Author-email: mrshaunwilson@msn.com
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
|
14
|
+
Project-URL: documentation, https://github.com/wilson0x4d/appsettings2/README.md
|
|
15
|
+
Project-URL: homepage, https://github.com/wilson0x4d/appsettings2
|
|
16
|
+
Project-URL: repository, https://github.com/wilson0x4d/appsettings2.git
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# `appsettings2`
|
|
20
|
+
|
|
21
|
+
A python library that unifies configuration sources into a Configuration object that can be bound to complex types, or accessed directly for configuration data.
|
|
22
|
+
|
|
23
|
+
## Why?
|
|
24
|
+
|
|
25
|
+
Because I write a lot of containerized applications where I typically supply configuration settings using a combination of Configuration Files and Environment Variables.
|
|
26
|
+
|
|
27
|
+
When I debug those applications I often need to override Confguration using Command-Line Arguments, Configuration Files, Environment Variables, or a combination thereof.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Construct a `ConfigurationBuilder` instance, add one or more `ConfigurationProvider` instances to it, and then use it to build a `Configuration` object containing your unified configuration data.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from appsettings2 import *
|
|
35
|
+
from appsettings2.providers import *
|
|
36
|
+
|
|
37
|
+
def get_configuration() -> Configuration:
|
|
38
|
+
return ConfigurationBuilder()\
|
|
39
|
+
.addProvider(JsonConfigurationProvider(f'appsettings.json'))\
|
|
40
|
+
.addProvider(JsonConfigurationProvider(f'appsettings.Development.json', required=False))\
|
|
41
|
+
.addProvider(EnvironmentConfigurationProvider())\
|
|
42
|
+
.build()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
In the above example two JSON Configuration Providers are added to the builder. The first will fail if "appsettings.json" is not found, the second will succeed whether or not the "appsettings.Development.json" exists (because `required=False`). This allows for an optional development configuration to exist on development workstations without requiring a code change.
|
|
46
|
+
|
|
47
|
+
A third Configuration Provider is also added, an `EnvironmentConfigurationProvider`, which allows configuration to be loaded from Environment Variables.
|
|
48
|
+
|
|
49
|
+
The order of providers determines precedence of configuration data. Providers added last will take precedence over providers added first. Values which are not provided by later registrations will not override values provided by earlier registrations.
|
|
50
|
+
|
|
51
|
+
Given the example above, this means that configuration data provided via Environment Variables will override configuration data provided via JSON files.
|
|
52
|
+
|
|
53
|
+
## Accssing Configuration Data
|
|
54
|
+
|
|
55
|
+
There are two methods of directly accessing Configuration data directly. The first is by using first-class attributes in python, and the second is by using a `get(...)` method.
|
|
56
|
+
|
|
57
|
+
Consider the following code which demonstrates both methods, where all three of these statements are equivalent:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
value = configuration.ConnectionStrings.SampleDb
|
|
61
|
+
value = configuration.get('ConnectionStrings:SampleDb')
|
|
62
|
+
value = configuration.get('ConnectionStrings__SampleDb')
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
When accessing hierarchical data you can use a double-underscore delimiter or a colon delimiter, they are equivalent. Devs and Ops from different walks will likely prefer one form over the other, so both are supported.
|
|
66
|
+
|
|
67
|
+
## Binding to Configuration Data
|
|
68
|
+
|
|
69
|
+
It's also possible to bind configuration data to complex types. While some of the implementation is currently naive, it should work well for the vast majority of use-cases. If you find your particular edge case does not work well please reach out to me and I will work with you to implement a sensible solution. Consider the following Python code:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
json = """{
|
|
73
|
+
"ConnectionStrings": {
|
|
74
|
+
"SampleDb": "my_cxn_string"
|
|
75
|
+
},
|
|
76
|
+
"EnableSwagger": true,
|
|
77
|
+
"MaxBatchSize": 100
|
|
78
|
+
}"""
|
|
79
|
+
|
|
80
|
+
class ConnStrs:
|
|
81
|
+
"""An ugly class name to demonstrate the class name does not matter."""
|
|
82
|
+
SampleDb:str = None
|
|
83
|
+
|
|
84
|
+
class AppSettings:
|
|
85
|
+
ConnectionStrings:ConnStrs = None
|
|
86
|
+
EnableSwagger:bool = None
|
|
87
|
+
MaxBatchSize:int = None
|
|
88
|
+
|
|
89
|
+
configuration = ConfigurationBuilder()\
|
|
90
|
+
.addProvider(JsonConfigurationProvider(json=json))
|
|
91
|
+
.build()
|
|
92
|
+
|
|
93
|
+
settings = AppSettings()
|
|
94
|
+
configuration.bind(settings)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The resulting `settings` object will contain all of the configuration data properly typed according to type hints.
|
|
98
|
+
|
|
99
|
+
It is also possible to bind to a subset of a configuration, building upon the above, consider the following:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
connectionStrings = configuration.bind(ConnectionStrings(), 'ConnectionStrings')
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Projecting Configuration Data as a Dictionary
|
|
106
|
+
|
|
107
|
+
Laslty, as a convenience item you can materialize a `Configuration` instance as a Dictionary. This is primarily included as a debugging aid, but, if you have some chunk of code that can consume a dictionary but can't consume a Python object (it happens) then you can get at a dictionary as follows:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
config:Configuration = builder.build()
|
|
111
|
+
my_dict = config.toDictionary()
|
|
112
|
+
print(my_dict)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Providers
|
|
116
|
+
|
|
117
|
+
### Command-Line
|
|
118
|
+
|
|
119
|
+
The `CommandLineConfigurationProvider` allows configuration data to be provided via command-line interface. This provider will process variables in the following forms as key-value pairs:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
--ConnectionStrings__SampleDb 'my_connection_string'
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
'ConnectionStrings__SampleDb=my_connection_string'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
--ConnectionStrings__SampleDb=my_connection_string
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
All three of these forms result in a configuration object with the following state (represented as JSON):
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"ConnectionStrings" : {
|
|
138
|
+
"SampleDb": "my_connection_string"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Take note that the leading dashes are stripped.
|
|
144
|
+
|
|
145
|
+
This double-underscore convention is a convention borrowed from other platforms/frameworks.
|
|
146
|
+
|
|
147
|
+
It's also possible to use a colon in lieu of a double_underscore, but this may have unintended side-effects depending on the shell or platform being used:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
'ConnectionStrings:SampleDb=my_connection_string'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This provider was implemented in a way that it does not interfere with libraries such as `argparse`, and should work as expected with a well-formed command-line interface.
|
|
154
|
+
|
|
155
|
+
### Environment Variables
|
|
156
|
+
|
|
157
|
+
The `EnvironmentConfigurationProvider` consumes environment variables as key-value pairs. For example, consider these `bash` exports:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
export ConnectionStrings__SampleDb=my_cxn_string
|
|
161
|
+
export ConnectionStrings__AnotherDb=another_cxn_string
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The above will resule in a Configuration object with the following state (represented as JSON):
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"ConnectionStrings": {
|
|
169
|
+
"SampleDb": "my_cxn_string",
|
|
170
|
+
"AnotherDb": "another_cxn_string"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### JSON
|
|
176
|
+
|
|
177
|
+
The `JsonConfigurationProvider` class allows you to load configuration data from a JSON file, JSON string, or JSON stream. Consider the following JSON:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"Logging": {
|
|
182
|
+
"Default": "Debug"
|
|
183
|
+
},
|
|
184
|
+
"ConnectionStrings": {
|
|
185
|
+
"SampleDb": "my_cxn_string"
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
When loaded using `JsonConfigurationProvider` the resulting `Configuration` object will have attribute names and a structure which matches the JSON, consider the following Python code (where "example.json" contains the above JSON):
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from appsettings2 import *
|
|
194
|
+
from appsettings2.providers import *
|
|
195
|
+
|
|
196
|
+
config = ConfigurationBuilder(normalize=True)\
|
|
197
|
+
.addProvider(JsonConfigurationProvider('example.json'))\
|
|
198
|
+
.build()
|
|
199
|
+
|
|
200
|
+
print(config.ConnectionStrings.SampleDb)
|
|
201
|
+
```
|
|
202
|
+
Outputs: `my_cxn_string` (as expected.)
|
|
203
|
+
|
|
204
|
+
### TOML
|
|
205
|
+
|
|
206
|
+
The `TomlConfigurationProvider` should be functionally identical to the JSON provider, with the obvious difference that it loads Configuration data from a toml file.
|
|
207
|
+
|
|
208
|
+
### YAML
|
|
209
|
+
|
|
210
|
+
The `YamlConfigurationProvider` should be functionally identical to the JSON and TOML providers, with the obvious difference that it loads Configuration data from a yaml file.
|
|
211
|
+
|
|
212
|
+
## A note on `normalize` parameters..
|
|
213
|
+
|
|
214
|
+
`ConfigurationBuilder` accepts a parameter `normalize:bool` which defaults to `False` causing `bind()` to behave in a case-sensitive fashion.
|
|
215
|
+
|
|
216
|
+
When set to `True` it causes the internal implementation of `Configuration` to normalize all configuration keys to uppercase, and consequently, `bind(...)` is no longer case-sensitive.
|
|
217
|
+
|
|
218
|
+
However, the resulting `Configuration` object attribute names also become normalized to upper case which may not be desired by some developers.
|
|
219
|
+
|
|
220
|
+
Probably most devs and devops working in enterprise environments will want to adopt `normalize=True` as a matter of practice. This will allow devops to conform around the use of upper-case key names in configuration systems, and allow devs to use whatever casing rules make the most sense for their projects (since `bind()` will still work as expected.) This does assume developers are creating concerete configuration types (you ARE doing that, correct?)
|
|
221
|
+
|
|
222
|
+
I would have preferred to make `normalize=True` by default, but, I suspect someone out there will want to use `Configuration` instances directly rather than create bind targets and this would have resulted in attribute names that were upper case. To illustrate, from one of the early examples in this doc would instead look like this:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
value = configuration.CONNECTIONSTRINGS.SAMPLEDB # <-- this
|
|
226
|
+
value = configuration.get('ConnectionStrings:SampleDb')
|
|
227
|
+
value = configuration.get('ConnectionStrings__SampleDb')
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
In the future I may further abstract this detail away so we get the best of both worlds, but for now there is the parameter to let you control the behavior.
|
|
231
|
+
|
|
232
|
+
## Conclusion
|
|
233
|
+
|
|
234
|
+
If you made it this far, this library is probably what you were looking for. I'm certain there are other libraries which accomplish much of what I am attempting to accomplish here, but I wanted something i can directly support over the coming years. That, and the nearest equivalent to what I wanted fell out of active development nearly a decade ago and I just didn't feel comfortable adopting it for new work.
|
|
235
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
appsettings2/Configuration.py,sha256=057TmdNlZdbBc0J1OYVkTmfEoYmGMmGkQMvrQmqZnKE,4296
|
|
2
|
+
appsettings2/ConfigurationBuilder.py,sha256=zGTvtqtcap4htyQw1C_LQVS8sQ2LOce0na136uAposY,1202
|
|
3
|
+
appsettings2/ConfigurationException.py,sha256=KJff_AGwRhEhC8GBoAvdJFjn45Wd-jNDJzQQSuWkIoE,246
|
|
4
|
+
appsettings2/__init__.py,sha256=vhaJ04p9YFLgKHPdc6buDQFdVgdnwKSvwcXRbCLIU8o,282
|
|
5
|
+
appsettings2/providers/CommandLineConfigurationProvider.py,sha256=qo9ZvyVXtJaIayewO-QOrxqiqgU1tX1y1WZPSai4dYo,1351
|
|
6
|
+
appsettings2/providers/ConfigurationProvider.py,sha256=KlkOo-CstkUihcP8pfnqu5cvok_a6Rdh86K2ex11LJg,395
|
|
7
|
+
appsettings2/providers/EnvironmentConfigurationProvider.py,sha256=28F4jgOnwy6KksRg8T8S0ozdM92lYlFjpcTIitD2Mv0,445
|
|
8
|
+
appsettings2/providers/JsonConfigurationProvider.py,sha256=cDUFTcYTHquNTVkE1iri8ptadfYKGZCwRVxaMSdUipc,1710
|
|
9
|
+
appsettings2/providers/TomlConfigurationProvider.py,sha256=XvUR3WCwSDMTYZRlQNDm6dmortcQxCABfJ-2JeC-GgE,1713
|
|
10
|
+
appsettings2/providers/YamlConfigurationProvider.py,sha256=wgxoqnTQYNf4CUPONIyo9b4wNRHSvEHfmrQqSyK_nbk,1837
|
|
11
|
+
appsettings2/providers/__init__.py,sha256=sd0lATlab1hL6F_VzSGGPCy_FFbLMORqpNGLKz3j1Ik,489
|
|
12
|
+
appsettings2-0.0.1.dist-info/LICENSE,sha256=3S8RxgIJZybuKMTWCqX5uZTDhZw2G38Dmu56LuTgxac,1090
|
|
13
|
+
appsettings2-0.0.1.dist-info/METADATA,sha256=0Z9wgYlBiRTdXFIRNnBV6j30GX1ODMCbogrQfkfuQ1s,10212
|
|
14
|
+
appsettings2-0.0.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
15
|
+
appsettings2-0.0.1.dist-info/RECORD,,
|