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.
- cgse_common-2024.1.1.dist-info/METADATA +64 -0
- cgse_common-2024.1.1.dist-info/RECORD +32 -0
- cgse_common-2024.1.1.dist-info/WHEEL +4 -0
- cgse_common-2024.1.1.dist-info/entry_points.txt +2 -0
- egse/bits.py +318 -0
- egse/command.py +699 -0
- egse/config.py +289 -0
- egse/control.py +429 -0
- egse/decorators.py +419 -0
- egse/device.py +269 -0
- egse/env.py +279 -0
- egse/exceptions.py +88 -0
- egse/mixin.py +464 -0
- egse/monitoring.py +96 -0
- egse/observer.py +41 -0
- egse/obsid.py +161 -0
- egse/persistence.py +58 -0
- egse/plugin.py +97 -0
- egse/process.py +460 -0
- egse/protocol.py +607 -0
- egse/proxy.py +522 -0
- egse/reload.py +122 -0
- egse/resource.py +438 -0
- egse/services.py +212 -0
- egse/services.yaml +51 -0
- egse/settings.py +379 -0
- egse/settings.yaml +981 -0
- egse/setup.py +1180 -0
- egse/state.py +173 -0
- egse/system.py +1499 -0
- egse/version.py +178 -0
- egse/zmq_ser.py +69 -0
egse/setup.py
ADDED
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the Setup, which contains the complete configuration information for a test.
|
|
3
|
+
|
|
4
|
+
The Setup class contains all configuration items that are specific for a test or observation
|
|
5
|
+
and is normally (during nominal operation/testing) loaded automatically from the configuration
|
|
6
|
+
manager. The Setup includes type and identification of hardware that is used, calibration files,
|
|
7
|
+
software versions, reference frames and coordinate systems that link positions of alignment
|
|
8
|
+
equipment, conversion functions for temperature sensors, etc.
|
|
9
|
+
|
|
10
|
+
The configuration information that is in the Setup can be navigated in two different ways. First,
|
|
11
|
+
the Setup is a dictionary, so all information can be accessed by keys as in the following example.
|
|
12
|
+
|
|
13
|
+
>>> setup = Setup({"gse": {"hexapod": {"ID": 42, "calibration": [0,1,2,3,4,5]}}})
|
|
14
|
+
>>> setup["gse"]["hexapod"]["ID"]
|
|
15
|
+
42
|
|
16
|
+
|
|
17
|
+
Second, each of the _keys_ is also available as an attribute of the Setup and that make it
|
|
18
|
+
possible to navigate the Setup with dot-notation:
|
|
19
|
+
|
|
20
|
+
>>> id = setup.gse.hexapod.ID
|
|
21
|
+
|
|
22
|
+
In the above example you can see how to navigate from the setup to a device like the PUNA Hexapod.
|
|
23
|
+
The Hexapod device is connected to the control server and accepts commands as usual. If you want to
|
|
24
|
+
know which keys you can use to navigate the Setup, use the `keys()` method.
|
|
25
|
+
|
|
26
|
+
>>> setup.gse.hexapod.keys()
|
|
27
|
+
dict_keys(['ID', 'calibration'])
|
|
28
|
+
>>> setup.gse.hexapod.calibration
|
|
29
|
+
[0, 1, 2, 3, 4, 5]
|
|
30
|
+
|
|
31
|
+
To get a full printout of the Setup, you can use the `pretty_str()` method. Be careful, because
|
|
32
|
+
this can print out a lot of information when a full Setup is loaded.
|
|
33
|
+
|
|
34
|
+
>>> print(setup)
|
|
35
|
+
gse:
|
|
36
|
+
hexapod:
|
|
37
|
+
ID: 42
|
|
38
|
+
calibration: [0, 1, 2, 3, 4, 5]
|
|
39
|
+
<BLANKLINE>
|
|
40
|
+
|
|
41
|
+
### Special Values
|
|
42
|
+
|
|
43
|
+
Some of the information in the Setup is interpreted in a special way, i.e. some values are
|
|
44
|
+
processed before returning. Examples are the device classes and calibration/data files. The
|
|
45
|
+
following values are treated special if they start with:
|
|
46
|
+
|
|
47
|
+
* `class//`: the class in instantiated and the object is returned
|
|
48
|
+
* `csv//`: the CSV file is loaded and a numpy array is returned
|
|
49
|
+
* `yaml//`: the YAML file is loaded and a dictionary is returned
|
|
50
|
+
* `enum//`: the enumeration is created dynamically and the object is returned
|
|
51
|
+
|
|
52
|
+
#### Device Classes
|
|
53
|
+
|
|
54
|
+
Most of the hardware components in the Setup will have a `device` key that defines the class for
|
|
55
|
+
the device controller. The `device` keys have a value that starts with `class//` and it will
|
|
56
|
+
return the device object. As an example, the following defines the Hexapod device:
|
|
57
|
+
|
|
58
|
+
>>> setup = Setup({
|
|
59
|
+
... "gse": {
|
|
60
|
+
... "hexapod": {"ID": 42, "device": "class//egse.hexapod.symetrie.puna.PunaSimulator"}
|
|
61
|
+
... }
|
|
62
|
+
... })
|
|
63
|
+
>>> setup.gse.hexapod.device.is_homing_done()
|
|
64
|
+
False
|
|
65
|
+
>>> setup.gse.hexapod.device.info() # doctest: +ELLIPSIS
|
|
66
|
+
'Info about the PunaSimulator...
|
|
67
|
+
|
|
68
|
+
In the above example you see that we can call the `is_homing_done()` and `info()` methodes
|
|
69
|
+
directly on the device by navigating the Setup. It would however be better (more performant) to
|
|
70
|
+
put the device object in a variable and work with that variable:
|
|
71
|
+
|
|
72
|
+
>>> hexapod = setup.gse.hexapod.device
|
|
73
|
+
>>> _ = hexapod.homing()
|
|
74
|
+
>>> hexapod.is_homing_done()
|
|
75
|
+
True
|
|
76
|
+
>>> _ = hexapod.get_user_positions()
|
|
77
|
+
|
|
78
|
+
If you need, for some reason, to have access to the actual raw value of the hexapod device key,
|
|
79
|
+
use the `get_raw_value()` method:
|
|
80
|
+
|
|
81
|
+
>>> setup.gse.hexapod.get_raw_value("device") # doctest: +ELLIPSIS
|
|
82
|
+
<egse.hexapod.symetrie.puna.PunaSimulator object at ...
|
|
83
|
+
|
|
84
|
+
#### Data Files
|
|
85
|
+
|
|
86
|
+
Some information is too large to add to the Setup as such and should be loaded from a data file.
|
|
87
|
+
Examples are calibration files, flat-fields, temperature conversion curves, etc.
|
|
88
|
+
|
|
89
|
+
The Setup will automatically load the file when you access a key that contains a value that
|
|
90
|
+
starts with `csv//` or `yaml//`.
|
|
91
|
+
|
|
92
|
+
>>> setup = Setup({
|
|
93
|
+
... "instrument": {"coeff": "csv//cal_coeff_1234.csv"}
|
|
94
|
+
... })
|
|
95
|
+
>>> setup.instrument.coeff[0, 4]
|
|
96
|
+
5.0
|
|
97
|
+
|
|
98
|
+
Note: the resource location is always relative to the path defined by the PLATO_CONF_DATA_LOCATION
|
|
99
|
+
environment variable.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
from __future__ import annotations
|
|
103
|
+
|
|
104
|
+
import enum
|
|
105
|
+
import importlib
|
|
106
|
+
import logging
|
|
107
|
+
import os
|
|
108
|
+
import re
|
|
109
|
+
import textwrap
|
|
110
|
+
from functools import lru_cache
|
|
111
|
+
from pathlib import Path
|
|
112
|
+
from typing import Any
|
|
113
|
+
from typing import Optional
|
|
114
|
+
from typing import Union
|
|
115
|
+
|
|
116
|
+
import rich
|
|
117
|
+
import yaml
|
|
118
|
+
from rich.tree import Tree
|
|
119
|
+
|
|
120
|
+
from egse.control import Failure
|
|
121
|
+
from egse.system import format_datetime
|
|
122
|
+
from egse.system import sanity_check
|
|
123
|
+
from egse.system import walk_dict_tree
|
|
124
|
+
|
|
125
|
+
MODULE_LOGGER = logging.getLogger(__name__)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class SetupError(Exception):
|
|
129
|
+
""" A setup-specific error."""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _load_class(class_name: str):
|
|
134
|
+
"""Find and returns a class based on the fully qualified name.
|
|
135
|
+
|
|
136
|
+
A class name can be preceded with the string `class//`. This is used in YAML
|
|
137
|
+
files where the class is then instantiated on load.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
class_name (str): a fully qualified name for the class
|
|
141
|
+
"""
|
|
142
|
+
if class_name.startswith("class//"):
|
|
143
|
+
class_name = class_name[7:]
|
|
144
|
+
elif class_name.startswith("factory//"):
|
|
145
|
+
class_name = class_name[9:]
|
|
146
|
+
|
|
147
|
+
module_name, class_name = class_name.rsplit(".", 1)
|
|
148
|
+
module = importlib.import_module(module_name)
|
|
149
|
+
return getattr(module, class_name)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _load_csv(resource_name: str):
|
|
153
|
+
"""Find and return the content of a CSV file."""
|
|
154
|
+
from numpy import genfromtxt # FIXME: use CSV standard module
|
|
155
|
+
|
|
156
|
+
parts = resource_name[5:].rsplit("/", 1)
|
|
157
|
+
[in_dir, fn] = parts if len(parts) > 1 else [None, parts[0]]
|
|
158
|
+
conf_location = os.environ['PLATO_CONF_DATA_LOCATION']
|
|
159
|
+
try:
|
|
160
|
+
csv_location = Path(conf_location) / in_dir / fn
|
|
161
|
+
content = genfromtxt(csv_location, delimiter=",", skip_header=1)
|
|
162
|
+
except TypeError as exc:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"Couldn't load resource '{resource_name}' from default {conf_location=}") from exc
|
|
165
|
+
return content
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _load_int_enum(enum_name: str, enum_content):
|
|
169
|
+
""" Dynamically build (and return) and IntEnum.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
- enum_name: Enumeration name (potentially prepended with "int_enum//").
|
|
173
|
+
- enum_content: Content of the enumeration, as read from the setup.
|
|
174
|
+
"""
|
|
175
|
+
if enum_name.startswith("int_enum//"):
|
|
176
|
+
enum_name = enum_name[10:]
|
|
177
|
+
|
|
178
|
+
definition = {}
|
|
179
|
+
for side_name, side_definition in enum_content.items():
|
|
180
|
+
|
|
181
|
+
if "alias" in side_definition:
|
|
182
|
+
aliases = side_definition["alias"]
|
|
183
|
+
else:
|
|
184
|
+
aliases = []
|
|
185
|
+
value = side_definition["value"]
|
|
186
|
+
|
|
187
|
+
definition[side_name] = value
|
|
188
|
+
|
|
189
|
+
for alias in aliases:
|
|
190
|
+
definition[alias] = value
|
|
191
|
+
return enum.IntEnum(enum_name, definition)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _load_yaml(resource_name: str):
|
|
195
|
+
"""Find and return the content of a YAML file."""
|
|
196
|
+
from egse.settings import Settings
|
|
197
|
+
from egse.settings import SettingsError
|
|
198
|
+
|
|
199
|
+
parts = resource_name[6:].rsplit("/", 1)
|
|
200
|
+
[in_dir, fn] = parts if len(parts) > 1 else [None, parts[0]]
|
|
201
|
+
conf_location = os.environ['PLATO_CONF_DATA_LOCATION']
|
|
202
|
+
try:
|
|
203
|
+
yaml_location = Path(conf_location) / in_dir / fn
|
|
204
|
+
content = NavigableDict(Settings.load(filename=yaml_location, add_local_settings=False))
|
|
205
|
+
except (TypeError, SettingsError) as exc:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"Couldn't load resource '{resource_name}' from default {conf_location=}") from exc
|
|
208
|
+
return content
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _load_pandas(resource_name: str, separator: str):
|
|
212
|
+
""" Find and return the content of the given files as a pandas DataFrame object.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
- resource_name: Filename, preceded by "pandas//".
|
|
216
|
+
- separator: Column separator.
|
|
217
|
+
"""
|
|
218
|
+
import pandas
|
|
219
|
+
|
|
220
|
+
parts = resource_name[8:].rsplit("/", 1)
|
|
221
|
+
[in_dir, fn] = parts if len(parts) > 1 else [None, parts[0]]
|
|
222
|
+
conf_location = os.environ['PLATO_CONF_DATA_LOCATION']
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
pandas_file_location = Path(conf_location) / in_dir / fn
|
|
226
|
+
return pandas.read_csv(pandas_file_location, sep=separator)
|
|
227
|
+
except TypeError as exc:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"Couldn't load resource '{resource_name}' from default {conf_location=}") from exc
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _get_attribute(self, name, default):
|
|
233
|
+
try:
|
|
234
|
+
attr = object.__getattribute__(self, name)
|
|
235
|
+
except AttributeError:
|
|
236
|
+
attr = default
|
|
237
|
+
return attr
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _parse_filename_for_setup_id(filename: str):
|
|
241
|
+
"""Returns the setup_id from the filename, or None when no match was found."""
|
|
242
|
+
|
|
243
|
+
match = re.search(r"SETUP_([^_]+)_(\d+)", filename)
|
|
244
|
+
|
|
245
|
+
# TypeError when match is None
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
return match[2] # match[2] is setup_id
|
|
249
|
+
except (IndexError, TypeError) as exc:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def get_last_setup_id_file_path(site_id: str = None) -> Path:
|
|
254
|
+
"""
|
|
255
|
+
Return the fully expanded file path of the file containing the last loaded Setup in the configuration manager.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
site_id: The SITE identifier
|
|
259
|
+
|
|
260
|
+
"""
|
|
261
|
+
from egse.env import get_data_storage_location
|
|
262
|
+
from egse.settings import Settings
|
|
263
|
+
|
|
264
|
+
site_id = site_id or Settings.load("SITE").ID
|
|
265
|
+
location = get_data_storage_location(site_id=site_id)
|
|
266
|
+
|
|
267
|
+
return Path(location).expanduser().resolve() / "last_setup_id.txt"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def load_last_setup_id(site_id: str = None) -> int:
|
|
271
|
+
"""
|
|
272
|
+
Returns the ID of the last Setup that was used by the configuration manager.
|
|
273
|
+
The file shall only contain the Setup ID which must be an integer on the first line of the file.
|
|
274
|
+
If no such ID can be found, the Setup ID = 0 will be returned.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
site_id: The SITE identifier
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
last_setup_id_file_path = get_last_setup_id_file_path(site_id=site_id)
|
|
281
|
+
try:
|
|
282
|
+
with last_setup_id_file_path.open('r') as fd:
|
|
283
|
+
setup_id = int(fd.read().strip())
|
|
284
|
+
except FileNotFoundError:
|
|
285
|
+
setup_id = 0
|
|
286
|
+
save_last_setup_id(setup_id)
|
|
287
|
+
|
|
288
|
+
return setup_id
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def save_last_setup_id(setup_id: int | str, site_id: str = None):
|
|
292
|
+
"""
|
|
293
|
+
Makes the given Setup ID persistent, so it can be restored upon the next startup.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
setup_id: The Setup identifier to be saved
|
|
297
|
+
site_id: The SITE identifier
|
|
298
|
+
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
last_setup_id_file_path = get_last_setup_id_file_path(site_id=site_id)
|
|
302
|
+
with last_setup_id_file_path.open('w') as fd:
|
|
303
|
+
fd.write(f"{int(setup_id):d}")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class NavigableDict(dict):
|
|
307
|
+
"""
|
|
308
|
+
A NavigableDict is a dictionary where all keys in the original dictionary are also accessible
|
|
309
|
+
as attributes to the class instance. So, if the original dictionary (setup) has a key
|
|
310
|
+
"site_id" which is accessible as `setup['site_id']`, it will also be accessible as
|
|
311
|
+
`setup.site_id`.
|
|
312
|
+
|
|
313
|
+
Examples:
|
|
314
|
+
>>> setup = NavigableDict({'site_id': 'KU Leuven', 'version': "0.1.0"})
|
|
315
|
+
>>> assert setup['site_id'] == setup.site_id
|
|
316
|
+
>>> assert setup['version'] == setup.version
|
|
317
|
+
|
|
318
|
+
.. note::
|
|
319
|
+
We always want **all** keys to be accessible as attributes, or none. That means all
|
|
320
|
+
keys of the original dictionary shall be of type `str`.
|
|
321
|
+
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def __init__(self, head: dict = None):
|
|
325
|
+
"""
|
|
326
|
+
Args:
|
|
327
|
+
head (dict): the original dictionary
|
|
328
|
+
"""
|
|
329
|
+
head = head or {}
|
|
330
|
+
super().__init__(head)
|
|
331
|
+
self.__dict__["_memoized"] = {}
|
|
332
|
+
|
|
333
|
+
# By agreement, we only want the keys to be set as attributes if all keys are strings.
|
|
334
|
+
# That way we enforce that always all keys are navigable, or none.
|
|
335
|
+
|
|
336
|
+
if any(True for k in head.keys() if not isinstance(k, str)):
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
for key, value in head.items():
|
|
340
|
+
if isinstance(value, dict):
|
|
341
|
+
setattr(self, key, NavigableDict(head.__getitem__(key)))
|
|
342
|
+
else:
|
|
343
|
+
setattr(self, key, head.__getitem__(key))
|
|
344
|
+
|
|
345
|
+
def add(self, key: str, value: Any):
|
|
346
|
+
"""Set a value for the given key.
|
|
347
|
+
|
|
348
|
+
If the value is a dictionary, it will be converted into a NavigableDict and the keys
|
|
349
|
+
will become available as attributes provided that all the keys are strings.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
key (str): the name of the key / attribute to access the value
|
|
353
|
+
value (Any): the value to assign to the key
|
|
354
|
+
"""
|
|
355
|
+
if isinstance(value, dict) and not isinstance(value, NavigableDict):
|
|
356
|
+
value = NavigableDict(value)
|
|
357
|
+
setattr(self, key, value)
|
|
358
|
+
|
|
359
|
+
def clear(self) -> None:
|
|
360
|
+
for key in list(self.keys()):
|
|
361
|
+
self.__delitem__(key)
|
|
362
|
+
|
|
363
|
+
def __repr__(self):
|
|
364
|
+
return f"{self.__class__.__name__}({super()!r})"
|
|
365
|
+
|
|
366
|
+
def __delitem__(self, key):
|
|
367
|
+
dict.__delitem__(self, key)
|
|
368
|
+
object.__delattr__(self, key)
|
|
369
|
+
|
|
370
|
+
def __setattr__(self, key, value):
|
|
371
|
+
# MODULE_LOGGER.info(f"called __setattr__({self!r}, {key}, {value})")
|
|
372
|
+
if isinstance(value, dict) and not isinstance(value, NavigableDict):
|
|
373
|
+
value = NavigableDict(value)
|
|
374
|
+
self.__dict__[key] = value
|
|
375
|
+
super().__setitem__(key, value)
|
|
376
|
+
try:
|
|
377
|
+
del self.__dict__["_memoized"][key]
|
|
378
|
+
except KeyError:
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
def __getattribute__(self, key):
|
|
382
|
+
# MODULE_LOGGER.info(f"called __getattribute__({key})")
|
|
383
|
+
value = object.__getattribute__(self, key)
|
|
384
|
+
if isinstance(value, str) and value.startswith("class//"):
|
|
385
|
+
try:
|
|
386
|
+
dev_args = object.__getattribute__(self, 'device_args')
|
|
387
|
+
except AttributeError:
|
|
388
|
+
dev_args = ()
|
|
389
|
+
return _load_class(value)(*dev_args)
|
|
390
|
+
if isinstance(value, str) and value.startswith("factory//"):
|
|
391
|
+
factory_args = _get_attribute(self, f'{key}_args', {})
|
|
392
|
+
return _load_class(value)().create(**factory_args)
|
|
393
|
+
if isinstance(value, str) and value.startswith("int_enum//"):
|
|
394
|
+
content = object.__getattribute__(self, "content")
|
|
395
|
+
return _load_int_enum(value, content)
|
|
396
|
+
if isinstance(value, str) and value.startswith("csv//"):
|
|
397
|
+
if key in self.__dict__["_memoized"]:
|
|
398
|
+
return self.__dict__["_memoized"][key]
|
|
399
|
+
content = _load_csv(value)
|
|
400
|
+
self.__dict__["_memoized"][key] = content
|
|
401
|
+
return content
|
|
402
|
+
if isinstance(value, str) and value.startswith("yaml//"):
|
|
403
|
+
if key in self.__dict__["_memoized"]:
|
|
404
|
+
return self.__dict__["_memoized"][key]
|
|
405
|
+
content = _load_yaml(value)
|
|
406
|
+
self.__dict__["_memoized"][key] = content
|
|
407
|
+
return content
|
|
408
|
+
if isinstance(value, str) and value.startswith("pandas//"):
|
|
409
|
+
separator = object.__getattribute__(self, 'separator')
|
|
410
|
+
return _load_pandas(value, separator)
|
|
411
|
+
else:
|
|
412
|
+
return value
|
|
413
|
+
|
|
414
|
+
def __delattr__(self, item):
|
|
415
|
+
# MODULE_LOGGER.info(f"called __delattr__({self!r}, {item})")
|
|
416
|
+
object.__delattr__(self, item)
|
|
417
|
+
dict.__delitem__(self, item)
|
|
418
|
+
|
|
419
|
+
def __setitem__(self, key, value):
|
|
420
|
+
# MODULE_LOGGER.info(f"called __setitem__({self!r}, {key}, {value})")
|
|
421
|
+
if isinstance(value, dict) and not isinstance(value, NavigableDict):
|
|
422
|
+
value = NavigableDict(value)
|
|
423
|
+
super().__setitem__(key, value)
|
|
424
|
+
self.__dict__[key] = value
|
|
425
|
+
try:
|
|
426
|
+
del self.__dict__["_memoized"][key]
|
|
427
|
+
except KeyError:
|
|
428
|
+
pass
|
|
429
|
+
|
|
430
|
+
def __getitem__(self, key):
|
|
431
|
+
# MODULE_LOGGER.info(f"called __getitem__({self!r}, {key})")
|
|
432
|
+
value = super().__getitem__(key)
|
|
433
|
+
if isinstance(value, str) and value.startswith("class//"):
|
|
434
|
+
try:
|
|
435
|
+
dev_args = object.__getattribute__(self, 'device_args')
|
|
436
|
+
except AttributeError:
|
|
437
|
+
dev_args = ()
|
|
438
|
+
return _load_class(value)(*dev_args)
|
|
439
|
+
if isinstance(value, str) and value.startswith("csv//"):
|
|
440
|
+
return _load_csv(value)
|
|
441
|
+
if isinstance(value, str) and value.startswith("int_enum//"):
|
|
442
|
+
content = object.__getattribute__(self, "content")
|
|
443
|
+
return _load_int_enum(value, content)
|
|
444
|
+
else:
|
|
445
|
+
return value
|
|
446
|
+
|
|
447
|
+
def set_private_attribute(self, key: str, value) -> None:
|
|
448
|
+
"""Sets a private attribute for this object.
|
|
449
|
+
|
|
450
|
+
The name in key will be accessible as an attribute for this object, but the key will not
|
|
451
|
+
be added to the dictionary and not be returned by methods like keys().
|
|
452
|
+
|
|
453
|
+
The idea behind this private attribute is to have the possibility to add status information
|
|
454
|
+
or identifiers to this classes object that can be used by save() or load() methods.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
key (str): the name of the private attribute (must start with an underscore character).
|
|
458
|
+
value: the value for this private attribute
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
None.
|
|
462
|
+
|
|
463
|
+
Examples:
|
|
464
|
+
>>> setup = NavigableDict({'a': 1, 'b': 2, 'c': 3})
|
|
465
|
+
>>> setup.set_private_attribute("_loaded_from_dict", True)
|
|
466
|
+
>>> assert "c" in setup
|
|
467
|
+
>>> assert "_loaded_from_dict" not in setup
|
|
468
|
+
>>> assert setup.get_private_attribute("_loaded_from_dict") == True
|
|
469
|
+
|
|
470
|
+
"""
|
|
471
|
+
if key in self:
|
|
472
|
+
raise ValueError(
|
|
473
|
+
f"Invalid argument key='{key}', this key already exists in dictionary."
|
|
474
|
+
)
|
|
475
|
+
if not key.startswith("_"):
|
|
476
|
+
raise ValueError(
|
|
477
|
+
f"Invalid argument key='{key}', must start with underscore character '_'."
|
|
478
|
+
)
|
|
479
|
+
self.__dict__[key] = value
|
|
480
|
+
|
|
481
|
+
def get_private_attribute(self, key: str):
|
|
482
|
+
"""Returns the value of the given private attribute.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
key (str): the name of the private attribute (must start with an underscore character).
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
the value of the private attribute given in `key`.
|
|
489
|
+
|
|
490
|
+
.. note::
|
|
491
|
+
Because of the implementation, this private attribute can also be accessed as a 'normal'
|
|
492
|
+
attribute of the object. This use is however discouraged as it will make your code less
|
|
493
|
+
understandable. Use the methods to access these 'private' attributes.
|
|
494
|
+
"""
|
|
495
|
+
if not key.startswith("_"):
|
|
496
|
+
raise ValueError(
|
|
497
|
+
f"Invalid argument key='{key}', must start with underscore character '_'."
|
|
498
|
+
)
|
|
499
|
+
return self.__dict__[key]
|
|
500
|
+
|
|
501
|
+
def has_private_attribute(self, key):
|
|
502
|
+
"""
|
|
503
|
+
Check if the given key is defined as a private attribute.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
key (str): the name of a private attribute (must start with an underscore)
|
|
507
|
+
Returns:
|
|
508
|
+
True if the given key is a known private attribute.
|
|
509
|
+
Raises:
|
|
510
|
+
ValueError: when the key doesn't start with an underscore.
|
|
511
|
+
"""
|
|
512
|
+
if not key.startswith("_"):
|
|
513
|
+
raise ValueError(
|
|
514
|
+
f"Invalid argument key='{key}', must start with underscore character '_'."
|
|
515
|
+
)
|
|
516
|
+
try:
|
|
517
|
+
self.__dict__[key]
|
|
518
|
+
return True
|
|
519
|
+
except KeyError:
|
|
520
|
+
return False
|
|
521
|
+
|
|
522
|
+
def get_raw_value(self, key):
|
|
523
|
+
"""
|
|
524
|
+
Returns the raw value of the given key.
|
|
525
|
+
|
|
526
|
+
Some keys have special values that are interpreted by the AtributeDict class. An example is
|
|
527
|
+
a value that starts with 'class//'. When you access these values, they are first converted
|
|
528
|
+
from their raw value into their expected value, e.g. the instantiated object in the above
|
|
529
|
+
example. This method allows you to access the raw value before conversion.
|
|
530
|
+
"""
|
|
531
|
+
try:
|
|
532
|
+
return object.__getattribute__(self, key)
|
|
533
|
+
except AttributeError:
|
|
534
|
+
raise KeyError(f"The key '{key}' is not defined.")
|
|
535
|
+
|
|
536
|
+
def __str__(self):
|
|
537
|
+
return self.pretty_str()
|
|
538
|
+
|
|
539
|
+
def pretty_str(self, indent: int = 0):
|
|
540
|
+
"""
|
|
541
|
+
Returns a pretty string representation of the dictionary.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
indent (int): number of indentations (of four spaces)
|
|
545
|
+
|
|
546
|
+
.. note::
|
|
547
|
+
The indent argument is intended for the recursive call of this function.
|
|
548
|
+
"""
|
|
549
|
+
msg = ""
|
|
550
|
+
|
|
551
|
+
for k, v in self.items():
|
|
552
|
+
if isinstance(v, NavigableDict):
|
|
553
|
+
msg += f"{' '*indent}{k}:\n"
|
|
554
|
+
msg += v.pretty_str(indent + 1)
|
|
555
|
+
else:
|
|
556
|
+
msg += f"{' '*indent}{k}: {v}\n"
|
|
557
|
+
|
|
558
|
+
return msg
|
|
559
|
+
|
|
560
|
+
def __rich__(self) -> Tree:
|
|
561
|
+
tree = Tree("NavigableDict", guide_style="dim")
|
|
562
|
+
walk_dict_tree(self, tree, text_style="dark grey")
|
|
563
|
+
return tree
|
|
564
|
+
|
|
565
|
+
def _save(self, fd, indent: int = 0):
|
|
566
|
+
"""
|
|
567
|
+
Recursive method to write the dictionary to the file descriptor.
|
|
568
|
+
|
|
569
|
+
Indentation is done in steps of four spaces, i.e. `' '*indent`.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
fd: a file descriptor as returned by the open() function
|
|
573
|
+
indent (int): indentation level of each line [default = 0]
|
|
574
|
+
|
|
575
|
+
"""
|
|
576
|
+
from egse.device import DeviceInterface
|
|
577
|
+
|
|
578
|
+
# Note that the .items() method returns the actual values of the keys and doesn't use the
|
|
579
|
+
# __getattribute__ or __getitem__ methods. So the raw value is returned and not the
|
|
580
|
+
# _processed_ value.
|
|
581
|
+
|
|
582
|
+
for k, v in self.items():
|
|
583
|
+
|
|
584
|
+
# history shall be saved last, skip it for now
|
|
585
|
+
|
|
586
|
+
if k == "history":
|
|
587
|
+
continue
|
|
588
|
+
|
|
589
|
+
# make sure to escape a colon in the key name
|
|
590
|
+
|
|
591
|
+
if isinstance(k, str) and ":" in k:
|
|
592
|
+
k = '"' + k + '"'
|
|
593
|
+
|
|
594
|
+
if isinstance(v, NavigableDict):
|
|
595
|
+
fd.write(f"{' '*indent}{k}:\n")
|
|
596
|
+
v._save(fd, indent + 1)
|
|
597
|
+
fd.flush()
|
|
598
|
+
continue
|
|
599
|
+
|
|
600
|
+
if isinstance(v, DeviceInterface):
|
|
601
|
+
v = f"class//{v.__module__}.{v.__class__.__name__}"
|
|
602
|
+
if isinstance(v, float):
|
|
603
|
+
v = f"{v:.6E}"
|
|
604
|
+
fd.write(f"{' '*indent}{k}: {v}\n")
|
|
605
|
+
fd.flush()
|
|
606
|
+
|
|
607
|
+
# now save the history as the last item
|
|
608
|
+
|
|
609
|
+
if "history" in self:
|
|
610
|
+
fd.write(f"{' ' * indent}history:\n")
|
|
611
|
+
self.history._save(fd, indent + 1)
|
|
612
|
+
|
|
613
|
+
def get_memoized_keys(self):
|
|
614
|
+
return list(self.__dict__["_memoized"].keys())
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
class Setup(NavigableDict):
|
|
618
|
+
"""The Setup class represents a version of the configuration of the test facility, the
|
|
619
|
+
test setup and the Camera Under Test (CUT)."""
|
|
620
|
+
|
|
621
|
+
def __init__(self, nav_dict: NavigableDict = None):
|
|
622
|
+
super().__init__(nav_dict or {})
|
|
623
|
+
|
|
624
|
+
@staticmethod
|
|
625
|
+
def from_dict(my_dict):
|
|
626
|
+
"""Create a Setup from a given dictionary.
|
|
627
|
+
|
|
628
|
+
Remember that all keys in the given dictionary shall be of type 'str' in order to be
|
|
629
|
+
accessible as attributes.
|
|
630
|
+
|
|
631
|
+
Examples:
|
|
632
|
+
>>> setup = Setup.from_dict({"ID": "my-setup-001", "version": "0.1.0"})
|
|
633
|
+
>>> assert setup["ID"] == setup.ID == "my-setup-001"
|
|
634
|
+
|
|
635
|
+
"""
|
|
636
|
+
return Setup(my_dict)
|
|
637
|
+
|
|
638
|
+
@staticmethod
|
|
639
|
+
def from_yaml_string(yaml_content: str = None):
|
|
640
|
+
"""Loads a Setup from the given YAML string.
|
|
641
|
+
|
|
642
|
+
This method is mainly used for easy creation of Setups from strings during unit tests.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
yaml_content (str): a string containing YAML
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
a Setup that was loaded from the content of the given string.
|
|
649
|
+
"""
|
|
650
|
+
|
|
651
|
+
if not yaml_content:
|
|
652
|
+
raise ValueError("Invalid argument to function: No input string or None given.")
|
|
653
|
+
|
|
654
|
+
setup_dict = yaml.safe_load(yaml_content)
|
|
655
|
+
|
|
656
|
+
if "Setup" in setup_dict:
|
|
657
|
+
setup_dict = setup_dict["Setup"]
|
|
658
|
+
|
|
659
|
+
return Setup(setup_dict)
|
|
660
|
+
|
|
661
|
+
@staticmethod
|
|
662
|
+
@lru_cache
|
|
663
|
+
def from_yaml_file(filename: Union[str, Path] = None):
|
|
664
|
+
"""Loads a Setup from the given YAML file.
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
filename (str): the path of the YAML file to be loaded
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
a Setup that was loaded from the given location.
|
|
671
|
+
"""
|
|
672
|
+
from egse.settings import Settings
|
|
673
|
+
|
|
674
|
+
if not filename:
|
|
675
|
+
raise ValueError("Invalid argument to function: No filename or None given.")
|
|
676
|
+
|
|
677
|
+
setup_dict = Settings.load("Setup", filename=filename, force=True)
|
|
678
|
+
|
|
679
|
+
setup = Setup(setup_dict)
|
|
680
|
+
setup.set_private_attribute("_filename", filename)
|
|
681
|
+
if setup_id := _parse_filename_for_setup_id(str(filename)):
|
|
682
|
+
setup.set_private_attribute("_setup_id", setup_id)
|
|
683
|
+
|
|
684
|
+
return setup
|
|
685
|
+
|
|
686
|
+
def to_yaml_file(self, filename=None):
|
|
687
|
+
"""Saves a NavigableDict to a YAML file.
|
|
688
|
+
|
|
689
|
+
When no filename is provided, this method will look for a 'private' attribute
|
|
690
|
+
`_filename` and use that to save the data.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
filename (str): the path of the YAML file where to save the data
|
|
694
|
+
|
|
695
|
+
.. note::
|
|
696
|
+
This method will **overwrite** the original or given YAML file and therefore you might
|
|
697
|
+
lose proper formatting and/or comments.
|
|
698
|
+
|
|
699
|
+
"""
|
|
700
|
+
if not filename:
|
|
701
|
+
try:
|
|
702
|
+
filename = self.get_private_attribute("_filename")
|
|
703
|
+
except KeyError:
|
|
704
|
+
raise ValueError("No filename given or known, can not save Setup.")
|
|
705
|
+
|
|
706
|
+
print(f"Saving Setup to {filename}")
|
|
707
|
+
|
|
708
|
+
with Path(filename).open("w") as fd:
|
|
709
|
+
|
|
710
|
+
fd.write(
|
|
711
|
+
f"# Setup generated by:\n"
|
|
712
|
+
f"#\n"
|
|
713
|
+
f"# Setup.to_yaml_file(setup, filename='{filename}')\n#\n"
|
|
714
|
+
)
|
|
715
|
+
fd.write(f"# Created on {format_datetime()}\n\n")
|
|
716
|
+
fd.write("Setup:\n")
|
|
717
|
+
|
|
718
|
+
self._save(fd, indent=1)
|
|
719
|
+
|
|
720
|
+
self.set_private_attribute("_filename", filename)
|
|
721
|
+
|
|
722
|
+
@staticmethod
|
|
723
|
+
def compare(setup_1: NavigableDict, setup_2: NavigableDict):
|
|
724
|
+
from egse.device import DeviceInterface
|
|
725
|
+
from egse.dpu import DPUSimulator
|
|
726
|
+
from deepdiff import DeepDiff
|
|
727
|
+
|
|
728
|
+
return DeepDiff(setup_1, setup_2, exclude_types={DeviceInterface, DPUSimulator})
|
|
729
|
+
|
|
730
|
+
# def get_devices(self):
|
|
731
|
+
# """Returns a list of devices for the current setup.
|
|
732
|
+
|
|
733
|
+
# Returns:
|
|
734
|
+
# - List of devices for the current setup.
|
|
735
|
+
# """
|
|
736
|
+
|
|
737
|
+
# devices = []
|
|
738
|
+
|
|
739
|
+
# Setup.walk(self, "device", devices)
|
|
740
|
+
|
|
741
|
+
# return devices
|
|
742
|
+
|
|
743
|
+
@staticmethod
|
|
744
|
+
def find_devices(node: NavigableDict, devices={}):
|
|
745
|
+
"""
|
|
746
|
+
Returns a dictionary with the devices that are included in the setup. The keys
|
|
747
|
+
in the dictionary are taken from the "device_name" entries in the setup file. The
|
|
748
|
+
corresponding values in the dictionary are taken from the "device" entries in the
|
|
749
|
+
setup file.
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
- node: Dictionary in which to look for the devices (and their names).
|
|
753
|
+
- devices: Dictionary in which to include the devices in the setup.
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
- Dictionary with the devices that are included in the setup.
|
|
757
|
+
"""
|
|
758
|
+
|
|
759
|
+
for sub_node in node.values():
|
|
760
|
+
|
|
761
|
+
if isinstance(sub_node, NavigableDict):
|
|
762
|
+
|
|
763
|
+
if ("device" in sub_node) and ("device_name" in sub_node):
|
|
764
|
+
|
|
765
|
+
device = sub_node.get_raw_value("device")
|
|
766
|
+
|
|
767
|
+
if "device_args" in sub_node:
|
|
768
|
+
device_args = sub_node.get_raw_value("device_args")
|
|
769
|
+
else:
|
|
770
|
+
device_args = ()
|
|
771
|
+
|
|
772
|
+
devices[sub_node["device_name"]] = (device, device_args)
|
|
773
|
+
|
|
774
|
+
else:
|
|
775
|
+
|
|
776
|
+
Setup.find_devices(sub_node, devices=devices)
|
|
777
|
+
|
|
778
|
+
return devices
|
|
779
|
+
|
|
780
|
+
@staticmethod
|
|
781
|
+
def walk(node: dict, key_of_interest, leaf_list):
|
|
782
|
+
|
|
783
|
+
"""
|
|
784
|
+
Walk through the given dictionary, in a recursive way, appending the leaf with
|
|
785
|
+
the given keyword to the given list.
|
|
786
|
+
|
|
787
|
+
Args:
|
|
788
|
+
- node: Dictionary in which to look for leaves with the given keyword.
|
|
789
|
+
- key_of_interest: Key to look for in the leaves of the given dictionary.
|
|
790
|
+
- leaf_list: List to which to add the leaves with the given keyword.
|
|
791
|
+
|
|
792
|
+
Returns:
|
|
793
|
+
- Given list with the leaves (with the given keyword) in the given dictionary
|
|
794
|
+
appended to it.
|
|
795
|
+
"""
|
|
796
|
+
|
|
797
|
+
for key, sub_node in node.items():
|
|
798
|
+
|
|
799
|
+
if isinstance(sub_node, dict):
|
|
800
|
+
|
|
801
|
+
Setup.walk(sub_node, key_of_interest, leaf_list)
|
|
802
|
+
|
|
803
|
+
elif key == key_of_interest:
|
|
804
|
+
|
|
805
|
+
leaf_list.append(sub_node)
|
|
806
|
+
|
|
807
|
+
def __rich__(self) -> Tree:
|
|
808
|
+
tree = super().__rich__()
|
|
809
|
+
if self.has_private_attribute("_setup_id"):
|
|
810
|
+
setup_id = self.get_private_attribute('_setup_id')
|
|
811
|
+
tree.add(f"Setup ID: {setup_id}", style="grey50")
|
|
812
|
+
if self.has_private_attribute("_filename"):
|
|
813
|
+
filename = self.get_private_attribute('_filename')
|
|
814
|
+
tree.add(f"Loaded from: {filename}", style="grey50")
|
|
815
|
+
return tree
|
|
816
|
+
|
|
817
|
+
def get_id(self) -> Optional[str]:
|
|
818
|
+
"""Returns the Setup ID (as a string) or None when no setup id could be identified."""
|
|
819
|
+
if self.has_private_attribute("_setup_id"):
|
|
820
|
+
return self.get_private_attribute('_setup_id')
|
|
821
|
+
else:
|
|
822
|
+
return None
|
|
823
|
+
|
|
824
|
+
def get_filename(self) -> Optional[str]:
|
|
825
|
+
"""Returns the filename for this Setup or None when no filename could be determined."""
|
|
826
|
+
if self.has_private_attribute("_filename"):
|
|
827
|
+
return self.get_private_attribute('_filename')
|
|
828
|
+
else:
|
|
829
|
+
return None
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def list_setups(**attr):
|
|
833
|
+
"""
|
|
834
|
+
This is a function to be used for interactive use, it will print to the terminal (stdout) a
|
|
835
|
+
list of Setups known at the Configuration Manager. This list is sorted with the most recent (
|
|
836
|
+
highest) value last.
|
|
837
|
+
|
|
838
|
+
The list can be restricted with key:value pairs (keyword arguments). This _search_ mechanism
|
|
839
|
+
allows us to find all Setups that adhere to the key:value pairs, e.g. to find all Setups for
|
|
840
|
+
CSL at position 2, use:
|
|
841
|
+
|
|
842
|
+
>>> list_setups(site_id="CSL", position=2)
|
|
843
|
+
|
|
844
|
+
To have a nested keyword search (i.e. search by `gse.hexapod.ID`) then pass in
|
|
845
|
+
`gse__hexapod__ID` as the keyword argument. Replace the '.' notation with double underscores
|
|
846
|
+
'__'.
|
|
847
|
+
|
|
848
|
+
>>> list_setups(gse__hexapod__ID=4)
|
|
849
|
+
"""
|
|
850
|
+
|
|
851
|
+
from egse.confman import ConfigurationManagerProxy
|
|
852
|
+
|
|
853
|
+
try:
|
|
854
|
+
with ConfigurationManagerProxy() as proxy:
|
|
855
|
+
setups = proxy.list_setups(**attr)
|
|
856
|
+
if setups:
|
|
857
|
+
# We want to have the most recent (highest id number) last, but keep the site together
|
|
858
|
+
setups = sorted(setups, key=lambda x: (x[1], x[0]))
|
|
859
|
+
print("\n".join(f"{setup}" for setup in setups))
|
|
860
|
+
else:
|
|
861
|
+
print("no Setups found")
|
|
862
|
+
except ConnectionError:
|
|
863
|
+
print("Could not make a connection with the Configuration Manager, no Setup to show you.")
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def get_setup(setup_id: int = None):
|
|
867
|
+
"""
|
|
868
|
+
Retrieve the currently active Setup from the configuration manager.
|
|
869
|
+
|
|
870
|
+
When a setup_id is provided, that setup will be returned, but not loaded in the configuration
|
|
871
|
+
manager. This function does NOT change the configuration manager.
|
|
872
|
+
|
|
873
|
+
This function is for interactive use and consults the configuration manager server. Don't use
|
|
874
|
+
this within the test script, but use the `GlobalState.setup` property instead.
|
|
875
|
+
"""
|
|
876
|
+
from egse.confman import ConfigurationManagerProxy
|
|
877
|
+
|
|
878
|
+
try:
|
|
879
|
+
with ConfigurationManagerProxy() as proxy:
|
|
880
|
+
setup = proxy.get_setup(setup_id)
|
|
881
|
+
return setup
|
|
882
|
+
except ConnectionError as exc:
|
|
883
|
+
print(
|
|
884
|
+
"Could not make a connection with the Configuration Manager, no Setup returned."
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _check_conditions_for_get_path_of_setup_file(site_id: str) -> Path:
|
|
889
|
+
"""
|
|
890
|
+
Check some pre-conditions that need to be met before we try to determine the file path for
|
|
891
|
+
the requested Setup file.
|
|
892
|
+
|
|
893
|
+
The following checks are performed:
|
|
894
|
+
|
|
895
|
+
* if the environment variable 'PLATO_CONF_REPO_LOCATION' is set
|
|
896
|
+
|
|
897
|
+
* if the directory specified in the env variable actually exists
|
|
898
|
+
|
|
899
|
+
* if the folder with the Setups exists for the given site_id
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
Args:
|
|
903
|
+
site_id (str): the name of the test house
|
|
904
|
+
|
|
905
|
+
Returns:
|
|
906
|
+
The location of the Setup files for the given test house.
|
|
907
|
+
|
|
908
|
+
Raises:
|
|
909
|
+
LookupError when the environment variable is not set.
|
|
910
|
+
|
|
911
|
+
NotADirectoryError when either the repository folder or the Setups folder doesn't exist.
|
|
912
|
+
|
|
913
|
+
"""
|
|
914
|
+
repo_location_env = 'PLATO_CONF_REPO_LOCATION'
|
|
915
|
+
if not (repo_location := os.environ.get(repo_location_env)):
|
|
916
|
+
raise LookupError(
|
|
917
|
+
f"Environment variable doesn't exist, please define {repo_location_env} and try again."
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
repo_location = Path(repo_location)
|
|
921
|
+
setup_location = repo_location / 'data' / site_id / 'conf'
|
|
922
|
+
|
|
923
|
+
if not repo_location.is_dir():
|
|
924
|
+
raise NotADirectoryError(
|
|
925
|
+
f"The location of the repository for Setup files doesn't exist: {repo_location!s}. "
|
|
926
|
+
f"Please check the environment variable {repo_location_env}."
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
if not setup_location.is_dir():
|
|
930
|
+
raise NotADirectoryError(
|
|
931
|
+
f"The location of the Setup files doesn't exist: {setup_location!s}. "
|
|
932
|
+
f"Please check if the given {site_id=} is correct."
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
return setup_location
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def get_path_of_setup_file(setup_id: int, site_id: str) -> Path:
|
|
939
|
+
"""
|
|
940
|
+
Returns the Path to the last Setup file for the given site_id. The last Setup file is the file
|
|
941
|
+
with the largest setup_id number.
|
|
942
|
+
|
|
943
|
+
This function needs the environment variable PLATO_CONF_REPO_LOCATION to be defined as the
|
|
944
|
+
location of the repository 'plato-cgse-conf' on your disk.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
setup_id (int): the identifier for the requested Setup
|
|
948
|
+
site_id (str): the test house name, one of CSL, SRON, IAS, INTA
|
|
949
|
+
|
|
950
|
+
Returns:
|
|
951
|
+
The full path to the requested Setup file.
|
|
952
|
+
|
|
953
|
+
Raises:
|
|
954
|
+
LookupError when the environment variable is not set.
|
|
955
|
+
|
|
956
|
+
NotADirectoryError when either the repository folder or the Setups folder doesn't exist.
|
|
957
|
+
|
|
958
|
+
FileNotFound when no Setup file can be found for the given arguments.
|
|
959
|
+
|
|
960
|
+
"""
|
|
961
|
+
|
|
962
|
+
setup_location = _check_conditions_for_get_path_of_setup_file(site_id)
|
|
963
|
+
|
|
964
|
+
if setup_id:
|
|
965
|
+
files = list(setup_location.glob(f'SETUP_{site_id}_{setup_id:05d}_*.yaml'))
|
|
966
|
+
|
|
967
|
+
if not files:
|
|
968
|
+
raise FileNotFoundError(f"No Setup found for {setup_id=} and {site_id=}.")
|
|
969
|
+
|
|
970
|
+
file_path = Path(setup_location) / files[-1]
|
|
971
|
+
else:
|
|
972
|
+
files = setup_location.glob('SETUP*.yaml')
|
|
973
|
+
|
|
974
|
+
last_file_parts = sorted([file.name.split('_') for file in files])[-1]
|
|
975
|
+
file_path = Path(setup_location) / "_".join(last_file_parts)
|
|
976
|
+
|
|
977
|
+
sanity_check(file_path.is_file(), f"The expected Setup file doesn't exist: {file_path!s}")
|
|
978
|
+
|
|
979
|
+
return file_path
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def load_setup(
|
|
983
|
+
setup_id: int = None,
|
|
984
|
+
site_id: str = None, from_disk: bool = False):
|
|
985
|
+
"""
|
|
986
|
+
This function loads the Setup corresponding with the given `setup_id`.
|
|
987
|
+
|
|
988
|
+
Loading a Setup means:
|
|
989
|
+
|
|
990
|
+
* that this Setup will also be loaded and activated in the configuration manager,
|
|
991
|
+
* that this Setup will be available from the `GlobalState.setup`
|
|
992
|
+
|
|
993
|
+
When no setup_id is provided, the current Setup is loaded from the configuration manager.
|
|
994
|
+
|
|
995
|
+
Args:
|
|
996
|
+
setup_id (int): the identifier for the Setup
|
|
997
|
+
site_id (str): the name of the test house
|
|
998
|
+
from_disk (bool): True if the Setup needs to be loaded from disk
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
The requested Setup or None when the Setup could not be loaded from the
|
|
1002
|
+
configuration manager.
|
|
1003
|
+
|
|
1004
|
+
"""
|
|
1005
|
+
from egse.state import GlobalState
|
|
1006
|
+
|
|
1007
|
+
if from_disk:
|
|
1008
|
+
if site_id is None:
|
|
1009
|
+
raise ValueError(
|
|
1010
|
+
"The site_id argument can not be empty when from_disk is given and True")
|
|
1011
|
+
|
|
1012
|
+
setup_file_path = get_path_of_setup_file(setup_id, site_id)
|
|
1013
|
+
|
|
1014
|
+
rich.print(
|
|
1015
|
+
f"Loading {'' if setup_id else 'the latest '}Setup {f'{setup_id} ' if setup_id else ''}for {site_id}..."
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
return Setup.from_yaml_file(setup_file_path)
|
|
1019
|
+
|
|
1020
|
+
# When we arrive here the Setup shall be loaded from the Configuration manager
|
|
1021
|
+
|
|
1022
|
+
from egse.confman import ConfigurationManagerProxy
|
|
1023
|
+
|
|
1024
|
+
if setup_id is not None:
|
|
1025
|
+
try:
|
|
1026
|
+
with ConfigurationManagerProxy() as proxy:
|
|
1027
|
+
proxy.load_setup(setup_id)
|
|
1028
|
+
|
|
1029
|
+
except ConnectionError:
|
|
1030
|
+
MODULE_LOGGER.warning(
|
|
1031
|
+
"Could not make a connection with the Configuration Manager, no Setup to show you."
|
|
1032
|
+
)
|
|
1033
|
+
rich.print(
|
|
1034
|
+
"\n"
|
|
1035
|
+
"If you are not running this from an operational machine, do not have a CM "
|
|
1036
|
+
"running locally or don't know what this means, then: \n"
|
|
1037
|
+
" (1) define the environment variable 'PLATO_CONF_REPO_LOCATION' and \n"
|
|
1038
|
+
" it points to the location of the plato-cgse-conf repository,\n"
|
|
1039
|
+
" (2) try again using the argument 'from_disk=True'.\n"
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
return GlobalState.load_setup()
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def submit_setup(setup: Setup, description: str):
|
|
1046
|
+
"""
|
|
1047
|
+
Submit the given Setup to the Configuration Manager.
|
|
1048
|
+
|
|
1049
|
+
When you submit a Setup, the Configuration Manager will save this Setup with the
|
|
1050
|
+
next (new) setup id and make this Setup the current Setup in the Configuration manager
|
|
1051
|
+
unless you have explicitly set `replace=False` in which case the current Setup will
|
|
1052
|
+
not be replaced with the new Setup.
|
|
1053
|
+
|
|
1054
|
+
Args:
|
|
1055
|
+
setup (Setup): a (new) Setup to submit to the configuration manager
|
|
1056
|
+
description (str): one-liner to help identifying the Setup afterwards
|
|
1057
|
+
Returns:
|
|
1058
|
+
The Setup ID of the newly created Setup or None.
|
|
1059
|
+
"""
|
|
1060
|
+
# We have not yet decided if this option should be made available. Therefore, we
|
|
1061
|
+
# leave it here as hardcoded True.
|
|
1062
|
+
|
|
1063
|
+
# replace (bool): True if the current Setup in the configuration manager shall
|
|
1064
|
+
# be replaced by this new Setup. [default=True]
|
|
1065
|
+
replace: bool = True
|
|
1066
|
+
|
|
1067
|
+
from egse.confman import ConfigurationManagerProxy
|
|
1068
|
+
|
|
1069
|
+
try:
|
|
1070
|
+
with ConfigurationManagerProxy() as proxy:
|
|
1071
|
+
setup = proxy.submit_setup(setup, description, replace)
|
|
1072
|
+
|
|
1073
|
+
if setup is None:
|
|
1074
|
+
rich.print("[red]Submit failed for given Setup, no reason given.[/red]")
|
|
1075
|
+
elif isinstance(setup, Failure):
|
|
1076
|
+
rich.print(f"[red]Submit failed for given Setup[/red]: {setup}")
|
|
1077
|
+
setup = None
|
|
1078
|
+
elif replace:
|
|
1079
|
+
rich.print(textwrap.dedent(
|
|
1080
|
+
f"""\
|
|
1081
|
+
[green]
|
|
1082
|
+
Your new setup has been submitted and pushed to GitHub. The new setup is also
|
|
1083
|
+
activated in the configuration manager. Load the new setup in your session with:
|
|
1084
|
+
|
|
1085
|
+
setup = load_setup()
|
|
1086
|
+
[/]
|
|
1087
|
+
"""
|
|
1088
|
+
))
|
|
1089
|
+
else:
|
|
1090
|
+
rich.print(textwrap.dedent(
|
|
1091
|
+
f"""\
|
|
1092
|
+
[dark_orange]
|
|
1093
|
+
Your new setup has been submitted and pushed to GitHub, but has not been
|
|
1094
|
+
activated in the configuration manager. To activate this setup, use the
|
|
1095
|
+
following command:
|
|
1096
|
+
|
|
1097
|
+
setup = load_setup({str(setup.get_id())})
|
|
1098
|
+
[/]
|
|
1099
|
+
""")
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
return setup.get_id() if setup is not None else None
|
|
1103
|
+
|
|
1104
|
+
except ConnectionError:
|
|
1105
|
+
rich.print("Could not make a connection with the Configuration Manager, no Setup was submitted.")
|
|
1106
|
+
except NotImplementedError:
|
|
1107
|
+
rich.print(textwrap.dedent(
|
|
1108
|
+
"""\
|
|
1109
|
+
Caught a NotImplementedError. That usually means the configuration manager is not running or
|
|
1110
|
+
can not be reached. Check on the egse-server if the `cm_cs` process is running. If not you will
|
|
1111
|
+
need to be restart the core services.
|
|
1112
|
+
"""
|
|
1113
|
+
))
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
__all__ = [
|
|
1117
|
+
"Setup",
|
|
1118
|
+
"list_setups",
|
|
1119
|
+
"load_setup",
|
|
1120
|
+
"get_setup",
|
|
1121
|
+
"submit_setup",
|
|
1122
|
+
"SetupError",
|
|
1123
|
+
"load_last_setup_id",
|
|
1124
|
+
"save_last_setup_id",
|
|
1125
|
+
]
|
|
1126
|
+
|
|
1127
|
+
if __name__ == "__main__":
|
|
1128
|
+
|
|
1129
|
+
import sys
|
|
1130
|
+
import argparse
|
|
1131
|
+
|
|
1132
|
+
from rich import print
|
|
1133
|
+
|
|
1134
|
+
from egse.config import find_files
|
|
1135
|
+
from egse.settings import Settings
|
|
1136
|
+
|
|
1137
|
+
SITE = Settings.load("SITE")
|
|
1138
|
+
location = os.environ.get("PLATO_CONF_DATA_LOCATION")
|
|
1139
|
+
parser = argparse.ArgumentParser(
|
|
1140
|
+
description=textwrap.dedent(f"""\
|
|
1141
|
+
Print out the Setup for the given setup-id. The Setup will
|
|
1142
|
+
be loaded from the location given by the environment variable
|
|
1143
|
+
PLATO_CONF_DATA_LOCATION. If this env is not set, the Setup
|
|
1144
|
+
will be searched from the current directory."""
|
|
1145
|
+
),
|
|
1146
|
+
epilog=f"PLATO_CONF_DATA_LOCATION={location}"
|
|
1147
|
+
)
|
|
1148
|
+
parser.add_argument(
|
|
1149
|
+
"--setup-id", type=int, default=-1,
|
|
1150
|
+
help="the Setup ID. If not given, the last Setup will be selected.")
|
|
1151
|
+
parser.add_argument("--list", "-l", action="store_true", help="list available Setups.")
|
|
1152
|
+
parser.add_argument("--use-cm", action="store_true", help="use the configuration manager.")
|
|
1153
|
+
args = parser.parse_args()
|
|
1154
|
+
|
|
1155
|
+
if args.use_cm:
|
|
1156
|
+
from egse.confman import ConfigurationManagerProxy
|
|
1157
|
+
|
|
1158
|
+
with ConfigurationManagerProxy() as cm:
|
|
1159
|
+
if args.list:
|
|
1160
|
+
print(cm.list_setups())
|
|
1161
|
+
else:
|
|
1162
|
+
print(cm.get_setup())
|
|
1163
|
+
sys.exit(0)
|
|
1164
|
+
|
|
1165
|
+
if args.list:
|
|
1166
|
+
files = find_files(f"SETUP_{SITE.ID}_*_*.yaml", root=location)
|
|
1167
|
+
files = list(files)
|
|
1168
|
+
if files:
|
|
1169
|
+
location = files[0].parent.resolve()
|
|
1170
|
+
print(sorted([f.name for f in files]))
|
|
1171
|
+
print(f"Loaded from [purple]{location}.")
|
|
1172
|
+
else:
|
|
1173
|
+
setup_id = args.setup_id
|
|
1174
|
+
if setup_id == -1:
|
|
1175
|
+
setup_files = find_files(f"SETUP_{SITE.ID}_*_*.yaml", root=location)
|
|
1176
|
+
else:
|
|
1177
|
+
setup_files = find_files(f"SETUP_{SITE.ID}_{setup_id:05d}_*.yaml", root=location)
|
|
1178
|
+
setup_file = sorted(setup_files)[-1]
|
|
1179
|
+
setup = Setup.from_yaml_file(setup_file)
|
|
1180
|
+
print(setup)
|