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/obsid.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
An ObservationIdentifier or OBSID is a unique identifier for an observation or test.
|
|
3
|
+
|
|
4
|
+
Each observation or test needs a unique identification that can be used as a key in a
|
|
5
|
+
database or in a filename for test data etc.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
from egse.config import find_dir
|
|
12
|
+
from egse.settings import Settings
|
|
13
|
+
|
|
14
|
+
LAB_SETUP_TEST = 0
|
|
15
|
+
TEST_LAB_SETUP = 1
|
|
16
|
+
TEST_LAB = 3
|
|
17
|
+
|
|
18
|
+
SITE = Settings.load("SITE")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ObservationIdentifier:
|
|
22
|
+
"""A unique identifier for each observation or test."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, lab_id: str = None, setup_id: int = None, test_id: int = None):
|
|
25
|
+
"""
|
|
26
|
+
Args:
|
|
27
|
+
lab_id: the identifier for the site or lab that performs the test
|
|
28
|
+
setup_id: the identifier for the setup that is used for the test
|
|
29
|
+
test_id: the test identifier or test number
|
|
30
|
+
"""
|
|
31
|
+
if lab_id is None or setup_id is None or test_id is None:
|
|
32
|
+
raise ValueError("arguments can not be None or an empty string")
|
|
33
|
+
|
|
34
|
+
self._lab_id = lab_id
|
|
35
|
+
self._setup_id = setup_id
|
|
36
|
+
self._test_id = test_id
|
|
37
|
+
|
|
38
|
+
# Construct the OBSID as it will be used in serialisation etc.
|
|
39
|
+
|
|
40
|
+
self._obsid = f"{lab_id}_{setup_id:05d}_{test_id:05d}"
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def create_from_string(obsid: str, order: int = LAB_SETUP_TEST):
|
|
44
|
+
if order == LAB_SETUP_TEST:
|
|
45
|
+
lab_id, setup_id, test_id = obsid.split("_")
|
|
46
|
+
elif order == TEST_LAB_SETUP:
|
|
47
|
+
test_id, lab_id, setup_id = obsid.split("_")
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"The order argument can only be {LAB_SETUP_TEST=} or {TEST_LAB_SETUP=}")
|
|
51
|
+
|
|
52
|
+
return ObservationIdentifier(lab_id, int(setup_id), int(test_id))
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def lab_id(self):
|
|
56
|
+
return self._lab_id
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def setup_id(self):
|
|
60
|
+
return self._setup_id
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def test_id(self):
|
|
64
|
+
return self._test_id
|
|
65
|
+
|
|
66
|
+
def __eq__(self, other):
|
|
67
|
+
if not isinstance(other, ObservationIdentifier):
|
|
68
|
+
return NotImplemented
|
|
69
|
+
return self._obsid == other._obsid
|
|
70
|
+
|
|
71
|
+
def __hash__(self):
|
|
72
|
+
return hash(self._obsid)
|
|
73
|
+
|
|
74
|
+
def __str__(self):
|
|
75
|
+
return self._obsid
|
|
76
|
+
|
|
77
|
+
def create_id(self, *, order: int = LAB_SETUP_TEST, camera_name: str = None) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Creates a string representation of the observation identifier.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
order: the order in which the parts are concatenated
|
|
83
|
+
camera_name: if a camera name is given, it will be appended in lower case
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A string representation of the obsid with or without camera name attached.
|
|
87
|
+
"""
|
|
88
|
+
camera = f"{f'_{camera_name.lower()}' if camera_name else ''}"
|
|
89
|
+
|
|
90
|
+
if order == TEST_LAB_SETUP:
|
|
91
|
+
return f"{self._test_id:05d}_{self._lab_id}_{self._setup_id:05d}{camera}"
|
|
92
|
+
if order == TEST_LAB:
|
|
93
|
+
return f"{self._test_id:05d}_{self._lab_id}{camera}"
|
|
94
|
+
if order == LAB_SETUP_TEST:
|
|
95
|
+
return f"{self._lab_id}_{self._setup_id:05d}_{self._test_id:05d}{camera}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def obsid_from_storage(obsid: Union[ObservationIdentifier, str, int], data_dir: str,
|
|
99
|
+
site_id: str = None, camera_name: str = None) -> str:
|
|
100
|
+
"""
|
|
101
|
+
Return the name of the folder for the given obsid in the 'obs' sub-folder of data_dir.
|
|
102
|
+
|
|
103
|
+
For the oldest observations, the obsid used in the directory structure and filenames was of the format
|
|
104
|
+
TEST_LAB_SETUP. All files in this folder also have the obsid in that format in their name. At some point, we
|
|
105
|
+
decided to change this to TEST_LAB, but we still need to be able to re-process the old data (with the setup ID in
|
|
106
|
+
the names of the directories and files).
|
|
107
|
+
|
|
108
|
+
For newer observations (>= 2023.6.0+CGSE), the camera name is appended to the folder name and also included
|
|
109
|
+
in the filenames in that folder.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
obsid: Observation identifier. This can be an ObservationIdentifier object, a string in format TEST_LAB or
|
|
113
|
+
TEST_LAB_SETUP, or an integer representing the test ID. In this last case, the site id is taken from the
|
|
114
|
+
Settings.
|
|
115
|
+
data_dir: root folder in which the observations are stored. This folder shall have a sub-folder 'obs'.
|
|
116
|
+
site_id: a site id like 'CSL1' or 'IAS', when `None`, the `SITE.ID` from the Settings will be used
|
|
117
|
+
camera_name: if not None, append the camera name to the result
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The name of the folder for the given obsid in the 'obs' sub-folder of data_dir.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
obs_dir = f"{data_dir}/obs/"
|
|
124
|
+
site_id = site_id or SITE.ID
|
|
125
|
+
camera = f"_{camera_name.lower()}" if camera_name else ""
|
|
126
|
+
|
|
127
|
+
if isinstance(obsid, ObservationIdentifier):
|
|
128
|
+
test, site = obsid.test_id, obsid.lab_id
|
|
129
|
+
elif isinstance(obsid, str): # TEST_LAB or TEST_LAB_SETUP
|
|
130
|
+
test, site = obsid.split("_")[:2]
|
|
131
|
+
else:
|
|
132
|
+
test, site = obsid, site_id
|
|
133
|
+
|
|
134
|
+
test = int(test)
|
|
135
|
+
|
|
136
|
+
# Remember the camera name can be an empty string, so this will match both
|
|
137
|
+
# '00313_CSL' and '00313_CSL_achel'.
|
|
138
|
+
|
|
139
|
+
result_without_setup = Path(f"{obs_dir}/{test:05d}_{site}{camera}")
|
|
140
|
+
|
|
141
|
+
if result_without_setup.exists():
|
|
142
|
+
return result_without_setup.stem
|
|
143
|
+
|
|
144
|
+
# If a camera name was provided, but we try to find an old observation where the
|
|
145
|
+
# camera name was not appended to the folder name yet, the following will match
|
|
146
|
+
# that folder name.
|
|
147
|
+
|
|
148
|
+
result_without_camera = Path(f"{obs_dir}/{test:05d}_{site}")
|
|
149
|
+
|
|
150
|
+
if result_without_camera.exists():
|
|
151
|
+
return result_without_camera.stem
|
|
152
|
+
|
|
153
|
+
# When we come here, we can still match old observations that included the setup id in their
|
|
154
|
+
# folder name.
|
|
155
|
+
|
|
156
|
+
pattern = f"{test:05d}_{site}_*{camera}"
|
|
157
|
+
|
|
158
|
+
if (match := find_dir(pattern=pattern, root=obs_dir)) is None:
|
|
159
|
+
raise ValueError(f"Could not find a folder matching '{pattern}' in '{obs_dir}'")
|
|
160
|
+
|
|
161
|
+
return match.stem
|
egse/persistence.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PersistenceLayer(ABC):
|
|
6
|
+
"""The Persistence Layer implements the CRUD paradigm for storing data."""
|
|
7
|
+
|
|
8
|
+
extension = "no_ext"
|
|
9
|
+
"""The file extension to use for this persistence type."""
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def open(self, mode=None):
|
|
13
|
+
"""Opens the resource."""
|
|
14
|
+
raise NotImplementedError("Persistence layers must implement the open method")
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def close(self):
|
|
18
|
+
"""Closes the resource."""
|
|
19
|
+
raise NotImplementedError("Persistence layers must implement the close method")
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def exists(self):
|
|
23
|
+
"""Does the resource exists."""
|
|
24
|
+
raise NotImplementedError("Persistence layers must implement the exists method")
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def create(self, data):
|
|
28
|
+
"""Creates an entry in the persistence store."""
|
|
29
|
+
raise NotImplementedError("Persistence layers must implement a create method")
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def read(self, select=None):
|
|
33
|
+
"""Returns a list of all entries in the persistence store.
|
|
34
|
+
|
|
35
|
+
The list can be filtered based on a selection from the `select` argument which
|
|
36
|
+
should be a Callable object.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
select (Callable): a filter function to narrow down the list of all entries.
|
|
40
|
+
Returns:
|
|
41
|
+
A list or generator for all entries in the persistence store.
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError("Persistence layers must implement a read method")
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def update(self, idx, data):
|
|
47
|
+
"""Updates the entry for index `idx` in the persistence store."""
|
|
48
|
+
raise NotImplementedError("Persistence layers must implement an update method")
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def delete(self, idx):
|
|
52
|
+
"""Deletes the entry for index `idx` from the persistence store."""
|
|
53
|
+
raise NotImplementedError("Persistence layers must implement a delete method")
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def get_filepath(self):
|
|
57
|
+
"""If this persistence class is file based, return its file path, otherwise return None."""
|
|
58
|
+
raise NotImplementedError("Persistence layers must implement a get_filepath method")
|
egse/plugin.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import textwrap
|
|
5
|
+
import traceback
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import rich
|
|
9
|
+
|
|
10
|
+
LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def entry_points(name: str):
|
|
14
|
+
import importlib.metadata
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
x = importlib.metadata.entry_points()[name]
|
|
18
|
+
return {ep for ep in x} # use of set here to remove duplicates
|
|
19
|
+
except KeyError:
|
|
20
|
+
return []
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_plugins(entry_point: str) -> dict:
|
|
24
|
+
|
|
25
|
+
eps = {}
|
|
26
|
+
for ep in entry_points(entry_point):
|
|
27
|
+
try:
|
|
28
|
+
eps[ep.name] = ep.load()
|
|
29
|
+
except Exception as exc:
|
|
30
|
+
eps[ep.name] = None
|
|
31
|
+
LOGGER.error(f"Couldn't load entry point: {exc}")
|
|
32
|
+
|
|
33
|
+
return eps
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# The following code was adapted from the inspiring package click-plugins
|
|
37
|
+
# at https://github.com/click-contrib/click-plugins/
|
|
38
|
+
|
|
39
|
+
def handle_click_plugins(plugins):
|
|
40
|
+
def decorator(group):
|
|
41
|
+
if not isinstance(group, click.Group):
|
|
42
|
+
raise TypeError("Plugins can only be attached to an instance of click.Group()")
|
|
43
|
+
|
|
44
|
+
for entry_point in plugins or ():
|
|
45
|
+
try:
|
|
46
|
+
group.add_command(entry_point.load())
|
|
47
|
+
except Exception:
|
|
48
|
+
# Catch this so a busted plugin doesn't take down the CLI.
|
|
49
|
+
# Handled by registering a dummy command that does nothing
|
|
50
|
+
# other than explain the error.
|
|
51
|
+
group.add_command(BrokenCommand(entry_point.name))
|
|
52
|
+
|
|
53
|
+
return group
|
|
54
|
+
|
|
55
|
+
return decorator
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BrokenCommand(click.Command):
|
|
59
|
+
"""
|
|
60
|
+
Rather than completely crash the CLI when a broken plugin is loaded, this
|
|
61
|
+
class provides a modified help message informing the user that the plugin is
|
|
62
|
+
broken, and they should contact the owner. If the user executes the plugin
|
|
63
|
+
or specifies `--help` a traceback is reported showing the exception the
|
|
64
|
+
plugin loader encountered.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, name):
|
|
68
|
+
"""
|
|
69
|
+
Define the special help messages after instantiating a `click.Command()`.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
click.Command.__init__(self, name)
|
|
73
|
+
|
|
74
|
+
util_name = os.path.basename(sys.argv and sys.argv[0] or __file__)
|
|
75
|
+
icon = u'\u2020'
|
|
76
|
+
|
|
77
|
+
self.help = textwrap.dedent(
|
|
78
|
+
f"""\
|
|
79
|
+
Warning: entry point could not be loaded. Contact its author for help.
|
|
80
|
+
|
|
81
|
+
{traceback.format_exc()}
|
|
82
|
+
"""
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.short_help = f"{icon} Warning: could not load plugin. See `{util_name} {self.name} --help`."
|
|
86
|
+
|
|
87
|
+
def invoke(self, ctx):
|
|
88
|
+
"""
|
|
89
|
+
Print the traceback instead of doing nothing.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
rich.print()
|
|
93
|
+
rich.print(self.help)
|
|
94
|
+
ctx.exit(1)
|
|
95
|
+
|
|
96
|
+
def parse_args(self, ctx, args):
|
|
97
|
+
return args
|