cgse-common 2024.1.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.
egse/settings.py ADDED
@@ -0,0 +1,379 @@
1
+ """
2
+ The Settings class handles user and configuration settings that are provided in
3
+ a [`YAML`](http://yaml.org) file.
4
+
5
+ The idea is that settings are grouped by components or any arbitrary grouping that makes sense for
6
+ the application or for the user. The Settings class can read from different YAML files. By default,
7
+ settings are loaded from a file called ``settings.yaml``. The default yaml configuration file is
8
+ located in the same directory as this module.
9
+
10
+ The YAML file is read and the configuration parameters for the given group are
11
+ made available as instance variables of the returned class.
12
+
13
+ The intended use is as follows:
14
+
15
+ from egse.settings import Settings
16
+
17
+ dsi_settings = Settings.load("DSI")
18
+
19
+ if (dsi_settings.RMAP_BASE_ADDRESS
20
+ <= addr
21
+ < dsi_settings.RMAP_BASE_ADDRESS + dsi_settings.RMAP_MEMORY_SIZE):
22
+ # do something here
23
+ else:
24
+ raise RMAPError("Attempt to access outside the RMAP memory map.")
25
+
26
+
27
+ The above code reads the settings from the default YAML file for a group called ``DSI``.
28
+ The settings will then be available as variables of the returned class, in this case
29
+ ``dsi_settings``. The returned class is and behaves also like a dictionary, so you can check
30
+ if a configuration parameter is defined like this:
31
+
32
+ if "DSI_FEE_IP_ADDRESS" not in dsi_settings:
33
+ # define the IP address of the DSI
34
+
35
+ The YAML section for the above code looks like this:
36
+
37
+ DSI:
38
+
39
+ # DSI Specific Settings
40
+
41
+ DSI_FEE_IP_ADDRESS 10.33.178.144 # IP address of the DSI EtherSpaceLink interface
42
+ LINK_SPEED: 100 # SpW link speed used for both up- and downlink
43
+
44
+ # RMAP Specific Settings
45
+
46
+ RMAP_BASE_ADDRESS: 0x00000000 # The start of the RMAP memory map managed by the FEE
47
+ RMAP_MEMORY_SIZE: 4096 # The size of the RMAP memory map managed by the FEE
48
+
49
+ When you want to read settings from another YAML file, specify the ``filename=`` keyword.
50
+ If that file is located at a specific location, also use the ``location=`` keyword.
51
+
52
+ my_settings = Settings.load(filename="user.yaml", location="/Users/JohnDoe")
53
+
54
+ The above code will read the complete YAML file, i.e. all the groups into a dictionary.
55
+
56
+ """
57
+
58
+ import logging
59
+ import os
60
+ import pathlib
61
+ import re
62
+
63
+ import yaml # This module is provided by the pip package PyYaml - pip install pyyaml
64
+
65
+ from egse.env import ENV_PLATO_LOCAL_SETTINGS
66
+ from egse.exceptions import FileIsEmptyError
67
+ from egse.system import AttributeDict
68
+ from egse.system import get_caller_info
69
+ from egse.system import ignore_m_warning
70
+ from egse.system import recursive_dict_update
71
+
72
+ logger = logging.getLogger(__name__)
73
+
74
+
75
+ class SettingsError(Exception):
76
+ pass
77
+
78
+
79
+ def is_defined(cls, name):
80
+ return hasattr(cls, name)
81
+
82
+
83
+ def get_attr_value(cls, name, default=None):
84
+ try:
85
+ return getattr(cls, name)
86
+ except AttributeError:
87
+ return default
88
+
89
+
90
+ def set_attr_value(cls, name, value):
91
+ if hasattr(cls, name):
92
+ raise KeyError(f"Overwriting setting {name} with {value}, was {hasattr(cls, name)}")
93
+
94
+
95
+ # Fix the problem: YAML loads 5e-6 as string and not a number
96
+ # https://stackoverflow.com/questions/30458977/yaml-loads-5e-6-as-string-and-not-a-number
97
+
98
+ SAFE_LOADER = yaml.SafeLoader
99
+ SAFE_LOADER.add_implicit_resolver(
100
+ u'tag:yaml.org,2002:float',
101
+ re.compile(u"""^(?:
102
+ [-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+]?[0-9]+)?
103
+ |[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
104
+ |\\.[0-9_]+(?:[eE][-+][0-9]+)?
105
+ |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*
106
+ |[-+]?\\.(?:inf|Inf|INF)
107
+ |\\.(?:nan|NaN|NAN))$""", re.X),
108
+ list(u'-+0123456789.'))
109
+
110
+
111
+ class Settings:
112
+ """
113
+ The Settings class provides a load() method that loads configuration settings for a group
114
+ into a dynamically created class as instance variables.
115
+ """
116
+
117
+ __memoized_yaml = {} # Memoized settings yaml files
118
+
119
+ __profile = False # Used for profiling methods and functions
120
+ __simulation = False # Use simulation mode where applicable and possible
121
+
122
+ LOG_FORMAT_DEFAULT = "%(levelname)s:%(module)s:%(lineno)d:%(message)s"
123
+ LOG_FORMAT_FULL = "%(asctime)23s:%(levelname)8s:%(lineno)5d:%(name)-20s: %(message)s"
124
+ LOG_FORMAT_THREAD = (
125
+ "%(asctime)23s:%(levelname)7s:%(lineno)5d:%(name)-20s(%(threadName)-15s): %(message)s"
126
+ )
127
+ LOG_FORMAT_PROCESS = (
128
+ "%(asctime)23s:%(levelname)7s:%(lineno)5d:%(name)20s.%(funcName)-31s(%(processName)-20s): "
129
+ "%(message)s"
130
+ )
131
+ LOG_FORMAT_DATE = "%d/%m/%Y %H:%M:%S"
132
+
133
+ @classmethod
134
+ def read_configuration_file(cls, filename: str, *, force=False):
135
+ """
136
+ Read the YAML input configuration file. The configuration file is only read
137
+ once and memoized as load optimization.
138
+
139
+ Args:
140
+ filename (str): the fully qualified filename of the YAML file
141
+ force (bool): force reloading the file
142
+
143
+ Returns:
144
+ a dictionary containing all the configuration settings from the YAML file.
145
+ """
146
+ if force or filename not in cls.__memoized_yaml:
147
+
148
+ logger.debug(f"Parsing YAML configuration file {filename}.")
149
+
150
+ with open(filename, "r") as stream:
151
+ try:
152
+ yaml_document = yaml.load(stream, Loader=SAFE_LOADER)
153
+ except yaml.YAMLError as exc:
154
+ logger.error(exc)
155
+ raise SettingsError(f"Error loading YAML document {filename}") from exc
156
+
157
+ cls.__memoized_yaml[filename] = yaml_document
158
+
159
+ return cls.__memoized_yaml[filename]
160
+
161
+ @classmethod
162
+ def get_memoized_locations(cls):
163
+ return cls.__memoized_yaml.keys()
164
+
165
+ @classmethod
166
+ def load(cls, group_name=None, filename="settings.yaml", location=None, *, force=False,
167
+ add_local_settings=True):
168
+ """
169
+ Load the settings for the given group from YAML configuration file.
170
+ When no group is provided, the complete configuration is returned.
171
+
172
+ The default YAML file is 'settings.yaml' and is located in the same directory
173
+ as the settings module.
174
+
175
+ About the ``location`` keyword several options are available.
176
+
177
+ * when no location is given, i.e. ``location=None``, the YAML settings file is searched for
178
+ at the same location as the settings module.
179
+
180
+ * when a relative location is given, the YAML settings file is searched for relative to the
181
+ current working directory.
182
+
183
+ * when an absolute location is given, that location is used 'as is'.
184
+
185
+ Args:
186
+ group_name (str): the name of one of the main groups from the YAML file
187
+ filename (str): the name of the YAML file to read
188
+ location (str): the path to the location of the YAML file
189
+ force (bool): force reloading the file
190
+ add_local_settings (bool): update the Settings with site specific local settings
191
+
192
+ Returns:
193
+ a dynamically created class with the configuration parameters as instance variables.
194
+
195
+ Raises:
196
+ a SettingsError when the group is not defined in the YAML file.
197
+ """
198
+
199
+ _THIS_FILE_LOCATION = pathlib.Path(__file__).resolve().parent
200
+
201
+ if location is None:
202
+
203
+ # Check if the yaml file is located at the location of the caller,
204
+ # if not, use the file that is located where the Settings module is located.
205
+
206
+ caller_dir = get_caller_info(level=2).filename
207
+ caller_dir = pathlib.Path(caller_dir).resolve().parent
208
+
209
+ if (caller_dir / filename).is_file():
210
+ yaml_location = caller_dir
211
+ else:
212
+ yaml_location = _THIS_FILE_LOCATION
213
+ else:
214
+
215
+ # The location was given as an argument
216
+
217
+ yaml_location = pathlib.Path(location).resolve()
218
+
219
+ logger.log(5, f"yaml_location in Settings.load(location={location}) is {yaml_location}")
220
+
221
+ # Load the YAML global document
222
+
223
+ try:
224
+ yaml_document_global = cls.read_configuration_file(
225
+ yaml_location / filename, force=force
226
+ )
227
+ except FileNotFoundError as exc:
228
+ raise SettingsError(
229
+ f"Filename {filename} not found at location {yaml_location}."
230
+ ) from exc
231
+
232
+ # Check if there were any groups defined in the YAML document
233
+
234
+ if not yaml_document_global:
235
+ raise SettingsError(f"Empty YAML document {filename} at {yaml_location}.")
236
+
237
+ # Load the LOCAL settings YAML file
238
+
239
+ if add_local_settings:
240
+ try:
241
+ local_settings_location = os.environ[ENV_PLATO_LOCAL_SETTINGS]
242
+ logger.log(5, f"Using {ENV_PLATO_LOCAL_SETTINGS} to update global settings.")
243
+ try:
244
+ yaml_document_local = cls.read_configuration_file(
245
+ local_settings_location, force=force
246
+ )
247
+ if yaml_document_local is None:
248
+ raise FileIsEmptyError()
249
+ local_settings = AttributeDict(
250
+ {name: value for name, value in yaml_document_local.items()}
251
+ )
252
+ except FileNotFoundError as exc:
253
+ raise SettingsError(
254
+ f"Local settings YAML file '{local_settings_location}' not found. "
255
+ f"Check your environment variable {ENV_PLATO_LOCAL_SETTINGS}."
256
+ ) from exc
257
+ except FileIsEmptyError:
258
+ logger.warning(f"Local settings YAML file '{local_settings_location}' is empty. "
259
+ f"No local settings were loaded.")
260
+ local_settings = {}
261
+ except KeyError:
262
+ logger.debug(f"The environment variable {ENV_PLATO_LOCAL_SETTINGS} is not defined.")
263
+ local_settings = {}
264
+
265
+ if group_name in (None, ""):
266
+ global_settings = AttributeDict(
267
+ {name: value for name, value in yaml_document_global.items()}
268
+ )
269
+ if add_local_settings:
270
+ recursive_dict_update(global_settings, local_settings)
271
+ return global_settings
272
+
273
+ # Check if the requested group is defined in the YAML document
274
+
275
+ if group_name not in yaml_document_global:
276
+ raise SettingsError(
277
+ f"Group name '{group_name}' is not defined in the YAML "
278
+ f"document '{filename}' at '{yaml_location}."
279
+ )
280
+
281
+ # Check if the group has any settings
282
+
283
+ if not yaml_document_global[group_name]:
284
+ raise SettingsError(f"Empty group in YAML document {filename} at {yaml_location}.")
285
+
286
+ group_settings = AttributeDict(
287
+ {name: value for name, value in yaml_document_global[group_name].items()}
288
+ )
289
+
290
+ if add_local_settings and group_name in local_settings:
291
+ recursive_dict_update(group_settings, local_settings[group_name])
292
+
293
+ return group_settings
294
+
295
+ @classmethod
296
+ def to_string(cls):
297
+ """
298
+ Returns a simple string representation of the cached configuration of this Settings class.
299
+ """
300
+ memoized = cls.__memoized_yaml
301
+
302
+ msg = ""
303
+ for key in memoized.keys():
304
+ msg += f"YAML file: {key}\n"
305
+ for field in memoized[key].keys():
306
+ length = 60
307
+ line = str(memoized[key][field])
308
+ trunc = line[:length]
309
+ if len(line) > length:
310
+ trunc += " ..."
311
+ msg += f" {field}: {trunc}\n"
312
+
313
+ return msg
314
+
315
+ @classmethod
316
+ def set_profiling(cls, flag):
317
+ cls.__profile = flag
318
+
319
+ @classmethod
320
+ def profiling(cls):
321
+ return cls.__profile
322
+
323
+ @classmethod
324
+ def set_simulation_mode(cls, flag: bool) -> bool:
325
+ cls.__simulation = flag
326
+
327
+ @classmethod
328
+ def simulation_mode(cls) -> bool:
329
+ return cls.__simulation
330
+
331
+
332
+ ignore_m_warning('egse.settings')
333
+
334
+ if __name__ == "__main__":
335
+
336
+ # We provide convenience to inspect the settings by calling this module directly from Python.
337
+ #
338
+ # python -m egse.settings
339
+ #
340
+ # Use the '--help' option to see the what your choices are.
341
+
342
+ logging.basicConfig(level=20)
343
+
344
+ import argparse
345
+
346
+ parser = argparse.ArgumentParser(
347
+ description=(
348
+ f"Print out the default Settings, updated with local settings if the "
349
+ f"{ENV_PLATO_LOCAL_SETTINGS} environment variable is set."
350
+ ),
351
+ )
352
+ parser.add_argument("--local", action="store_true", help="print only the local settings.")
353
+ args = parser.parse_args()
354
+
355
+ # The following import will activate the pretty printing of the AttributeDict
356
+ # through the __rich__ method.
357
+
358
+ from rich import print
359
+
360
+ if args.local:
361
+ location = os.environ.get(ENV_PLATO_LOCAL_SETTINGS)
362
+ if location:
363
+ settings = Settings.load(filename=location)
364
+ print(settings)
365
+ print(f"Loaded from [purple]{location}.")
366
+ else:
367
+ print("[red]No local settings defined.")
368
+ else:
369
+ settings = Settings.load()
370
+ print(settings)
371
+ print("[blue]Memoized locations:")
372
+ locations = Settings.get_memoized_locations()
373
+ print([str(loc) for loc in locations])
374
+
375
+
376
+ def get_site_id() -> str:
377
+
378
+ site = Settings.load("SITE")
379
+ return site.ID