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.
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
@@ -90,6 +90,7 @@ def monitoring(hostname: str, port: int, subscribe: str, multipart: bool, use_pi
90
90
  receiver.close(linger=0)
91
91
  context.term()
92
92
 
93
+
93
94
  if __name__ == "__main__":
94
95
  multiprocessing.current_process().name = "Monitoring"
95
96
  monitoring()
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
- LOGGER = logging.getLogger(__name__)
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
- LOGGER.debug(f"Resources have been initialised: {resources = }")
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 ENV_PLATO_LOCAL_SETTINGS
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
- logger = logging.getLogger(__name__)
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
- logger.debug(f"Parsing YAML configuration file {filename}.")
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
- logger.error(exc)
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
- logger.log(5, f"yaml_location in Settings.load(location={location}) is {yaml_location}")
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 = 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 = {}
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
- logger.debug(f"The environment variable {ENV_PLATO_LOCAL_SETTINGS} is not defined.")
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) -> 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"{ENV_PLATO_LOCAL_SETTINGS} environment variable is set."
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 = os.environ.get(ENV_PLATO_LOCAL_SETTINGS)
363
+ location = get_local_settings()
362
364
  if location:
363
365
  settings = Settings.load(filename=location)
364
366
  print(settings)