cgse-common 2023.1.4__py3-none-any.whl → 2024.1.4__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.
- {cgse_common-2023.1.4.dist-info → cgse_common-2024.1.4.dist-info}/METADATA +28 -25
- cgse_common-2024.1.4.dist-info/RECORD +36 -0
- {cgse_common-2023.1.4.dist-info → cgse_common-2024.1.4.dist-info}/WHEEL +1 -1
- cgse_common-2024.1.4.dist-info/entry_points.txt +2 -0
- egse/bits.py +266 -41
- egse/calibration.py +250 -0
- egse/command.py +10 -29
- egse/config.py +17 -12
- egse/control.py +0 -81
- egse/decorators.py +8 -8
- egse/device.py +3 -1
- egse/env.py +411 -106
- egse/hk.py +794 -0
- egse/metrics.py +106 -0
- egse/monitoring.py +1 -0
- egse/resource.py +70 -2
- egse/response.py +101 -0
- egse/settings.py +33 -31
- egse/settings.yaml +0 -973
- egse/setup.py +116 -81
- egse/system.py +33 -14
- cgse_common-2023.1.4.dist-info/RECORD +0 -32
- cgse_common-2023.1.4.dist-info/entry_points.txt +0 -3
egse/metrics.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from prometheus_client import Gauge
|
|
6
|
+
|
|
7
|
+
from egse.hk import TmDictionaryColumns
|
|
8
|
+
from egse.settings import Settings
|
|
9
|
+
from egse.setup import SetupError, load_setup, Setup
|
|
10
|
+
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
SITE_ID = Settings.load("SITE").ID
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def define_metrics(origin: str, dashboard: str = None, use_site: bool = False, setup: Optional[Setup] = None) -> dict:
|
|
16
|
+
""" Creates a metrics dictionary from the telemetry dictionary.
|
|
17
|
+
|
|
18
|
+
Read the metric names and their descriptions from the telemetry dictionary, and create Prometheus gauges based on
|
|
19
|
+
this information.
|
|
20
|
+
|
|
21
|
+
If `dashboard` is not provided, all telemetry parameters for the given origin will be returned.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
origin: Storage mnemonics for the requested metrics
|
|
25
|
+
dashboard: Restrict the metrics selection to those that are defined for the given dashboard. You can select
|
|
26
|
+
all dashboards with `dashboard='*'`.
|
|
27
|
+
use_site: Indicate whether the prefixes of the new HK names are TH-specific
|
|
28
|
+
setup: Setup.
|
|
29
|
+
|
|
30
|
+
Returns: Dictionary with all Prometheus gauges for the given origin and dashboard.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
setup = setup or load_setup()
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
hk_info_table = setup.telemetry.dictionary
|
|
37
|
+
except AttributeError:
|
|
38
|
+
raise SetupError("Version of the telemetry dictionary not specified in the current setup")
|
|
39
|
+
|
|
40
|
+
hk_info_table = hk_info_table.replace(np.nan, "")
|
|
41
|
+
|
|
42
|
+
storage_mnemonic = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values
|
|
43
|
+
hk_names = hk_info_table[TmDictionaryColumns.CORRECT_HK_NAMES].values
|
|
44
|
+
descriptions = hk_info_table[TmDictionaryColumns.DESCRIPTION].values
|
|
45
|
+
mon_screen = hk_info_table[TmDictionaryColumns.DASHBOARD].values
|
|
46
|
+
|
|
47
|
+
condition = storage_mnemonic == origin.upper()
|
|
48
|
+
if dashboard is not None:
|
|
49
|
+
if dashboard == "*":
|
|
50
|
+
extra_condition = mon_screen != ""
|
|
51
|
+
else:
|
|
52
|
+
extra_condition = mon_screen == dashboard.upper()
|
|
53
|
+
condition = np.all((condition, extra_condition), axis=0)
|
|
54
|
+
|
|
55
|
+
selection = np.where(condition)
|
|
56
|
+
|
|
57
|
+
syn_names = hk_names[selection]
|
|
58
|
+
descriptions = descriptions[selection]
|
|
59
|
+
|
|
60
|
+
if not use_site:
|
|
61
|
+
metrics = {}
|
|
62
|
+
|
|
63
|
+
for syn_name, description in zip(syn_names, descriptions):
|
|
64
|
+
try:
|
|
65
|
+
metrics[syn_name] = Gauge(syn_name, description)
|
|
66
|
+
except ValueError:
|
|
67
|
+
LOGGER.warning(f"ValueError for {syn_name}")
|
|
68
|
+
|
|
69
|
+
return metrics
|
|
70
|
+
|
|
71
|
+
th_prefix = f"G{SITE_ID}_"
|
|
72
|
+
|
|
73
|
+
th_syn_names = []
|
|
74
|
+
th_descriptions = []
|
|
75
|
+
for syn_name, description in zip(syn_names, descriptions):
|
|
76
|
+
if syn_name.startswith(th_prefix):
|
|
77
|
+
th_syn_names.append(syn_name)
|
|
78
|
+
th_descriptions.append(description)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
syn_name: Gauge(syn_name, description)
|
|
82
|
+
for syn_name, description in zip(th_syn_names, th_descriptions)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def update_metrics(metrics: dict, updates: dict):
|
|
87
|
+
""" Updates the metrics parameters with the values from the updates dictionary.
|
|
88
|
+
|
|
89
|
+
Only the metrics parameters for which the names are keys in the given updates dict are actually updated. Other
|
|
90
|
+
metrics remain untouched.
|
|
91
|
+
|
|
92
|
+
The functions log a warning when the updates dict contains a name which is not known as a metrics parameter.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
metrics: Metrics dictionary previously defined with the define_metrics function
|
|
96
|
+
updates: Dictionary with key=metrics name and value is the to-be-updated value
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
for metric_name, value in updates.items():
|
|
100
|
+
try:
|
|
101
|
+
if value is None:
|
|
102
|
+
metrics[metric_name].set(float('nan'))
|
|
103
|
+
else:
|
|
104
|
+
metrics[metric_name].set(float(value))
|
|
105
|
+
except KeyError:
|
|
106
|
+
LOGGER.warning(f"Unknown metric name: {metric_name=}")
|
egse/monitoring.py
CHANGED
egse/resource.py
CHANGED
|
@@ -75,20 +75,25 @@ their functionality is fully replaced by this `egse.resource` module.
|
|
|
75
75
|
|
|
76
76
|
"""
|
|
77
77
|
|
|
78
|
+
import errno
|
|
78
79
|
import logging
|
|
79
80
|
import re
|
|
80
81
|
from pathlib import Path
|
|
82
|
+
from pathlib import PurePath
|
|
81
83
|
from typing import Dict
|
|
82
84
|
from typing import List
|
|
83
85
|
from typing import Union
|
|
84
86
|
|
|
87
|
+
from os.path import exists
|
|
88
|
+
from os.path import join
|
|
89
|
+
|
|
85
90
|
from egse.config import find_first_occurrence_of_dir
|
|
86
91
|
from egse.config import find_files
|
|
87
92
|
from egse.exceptions import InternalError
|
|
88
93
|
from egse.plugin import entry_points
|
|
89
94
|
from egse.system import get_package_location
|
|
90
95
|
|
|
91
|
-
|
|
96
|
+
_LOGGER = logging.getLogger(__name__)
|
|
92
97
|
|
|
93
98
|
|
|
94
99
|
class ResourceError(Exception):
|
|
@@ -106,6 +111,8 @@ class NoSuchFileError(ResourceError):
|
|
|
106
111
|
__all__ = [
|
|
107
112
|
"get_resource",
|
|
108
113
|
"get_resource_locations",
|
|
114
|
+
"get_resource_dirs",
|
|
115
|
+
"get_resource_path",
|
|
109
116
|
"add_resource_id",
|
|
110
117
|
"initialise_resources",
|
|
111
118
|
"ResourceError",
|
|
@@ -129,6 +136,8 @@ DEFAULT_RESOURCES = {
|
|
|
129
136
|
"data": "/data",
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
_RESOURCE_DIRS = ["resources", "icons", "images", "styles", "data"]
|
|
140
|
+
|
|
132
141
|
resources = {}
|
|
133
142
|
|
|
134
143
|
|
|
@@ -180,6 +189,65 @@ def get_resource_locations() -> Dict[str, List[Path]]:
|
|
|
180
189
|
return resources.copy()
|
|
181
190
|
|
|
182
191
|
|
|
192
|
+
def get_resource_dirs(root_dir: Union[str, PurePath]) -> List[Path]:
|
|
193
|
+
"""
|
|
194
|
+
Define directories that contain resources like images, icons, and data files.
|
|
195
|
+
|
|
196
|
+
Resource directories can have the following names: `resources`, `data`, `icons`, or `images`.
|
|
197
|
+
This function checks if any of the resource directories exist in the `root_dir` that is given as an argument.
|
|
198
|
+
|
|
199
|
+
For all existing directories the function returns the absolute path.
|
|
200
|
+
|
|
201
|
+
If the argument root_dir is None, an empty list will be returned and a warning message will be issued.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
root_dir (str): the directory to search for resource folders
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
a list of absolute Paths.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
if root_dir is None:
|
|
211
|
+
_LOGGER.warning("The argument root_dir can not be None, an empty list is returned.")
|
|
212
|
+
return []
|
|
213
|
+
|
|
214
|
+
root_dir = Path(root_dir).resolve()
|
|
215
|
+
if not root_dir.is_dir():
|
|
216
|
+
root_dir = root_dir.parent
|
|
217
|
+
|
|
218
|
+
result = []
|
|
219
|
+
for dir_ in _RESOURCE_DIRS:
|
|
220
|
+
if (root_dir / dir_).is_dir():
|
|
221
|
+
result.append(Path(root_dir, dir_).resolve())
|
|
222
|
+
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_resource_path(name: str, resource_root_dir: Union[str, PurePath] = None) -> PurePath:
|
|
227
|
+
"""
|
|
228
|
+
Searches for a data file (resource) with the given name.
|
|
229
|
+
|
|
230
|
+
When `resource_root_dir` is not given, the search for resources will start at the root
|
|
231
|
+
folder of the project (using the function `get_common_egse_root()`). Any other root
|
|
232
|
+
directory can be given, e.g. if you want to start the search from the location of your
|
|
233
|
+
source code file, use `Path(__file__).parent` as the `resource_root_dir` argument.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
name (str): the name of the resource that is requested
|
|
237
|
+
resource_root_dir (str): the root directory w_HERE the search for resources should be started
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
the absolute path of the data file with the given name. The first name that matches
|
|
241
|
+
is returned. If no file with the given name or path exists, a FileNotFoundError is raised.
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
for resource_dir in get_resource_dirs(resource_root_dir):
|
|
245
|
+
resource_path = join(resource_dir, name)
|
|
246
|
+
if exists(resource_path):
|
|
247
|
+
return Path(resource_path).absolute()
|
|
248
|
+
raise FileNotFoundError(errno.ENOENT, f"Could not locate resource '{name}'")
|
|
249
|
+
|
|
250
|
+
|
|
183
251
|
def initialise_resources(root: Union[Path, str] = Path(__file__).parent):
|
|
184
252
|
"""
|
|
185
253
|
Initialise the default resources and any resource published by a package entry point.
|
|
@@ -215,7 +283,7 @@ def initialise_resources(root: Union[Path, str] = Path(__file__).parent):
|
|
|
215
283
|
if location not in x:
|
|
216
284
|
x.append(location)
|
|
217
285
|
|
|
218
|
-
|
|
286
|
+
_LOGGER.debug(f"Resources have been initialised: {resources = }")
|
|
219
287
|
|
|
220
288
|
|
|
221
289
|
def print_resources():
|
egse/response.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides functionality to handle responses from the control servers.
|
|
3
|
+
"""
|
|
4
|
+
__all__ = [
|
|
5
|
+
"Success",
|
|
6
|
+
"Failure",
|
|
7
|
+
"Message",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import rich.repr
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Response:
|
|
16
|
+
"""
|
|
17
|
+
Base class for any reply or response between client-server communication.
|
|
18
|
+
|
|
19
|
+
The idea is that the response is encapsulated in one of the subclasses depending
|
|
20
|
+
on the type of response.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str):
|
|
24
|
+
self.message = message
|
|
25
|
+
|
|
26
|
+
def __str__(self):
|
|
27
|
+
return self.message
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def successful(self):
|
|
31
|
+
"""Returns True if the Response is not an Exception."""
|
|
32
|
+
return not isinstance(self, Exception)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@rich.repr.auto
|
|
36
|
+
class Failure(Response, Exception):
|
|
37
|
+
"""
|
|
38
|
+
A failure response indicating something went wrong at the other side.
|
|
39
|
+
|
|
40
|
+
This class is used to encapsulate an Exception that was caught and needs to be
|
|
41
|
+
passed to the client. So, the intended use is like this:
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# perform some useful action that might raise an Exception
|
|
45
|
+
except SomeException as exc:
|
|
46
|
+
return Failure("Our action failed", exc)
|
|
47
|
+
|
|
48
|
+
The client can inspect the Exception that was originally raised, in this case `SomeException`
|
|
49
|
+
with the `cause` variable.
|
|
50
|
+
|
|
51
|
+
Since a Failure is also an Exception, the property `successful` will return False.
|
|
52
|
+
So, the calling method can test for this easily.
|
|
53
|
+
|
|
54
|
+
rc: Response = function_that_returns_a_response()
|
|
55
|
+
|
|
56
|
+
if not rc.successful:
|
|
57
|
+
# handle the failure
|
|
58
|
+
else:
|
|
59
|
+
# handle success
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, message: str, cause: Exception = None):
|
|
64
|
+
msg = f"{message}: {cause}" if cause is not None else message
|
|
65
|
+
super().__init__(msg)
|
|
66
|
+
self.cause = cause
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@rich.repr.auto
|
|
70
|
+
class Success(Response):
|
|
71
|
+
"""
|
|
72
|
+
A success response for the client.
|
|
73
|
+
|
|
74
|
+
The return code from any action or function that needs to be returned to the
|
|
75
|
+
client shall be added.
|
|
76
|
+
|
|
77
|
+
Since `Success` doesn't inherit from `Exception`, the property `successful` will return True.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, message: str, return_code: Any = None):
|
|
81
|
+
msg = f"{message}: {return_code}" if return_code is not None else message
|
|
82
|
+
super().__init__(msg)
|
|
83
|
+
self.return_code = return_code
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def response(self):
|
|
87
|
+
return self.return_code
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@rich.repr.auto
|
|
91
|
+
class Message(Response):
|
|
92
|
+
"""
|
|
93
|
+
A message response from the client.
|
|
94
|
+
|
|
95
|
+
Send a Message when there is no Failure, but also no return code. This is the alternative of
|
|
96
|
+
returning a None.
|
|
97
|
+
|
|
98
|
+
Message returns True for the property successful since it doesn't inherit from Exception.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
pass
|
egse/settings.py
CHANGED
|
@@ -62,14 +62,15 @@ import re
|
|
|
62
62
|
|
|
63
63
|
import yaml # This module is provided by the pip package PyYaml - pip install pyyaml
|
|
64
64
|
|
|
65
|
-
from egse.env import
|
|
65
|
+
from egse.env import get_local_settings
|
|
66
|
+
from egse.env import get_local_settings_env_name
|
|
66
67
|
from egse.exceptions import FileIsEmptyError
|
|
67
68
|
from egse.system import AttributeDict
|
|
68
69
|
from egse.system import get_caller_info
|
|
69
70
|
from egse.system import ignore_m_warning
|
|
70
71
|
from egse.system import recursive_dict_update
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
_LOGGER = logging.getLogger(__name__)
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
class SettingsError(Exception):
|
|
@@ -145,13 +146,13 @@ class Settings:
|
|
|
145
146
|
"""
|
|
146
147
|
if force or filename not in cls.__memoized_yaml:
|
|
147
148
|
|
|
148
|
-
|
|
149
|
+
_LOGGER.debug(f"Parsing YAML configuration file {filename}.")
|
|
149
150
|
|
|
150
151
|
with open(filename, "r") as stream:
|
|
151
152
|
try:
|
|
152
153
|
yaml_document = yaml.load(stream, Loader=SAFE_LOADER)
|
|
153
154
|
except yaml.YAMLError as exc:
|
|
154
|
-
|
|
155
|
+
_LOGGER.error(exc)
|
|
155
156
|
raise SettingsError(f"Error loading YAML document {filename}") from exc
|
|
156
157
|
|
|
157
158
|
cls.__memoized_yaml[filename] = yaml_document
|
|
@@ -216,13 +217,13 @@ class Settings:
|
|
|
216
217
|
|
|
217
218
|
yaml_location = pathlib.Path(location).resolve()
|
|
218
219
|
|
|
219
|
-
|
|
220
|
+
_LOGGER.debug(f"yaml_location in Settings.load(location={location}) is {yaml_location}")
|
|
220
221
|
|
|
221
222
|
# Load the YAML global document
|
|
222
223
|
|
|
223
224
|
try:
|
|
224
225
|
yaml_document_global = cls.read_configuration_file(
|
|
225
|
-
yaml_location / filename, force=force
|
|
226
|
+
str(yaml_location / filename), force=force
|
|
226
227
|
)
|
|
227
228
|
except FileNotFoundError as exc:
|
|
228
229
|
raise SettingsError(
|
|
@@ -236,31 +237,32 @@ class Settings:
|
|
|
236
237
|
|
|
237
238
|
# Load the LOCAL settings YAML file
|
|
238
239
|
|
|
240
|
+
local_settings = {}
|
|
241
|
+
|
|
239
242
|
if add_local_settings:
|
|
240
243
|
try:
|
|
241
|
-
local_settings_location =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
244
|
+
local_settings_location = get_local_settings()
|
|
245
|
+
if local_settings_location:
|
|
246
|
+
_LOGGER.debug(f"Using {local_settings_location} to update global settings.")
|
|
247
|
+
try:
|
|
248
|
+
yaml_document_local = cls.read_configuration_file(
|
|
249
|
+
local_settings_location, force=force
|
|
250
|
+
)
|
|
251
|
+
if yaml_document_local is None:
|
|
252
|
+
raise FileIsEmptyError()
|
|
253
|
+
local_settings = AttributeDict(
|
|
254
|
+
{name: value for name, value in yaml_document_local.items()}
|
|
255
|
+
)
|
|
256
|
+
except FileNotFoundError as exc:
|
|
257
|
+
raise SettingsError(
|
|
258
|
+
f"Local settings YAML file '{local_settings_location}' not found. "
|
|
259
|
+
f"Check your environment variable {get_local_settings_env_name()}."
|
|
260
|
+
) from exc
|
|
261
|
+
except FileIsEmptyError:
|
|
262
|
+
_LOGGER.warning(f"Local settings YAML file '{local_settings_location}' is empty. "
|
|
263
|
+
f"No local settings were loaded.")
|
|
261
264
|
except KeyError:
|
|
262
|
-
|
|
263
|
-
local_settings = {}
|
|
265
|
+
_LOGGER.debug(f"The environment variable {get_local_settings_env_name()} is not defined.")
|
|
264
266
|
|
|
265
267
|
if group_name in (None, ""):
|
|
266
268
|
global_settings = AttributeDict(
|
|
@@ -321,7 +323,7 @@ class Settings:
|
|
|
321
323
|
return cls.__profile
|
|
322
324
|
|
|
323
325
|
@classmethod
|
|
324
|
-
def set_simulation_mode(cls, flag: bool)
|
|
326
|
+
def set_simulation_mode(cls, flag: bool):
|
|
325
327
|
cls.__simulation = flag
|
|
326
328
|
|
|
327
329
|
@classmethod
|
|
@@ -346,7 +348,7 @@ if __name__ == "__main__":
|
|
|
346
348
|
parser = argparse.ArgumentParser(
|
|
347
349
|
description=(
|
|
348
350
|
f"Print out the default Settings, updated with local settings if the "
|
|
349
|
-
f"{
|
|
351
|
+
f"{get_local_settings_env_name()} environment variable is set."
|
|
350
352
|
),
|
|
351
353
|
)
|
|
352
354
|
parser.add_argument("--local", action="store_true", help="print only the local settings.")
|
|
@@ -358,7 +360,7 @@ if __name__ == "__main__":
|
|
|
358
360
|
from rich import print
|
|
359
361
|
|
|
360
362
|
if args.local:
|
|
361
|
-
location =
|
|
363
|
+
location = get_local_settings()
|
|
362
364
|
if location:
|
|
363
365
|
settings = Settings.load(filename=location)
|
|
364
366
|
print(settings)
|