RFL.settings 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- RFL.settings-1.0.0.dist-info/METADATA +23 -0
- RFL.settings-1.0.0.dist-info/RECORD +8 -0
- RFL.settings-1.0.0.dist-info/WHEEL +5 -0
- RFL.settings-1.0.0.dist-info/top_level.txt +1 -0
- rfl/settings/__init__.py +151 -0
- rfl/settings/definition.py +201 -0
- rfl/settings/errors.py +23 -0
- rfl/settings/loaders.py +38 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: RFL.settings
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Rackslab Foundation Library: settings package
|
|
5
|
+
Author-email: Rémi Palancher <remi@rackslab.io>
|
|
6
|
+
License: GPLv3+
|
|
7
|
+
Project-URL: Homepage, https://github.com/rackslab/RFL
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/rackslab/RFL/issues
|
|
9
|
+
Keywords: utility,basic
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: System :: Systems Administration
|
|
16
|
+
Requires-Python: >=3.6
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: RFL.core
|
|
19
|
+
Requires-Dist: PyYAML
|
|
20
|
+
|
|
21
|
+
# RFL: settings package
|
|
22
|
+
|
|
23
|
+
Manage configuration files settings.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
rfl/settings/__init__.py,sha256=Jge5Y06IjO2aki9gtmTZAiwk8hAUxtdUvWUh6W1tiI0,6074
|
|
2
|
+
rfl/settings/definition.py,sha256=p8vdXRPh9H0HUFsZzYuFOeOziq0Ans9TCFKG642loNA,7077
|
|
3
|
+
rfl/settings/errors.py,sha256=Zaoe_iIN4OT4J6u81X0J7BBQTGm67D9LWJcaNbC-wXU,355
|
|
4
|
+
rfl/settings/loaders.py,sha256=3j3hBAiX0bvkRbgiyfp5BgTqy8foxL6Xd32XytZLgak,1152
|
|
5
|
+
RFL.settings-1.0.0.dist-info/METADATA,sha256=ALFOW8AjwyagCUZLPz6VOYr0VFqmJcyMEauCreK0s5Y,827
|
|
6
|
+
RFL.settings-1.0.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
7
|
+
RFL.settings-1.0.0.dist-info/top_level.txt,sha256=EIloiMrXuRz8n0vqkCE6NM9Ze1S2mrbCKBcdtWaM_og,4
|
|
8
|
+
RFL.settings-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rfl
|
rfl/settings/__init__.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Copyright (c) 2023 Rackslab
|
|
2
|
+
#
|
|
3
|
+
# This file is part of RFL.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import urllib
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from .definition import (
|
|
13
|
+
SettingsDefinition,
|
|
14
|
+
SettingsSectionDefinition,
|
|
15
|
+
SettingsParameterDefinition,
|
|
16
|
+
SettingsDefinitionLoaderYaml,
|
|
17
|
+
)
|
|
18
|
+
from .loaders import RuntimeSettingsSiteLoader, RuntimeSettingsSiteLoaderIni
|
|
19
|
+
from .errors import SettingsOverrideError
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RuntimeSettingsSection:
|
|
25
|
+
def __init__(self, section: SettingsSectionDefinition):
|
|
26
|
+
self._name = section.name
|
|
27
|
+
for parameter in section.parameters:
|
|
28
|
+
setattr(self, parameter.name, parameter.default)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RuntimeSettings:
|
|
32
|
+
def __init__(self, definition: SettingsDefinition):
|
|
33
|
+
# set to hold all sections names
|
|
34
|
+
self._sections = set()
|
|
35
|
+
self._definition = definition
|
|
36
|
+
for section in definition.sections:
|
|
37
|
+
o_section = type(
|
|
38
|
+
f"{self.__class__.__name__}{section.name.capitalize()}",
|
|
39
|
+
(RuntimeSettingsSection,),
|
|
40
|
+
dict(),
|
|
41
|
+
)(section)
|
|
42
|
+
setattr(self, o_section._name, o_section)
|
|
43
|
+
self._sections.add(o_section._name)
|
|
44
|
+
|
|
45
|
+
def override(self, loader: RuntimeSettingsSiteLoader):
|
|
46
|
+
# load site ini file and check it matches definition
|
|
47
|
+
for section_name in loader.content.sections():
|
|
48
|
+
if not self._definition.has_section(section_name):
|
|
49
|
+
raise SettingsOverrideError(
|
|
50
|
+
f"Section {section_name} loaded in settings overrides is not "
|
|
51
|
+
"defined in settings definition"
|
|
52
|
+
)
|
|
53
|
+
section = self._definition.section(section_name)
|
|
54
|
+
for parameter_name in loader.content[section.name]:
|
|
55
|
+
if not section.has_parameter(parameter_name):
|
|
56
|
+
raise SettingsOverrideError(
|
|
57
|
+
f"Parameter {parameter_name} loaded in settings overrides is "
|
|
58
|
+
f"not defined in section {section.name} of settings definition"
|
|
59
|
+
)
|
|
60
|
+
parameter = section.parameter(parameter_name)
|
|
61
|
+
self._override_parameter(loader, section, parameter)
|
|
62
|
+
|
|
63
|
+
# check required parameters
|
|
64
|
+
self._check_required()
|
|
65
|
+
|
|
66
|
+
def _check_required(self):
|
|
67
|
+
"""Verify that all parameters declared as required in settings definition are
|
|
68
|
+
properly defined with a real value or raise SettingsOverrideError."""
|
|
69
|
+
for section in self._definition.sections:
|
|
70
|
+
for parameter in section.parameters:
|
|
71
|
+
if (
|
|
72
|
+
parameter.required
|
|
73
|
+
and getattr(getattr(self, parameter.section), parameter.name)
|
|
74
|
+
is None
|
|
75
|
+
):
|
|
76
|
+
raise SettingsOverrideError(
|
|
77
|
+
f"Parameter {str(parameter)} is missing but required in "
|
|
78
|
+
"settings overrides"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _override_parameter(
|
|
82
|
+
self,
|
|
83
|
+
loader: RuntimeSettingsSiteLoader,
|
|
84
|
+
section: SettingsSectionDefinition,
|
|
85
|
+
parameter: SettingsParameterDefinition,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Load value from INI file, convert it to expected type, verify it is a valid
|
|
88
|
+
choice and set corresponding object attribute. Special logic is applied for list
|
|
89
|
+
to iterate over the items."""
|
|
90
|
+
raw = loader.content[section.name].get(parameter.name)
|
|
91
|
+
if parameter._type == "list":
|
|
92
|
+
value = [
|
|
93
|
+
self._load_parameter_value(parameter, parameter.content, raw)
|
|
94
|
+
for raw in raw.strip().split("\n")
|
|
95
|
+
]
|
|
96
|
+
for item in value:
|
|
97
|
+
self._validate_choice(parameter, item)
|
|
98
|
+
else:
|
|
99
|
+
value = self._load_parameter_value(parameter, parameter._type, raw)
|
|
100
|
+
self._validate_choice(parameter, value)
|
|
101
|
+
setattr(getattr(self, section.name), parameter.name, value)
|
|
102
|
+
|
|
103
|
+
def _load_parameter_value(self, parameter, _type: str, raw: str):
|
|
104
|
+
"""Try to convert raw value to expected type or raise SettingsOverrideError."""
|
|
105
|
+
if _type == "path":
|
|
106
|
+
# Converting a string to a pathlib.Path basically never fails.
|
|
107
|
+
return Path(raw)
|
|
108
|
+
elif _type == "uri":
|
|
109
|
+
# Converting a string to a urllib.ParseResult basically never fails.
|
|
110
|
+
return urllib.parse.urlparse(raw)
|
|
111
|
+
elif _type == "int":
|
|
112
|
+
try:
|
|
113
|
+
return int(raw)
|
|
114
|
+
except ValueError as err:
|
|
115
|
+
raise SettingsOverrideError(
|
|
116
|
+
f"Invalid integer value '{raw}' for {parameter} in site overrides"
|
|
117
|
+
) from err
|
|
118
|
+
elif _type == "float":
|
|
119
|
+
try:
|
|
120
|
+
return float(raw)
|
|
121
|
+
except ValueError as err:
|
|
122
|
+
raise SettingsOverrideError(
|
|
123
|
+
f"Invalid float value '{raw}' for {parameter} in site overrides"
|
|
124
|
+
) from err
|
|
125
|
+
elif _type == "bool":
|
|
126
|
+
if raw.lower() in ["1", "yes", "true", "on"]:
|
|
127
|
+
return True
|
|
128
|
+
elif raw.lower() in ["0", "no", "false", "off"]:
|
|
129
|
+
return False
|
|
130
|
+
else:
|
|
131
|
+
raise SettingsOverrideError(
|
|
132
|
+
f"Invalid boolean value '{raw}' for {parameter} in site overrides"
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
# Return unmodified value
|
|
136
|
+
return raw
|
|
137
|
+
|
|
138
|
+
def _validate_choice(self, parameter, value):
|
|
139
|
+
"""Validate value is in parameter choices or raise SettingsOverrideError."""
|
|
140
|
+
if parameter.choices is not None and value not in parameter.choices:
|
|
141
|
+
raise SettingsOverrideError(
|
|
142
|
+
f"Value {value} for parameter {parameter} in site overrides is "
|
|
143
|
+
f"not one of possible choices {parameter.choices}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def override_ini(self, path: Path) -> None:
|
|
147
|
+
self.override(RuntimeSettingsSiteLoaderIni(path=path))
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def yaml_definition(cls, path: Path):
|
|
151
|
+
return cls(SettingsDefinition(SettingsDefinitionLoaderYaml(path=path)))
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Copyright (c) 2023 Rackslab
|
|
2
|
+
#
|
|
3
|
+
# This file is part of RFL.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import urllib
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from .errors import SettingsDefinitionError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SettingsDefinitionLoader:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SettingsDefinitionLoaderYaml(SettingsDefinitionLoader):
|
|
20
|
+
def __init__(self, raw: str = None, path: Path = None):
|
|
21
|
+
try:
|
|
22
|
+
if raw is not None:
|
|
23
|
+
self.content = yaml.safe_load(raw)
|
|
24
|
+
elif path is not None:
|
|
25
|
+
try:
|
|
26
|
+
with open(path) as fh:
|
|
27
|
+
self.content = yaml.safe_load(fh)
|
|
28
|
+
except FileNotFoundError as err:
|
|
29
|
+
raise SettingsDefinitionError(
|
|
30
|
+
f"Settings definition file {path} not found"
|
|
31
|
+
) from err
|
|
32
|
+
else:
|
|
33
|
+
raise SettingsDefinitionError(
|
|
34
|
+
"Either a raw string value or a path must be given to load YAML "
|
|
35
|
+
"settings definition"
|
|
36
|
+
)
|
|
37
|
+
except yaml.parser.ParserError as err:
|
|
38
|
+
raise SettingsDefinitionError(
|
|
39
|
+
f"Invalid YAML settings definition: {str(err)}"
|
|
40
|
+
) from err
|
|
41
|
+
except yaml.scanner.ScannerError as err:
|
|
42
|
+
raise SettingsDefinitionError(f"YAML scanner error: {str(err)}") from err
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SettingsParameterDefinition:
|
|
46
|
+
# Map properties that can be found in definition file with corresponding
|
|
47
|
+
# object attributes names.
|
|
48
|
+
POSSIBLE_PROPERTIES = {
|
|
49
|
+
"type": "_type",
|
|
50
|
+
"content": "content",
|
|
51
|
+
"default": "default",
|
|
52
|
+
"choices": "choices",
|
|
53
|
+
"doc": "doc",
|
|
54
|
+
"ex": "example",
|
|
55
|
+
"required": "required",
|
|
56
|
+
}
|
|
57
|
+
EXPECTED_TYPES = {
|
|
58
|
+
"str": str,
|
|
59
|
+
"int": int,
|
|
60
|
+
"float": float,
|
|
61
|
+
"path": Path,
|
|
62
|
+
"bool": bool,
|
|
63
|
+
"uri": urllib.parse.ParseResult,
|
|
64
|
+
"list": list,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def __init__(self, section: str, name: str, properties: dict):
|
|
68
|
+
self.section = section
|
|
69
|
+
self.name = name
|
|
70
|
+
|
|
71
|
+
# Check for unsupported properties
|
|
72
|
+
unsupported_properties = list(
|
|
73
|
+
set(properties.keys()).difference(self.POSSIBLE_PROPERTIES.keys())
|
|
74
|
+
)
|
|
75
|
+
if len(unsupported_properties):
|
|
76
|
+
raise SettingsDefinitionError(
|
|
77
|
+
f"Unsupported properties found for {str(self)}: "
|
|
78
|
+
f"{','.join(unsupported_properties)}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
for _property, attribute in self.POSSIBLE_PROPERTIES.items():
|
|
82
|
+
value = None
|
|
83
|
+
if _property in properties:
|
|
84
|
+
value = properties[_property]
|
|
85
|
+
setattr(self, attribute, value)
|
|
86
|
+
|
|
87
|
+
# Check the type of defined parameter is actually supported
|
|
88
|
+
if self._type not in self.EXPECTED_TYPES:
|
|
89
|
+
raise SettingsDefinitionError(
|
|
90
|
+
f"Unsupported type {self._type} for {str(self)}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Check content property is defined for list and check content type is actually
|
|
94
|
+
# supported.
|
|
95
|
+
if self._type == "list":
|
|
96
|
+
if "content" not in properties:
|
|
97
|
+
raise SettingsDefinitionError(
|
|
98
|
+
f"List content type for parameter {str(self)} must be defined"
|
|
99
|
+
)
|
|
100
|
+
elif self.content not in set(self.EXPECTED_TYPES.keys()) - {"list"}:
|
|
101
|
+
raise SettingsDefinitionError(
|
|
102
|
+
f"Unsupported list content type {self.content} for {str(self)}"
|
|
103
|
+
)
|
|
104
|
+
# Check content property is not defined if not list
|
|
105
|
+
if "content" in properties and self._type != "list":
|
|
106
|
+
raise SettingsDefinitionError(
|
|
107
|
+
f"Content property is forbidden for parameter {str(self)} with type "
|
|
108
|
+
f"{self._type}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# If the default value is defined, convert to the expected type and validate it
|
|
112
|
+
# is a valid choice.
|
|
113
|
+
if self.default is not None:
|
|
114
|
+
if self._type == "list":
|
|
115
|
+
if not isinstance(self.default, list):
|
|
116
|
+
raise SettingsDefinitionError(
|
|
117
|
+
f"Default value {self.default} for parameter {str(self)} is "
|
|
118
|
+
"not a valid list"
|
|
119
|
+
)
|
|
120
|
+
self.default = [
|
|
121
|
+
self._load_default(self.content, default)
|
|
122
|
+
for default in self.default
|
|
123
|
+
]
|
|
124
|
+
for item in self.default:
|
|
125
|
+
self._validate_choice(item)
|
|
126
|
+
else:
|
|
127
|
+
self.default = self._load_default(self._type, self.default)
|
|
128
|
+
self._validate_choice(self.default)
|
|
129
|
+
|
|
130
|
+
if self.required is None:
|
|
131
|
+
self.required = False
|
|
132
|
+
elif not isinstance(self.required, bool):
|
|
133
|
+
raise SettingsDefinitionError(
|
|
134
|
+
f"Invalid boolean value of {str(self)} required property"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _load_default(self, _type, default):
|
|
138
|
+
"""Verify default has the expected type and convert to advanded type if
|
|
139
|
+
needed."""
|
|
140
|
+
if _type == "path":
|
|
141
|
+
return Path(default)
|
|
142
|
+
elif _type == "uri":
|
|
143
|
+
return urllib.parse.urlparse(default)
|
|
144
|
+
if not isinstance(default, self.EXPECTED_TYPES[_type]):
|
|
145
|
+
raise SettingsDefinitionError(
|
|
146
|
+
f"Default value {default} for parameter {str(self)} has not "
|
|
147
|
+
f"the expected type {_type}"
|
|
148
|
+
)
|
|
149
|
+
return default
|
|
150
|
+
|
|
151
|
+
def _validate_choice(self, default):
|
|
152
|
+
"""Check default is a valid choice or raise SettingsDefinitionError."""
|
|
153
|
+
if self.choices is not None and default not in self.choices:
|
|
154
|
+
raise SettingsDefinitionError(
|
|
155
|
+
f"Default value {default} for parameter {str(self)} is not "
|
|
156
|
+
f"one of possible choices {self.choices}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def __str__(self):
|
|
160
|
+
return f"[{self.section}]>{self.name}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class SettingsSectionDefinition:
|
|
164
|
+
def __init__(self, section: str, parameters: dict):
|
|
165
|
+
self.name = section
|
|
166
|
+
self.parameters = [
|
|
167
|
+
SettingsParameterDefinition(section, parameter, properties)
|
|
168
|
+
for parameter, properties in parameters.items()
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
def has_parameter(self, name: str) -> bool:
|
|
172
|
+
for parameter in self.parameters:
|
|
173
|
+
if parameter.name == name:
|
|
174
|
+
return True
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def parameter(self, name: str):
|
|
178
|
+
for parameter in self.parameters:
|
|
179
|
+
if parameter.name == name:
|
|
180
|
+
return parameter
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class SettingsDefinition:
|
|
185
|
+
def __init__(self, loader: SettingsDefinitionLoader):
|
|
186
|
+
self.sections = [
|
|
187
|
+
SettingsSectionDefinition(section, parameters)
|
|
188
|
+
for section, parameters in loader.content.items()
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
def has_section(self, name: str) -> bool:
|
|
192
|
+
for section in self.sections:
|
|
193
|
+
if section.name == name:
|
|
194
|
+
return True
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
def section(self, name: str):
|
|
198
|
+
for section in self.sections:
|
|
199
|
+
if section.name == name:
|
|
200
|
+
return section
|
|
201
|
+
return None
|
rfl/settings/errors.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Copyright (c) 2023 Rackslab
|
|
2
|
+
#
|
|
3
|
+
# This file is part of RFL.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
from rfl.core.errors import RFLError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SettingsError(RFLError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SettingsDefinitionError(SettingsError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SettingsSiteLoaderError(SettingsError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SettingsOverrideError(SettingsError):
|
|
23
|
+
pass
|
rfl/settings/loaders.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright (c) 2023 Rackslab
|
|
2
|
+
#
|
|
3
|
+
# This file is part of RFL.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
import configparser
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from .errors import SettingsSiteLoaderError
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RuntimeSettingsSiteLoader:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RuntimeSettingsSiteLoaderIni(RuntimeSettingsSiteLoader):
|
|
21
|
+
def __init__(self, raw: str = None, path: Path = None):
|
|
22
|
+
self.content = configparser.ConfigParser(
|
|
23
|
+
interpolation=configparser.ExtendedInterpolation()
|
|
24
|
+
)
|
|
25
|
+
try:
|
|
26
|
+
if raw is not None:
|
|
27
|
+
logger.debug("Loading site settings in INI format from raw value")
|
|
28
|
+
self.content.read_string(raw)
|
|
29
|
+
elif path is not None:
|
|
30
|
+
logger.debug("Loading site settings file %s", path)
|
|
31
|
+
self.content.read(path)
|
|
32
|
+
else:
|
|
33
|
+
raise SettingsSiteLoaderError(
|
|
34
|
+
"Either a raw string value or a path must be given to load site "
|
|
35
|
+
"settings"
|
|
36
|
+
)
|
|
37
|
+
except configparser.Error as error:
|
|
38
|
+
raise SettingsSiteLoaderError(error)
|