blissdata 0.3.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.
- blissdata/__init__.py +17 -0
- blissdata/beacon/__init__.py +1 -0
- blissdata/beacon/_base.py +106 -0
- blissdata/beacon/config.py +24 -0
- blissdata/beacon/data.py +45 -0
- blissdata/beacon/files.py +141 -0
- blissdata/client.py +62 -0
- blissdata/common/__init__.py +15 -0
- blissdata/common/utils.py +159 -0
- blissdata/data/__init__.py +15 -0
- blissdata/data/events/__init__.py +22 -0
- blissdata/data/events/channel.py +149 -0
- blissdata/data/events/lima.py +478 -0
- blissdata/data/events/node.py +28 -0
- blissdata/data/events/scan.py +49 -0
- blissdata/data/events/walk.py +42 -0
- blissdata/data/expiration.py +42 -0
- blissdata/data/lima_image.py +465 -0
- blissdata/data/node.py +1619 -0
- blissdata/data/nodes/__init__.py +11 -0
- blissdata/data/nodes/channel.py +394 -0
- blissdata/data/nodes/dataset.py +82 -0
- blissdata/data/nodes/dataset_collection.py +12 -0
- blissdata/data/nodes/lima.py +422 -0
- blissdata/data/nodes/node_ref_channel.py +34 -0
- blissdata/data/nodes/proposal.py +12 -0
- blissdata/data/nodes/scan.py +192 -0
- blissdata/data/nodes/scan_group.py +12 -0
- blissdata/data/remote_node.py +204 -0
- blissdata/data/scan.py +666 -0
- blissdata/h5api/__init__.py +1 -0
- blissdata/h5api/abstract.py +97 -0
- blissdata/h5api/dynamic_hdf5.py +153 -0
- blissdata/h5api/file_arguments.py +28 -0
- blissdata/h5api/static_hdf5.py +139 -0
- blissdata/h5api/utils/__init__.py +0 -0
- blissdata/h5api/utils/bliss.py +138 -0
- blissdata/h5api/utils/hdf5.py +280 -0
- blissdata/h5api/utils/hdf5_retry.py +98 -0
- blissdata/h5api/utils/lima.py +286 -0
- blissdata/h5api/utils/types.py +13 -0
- blissdata/redis/__init__.py +12 -0
- blissdata/redis/caching.py +390 -0
- blissdata/redis/connection.py +169 -0
- blissdata/redis/manager.py +164 -0
- blissdata/redis/proxy.py +971 -0
- blissdata/redis/scripting.py +24 -0
- blissdata/settings.py +1174 -0
- blissdata/streaming.py +819 -0
- blissdata/streaming_events.py +355 -0
- blissdata/tests/__init__.py +0 -0
- blissdata/tests/beacon/__init__.py +0 -0
- blissdata/tests/beacon/test_config.py +26 -0
- blissdata/tests/beacon/test_data.py +50 -0
- blissdata/tests/beacon/test_files.py +80 -0
- blissdata/tests/conftest.py +0 -0
- blissdata/tests/h5api/__init__.py +0 -0
- blissdata/tests/h5api/scanner.py +390 -0
- blissdata/tests/h5api/test_dynamic_files.py +309 -0
- blissdata/tests/h5api/test_static_files.py +169 -0
- blissdata/tests/redis/__init__.py +0 -0
- blissdata/tests/redis/manager.py +23 -0
- blissdata-0.3.4.dist-info/LICENSE +165 -0
- blissdata-0.3.4.dist-info/METADATA +79 -0
- blissdata-0.3.4.dist-info/RECORD +67 -0
- blissdata-0.3.4.dist-info/WHEEL +5 -0
- blissdata-0.3.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Implementation of the h5py-like Bliss Data API with dynamic HDF5 files"""
|
|
2
|
+
|
|
3
|
+
from numbers import Number
|
|
4
|
+
from typing import Any, Iterator, Optional, Union
|
|
5
|
+
|
|
6
|
+
import h5py
|
|
7
|
+
from silx.io import h5py_utils
|
|
8
|
+
|
|
9
|
+
from . import types
|
|
10
|
+
from . import lima
|
|
11
|
+
from .hdf5_retry import retry_file_access
|
|
12
|
+
from .hdf5_retry import ignore_retry_timeout
|
|
13
|
+
from .hdf5_retry import RetryTimeoutError, RetryError, SoftRetryError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
HDF5Item = Union[h5py.Dataset, h5py.Group, h5py.File, lima.LimaDataset, lima.LimaGroup]
|
|
17
|
+
HDF5Group = Union[h5py.Group, h5py.File, lima.LimaGroup]
|
|
18
|
+
HDF5Dataset = Union[h5py.Dataset, lima.LimaDataset]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
SEP = "/"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DynamicHDF5Handler:
|
|
25
|
+
"""Object to access an HDF5 file which is re-opened upon re-trying failed IO operations"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
file: str,
|
|
30
|
+
retry_timeout: Optional[Number] = None,
|
|
31
|
+
retry_period: Optional[Number] = None,
|
|
32
|
+
**openargs,
|
|
33
|
+
):
|
|
34
|
+
self._file = file
|
|
35
|
+
self._openargs = openargs
|
|
36
|
+
self._file_obj = None
|
|
37
|
+
self._closed = False
|
|
38
|
+
self._retry_period = retry_period
|
|
39
|
+
self._retry_options = {
|
|
40
|
+
"retry_timeout": retry_timeout,
|
|
41
|
+
"retry_period": retry_period,
|
|
42
|
+
}
|
|
43
|
+
self._native_items = dict()
|
|
44
|
+
|
|
45
|
+
def close(self) -> None:
|
|
46
|
+
"""Close the HDF5 file, cleanup all HDF5 objects and do not allow re-opening"""
|
|
47
|
+
self._closed = True
|
|
48
|
+
self._cleanup()
|
|
49
|
+
|
|
50
|
+
def reset(self) -> None:
|
|
51
|
+
"""Close the HDF5 file, cleanup all HDF5 objects but allow re-opening"""
|
|
52
|
+
self._cleanup()
|
|
53
|
+
|
|
54
|
+
def _cleanup(self) -> None:
|
|
55
|
+
"""Close the HDF5 file, cleanup all HDF5 objects"""
|
|
56
|
+
if self._file_obj is None:
|
|
57
|
+
return
|
|
58
|
+
self._file_obj.close()
|
|
59
|
+
self._file_obj = None
|
|
60
|
+
self._native_items = dict()
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def file_obj(self) -> h5py.File:
|
|
64
|
+
if self._file_obj is None:
|
|
65
|
+
if self._closed:
|
|
66
|
+
raise RuntimeError("File was closed")
|
|
67
|
+
try:
|
|
68
|
+
self._file_obj = h5py_utils.File(self._file, **self._openargs)
|
|
69
|
+
except FileNotFoundError:
|
|
70
|
+
raise RetryError(f"File {self._file} does not exist (yet)")
|
|
71
|
+
return self._file_obj
|
|
72
|
+
|
|
73
|
+
def get_item(self, name: str) -> HDF5Item:
|
|
74
|
+
try:
|
|
75
|
+
return self._retry_get_item(name)
|
|
76
|
+
except RetryTimeoutError:
|
|
77
|
+
raise KeyError(name)
|
|
78
|
+
|
|
79
|
+
def slice_dataset(self, name: str, idx: types.DataIndexType) -> types.DataType:
|
|
80
|
+
return self._retry_slice_dataset(name, idx)
|
|
81
|
+
|
|
82
|
+
def get_attr(self, name: str, key: str) -> Any:
|
|
83
|
+
try:
|
|
84
|
+
return self._retry_get_attr(name, key)
|
|
85
|
+
except RetryTimeoutError:
|
|
86
|
+
raise AttributeError(name)
|
|
87
|
+
|
|
88
|
+
@ignore_retry_timeout
|
|
89
|
+
def iter_attrs(self, name: str) -> Iterator[str]:
|
|
90
|
+
yield from self._retry_iter_attrs(name)
|
|
91
|
+
|
|
92
|
+
@ignore_retry_timeout
|
|
93
|
+
def len_attrs(self, name: str) -> int:
|
|
94
|
+
return self._retry_len_attrs(name)
|
|
95
|
+
|
|
96
|
+
@ignore_retry_timeout
|
|
97
|
+
def iter_item(self, name: str) -> Iterator[Union[str, types.DataType]]:
|
|
98
|
+
yield from self._retry_iter_item(name)
|
|
99
|
+
|
|
100
|
+
@ignore_retry_timeout
|
|
101
|
+
def len_item(self, name: str) -> int:
|
|
102
|
+
return self._retry_len_item(name)
|
|
103
|
+
|
|
104
|
+
def getattr_item(self, name: str, attr_name: str) -> Any:
|
|
105
|
+
return self._retry_getattr_item(name, attr_name)
|
|
106
|
+
|
|
107
|
+
@retry_file_access
|
|
108
|
+
def _retry_get_item(self, name: str) -> HDF5Item:
|
|
109
|
+
return self._get_item(name)
|
|
110
|
+
|
|
111
|
+
@retry_file_access
|
|
112
|
+
def _retry_slice_dataset(
|
|
113
|
+
self, name: str, idx: types.DataIndexType
|
|
114
|
+
) -> types.DataType:
|
|
115
|
+
return self._slice_dataset(name, idx)
|
|
116
|
+
|
|
117
|
+
@retry_file_access
|
|
118
|
+
def _retry_get_attr(self, name: str, key: str) -> Any:
|
|
119
|
+
return self._get_attr(name, key)
|
|
120
|
+
|
|
121
|
+
@retry_file_access
|
|
122
|
+
def _retry_iter_attrs(self, name: str, start_index: int = 0) -> Iterator[str]:
|
|
123
|
+
yield from self._iter_attrs(name, start_index=start_index)
|
|
124
|
+
|
|
125
|
+
@retry_file_access
|
|
126
|
+
def _retry_len_attrs(self, name: str) -> int:
|
|
127
|
+
return self._len_attrs(name)
|
|
128
|
+
|
|
129
|
+
@retry_file_access
|
|
130
|
+
def _retry_iter_item(
|
|
131
|
+
self, name: str, start_index: int = 0
|
|
132
|
+
) -> Iterator[Union[str, types.DataType]]:
|
|
133
|
+
yield from self._iter_item(name, start_index=start_index)
|
|
134
|
+
|
|
135
|
+
@retry_file_access
|
|
136
|
+
def _retry_len_item(self, name: str) -> int:
|
|
137
|
+
return self._len_item(name)
|
|
138
|
+
|
|
139
|
+
@retry_file_access
|
|
140
|
+
def _retry_getattr_item(self, name: str, attr_name: str) -> Any:
|
|
141
|
+
return self._getattr_item(name, attr_name)
|
|
142
|
+
|
|
143
|
+
def _get_item(self, name: str) -> HDF5Item:
|
|
144
|
+
item = self._native_items.get(name)
|
|
145
|
+
if item is not None:
|
|
146
|
+
return item
|
|
147
|
+
try:
|
|
148
|
+
item = self.file_obj[name]
|
|
149
|
+
except KeyError:
|
|
150
|
+
parent_name, _, item_name = name.rpartition(SEP)
|
|
151
|
+
if not parent_name:
|
|
152
|
+
parent_name = SEP
|
|
153
|
+
if parent_name == name:
|
|
154
|
+
raise
|
|
155
|
+
parent_item = self._get_item(parent_name)
|
|
156
|
+
if not self._is_finished(parent_item):
|
|
157
|
+
raise # item could still be created
|
|
158
|
+
try:
|
|
159
|
+
item = parent_item[item_name] # try one more time
|
|
160
|
+
except KeyError as e:
|
|
161
|
+
if not _key_does_not_exist(e):
|
|
162
|
+
raise
|
|
163
|
+
# item will never be created
|
|
164
|
+
raise RetryTimeoutError(str(e)) from e
|
|
165
|
+
self._native_items[name] = item
|
|
166
|
+
return item
|
|
167
|
+
|
|
168
|
+
def _slice_dataset(self, name: str, idx: types.DataIndexType) -> types.DataType:
|
|
169
|
+
item = self._get_item(name)
|
|
170
|
+
self._check_dataset_before_read(item)
|
|
171
|
+
return item[idx]
|
|
172
|
+
|
|
173
|
+
def _check_dataset_before_read(self, item: HDF5Dataset):
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
def _get_attr(self, name: str, key: str) -> Any:
|
|
177
|
+
item = self._get_item(name)
|
|
178
|
+
try:
|
|
179
|
+
return item.attrs[key]
|
|
180
|
+
except KeyError:
|
|
181
|
+
if not self._is_finished(item):
|
|
182
|
+
raise # attribute could still be created
|
|
183
|
+
try:
|
|
184
|
+
return item.attrs[key] # try one more time
|
|
185
|
+
except KeyError as e:
|
|
186
|
+
if not _key_does_not_exist(e):
|
|
187
|
+
raise
|
|
188
|
+
# attribute will never be created
|
|
189
|
+
raise RetryTimeoutError(str(e)) from e
|
|
190
|
+
|
|
191
|
+
def _iter_attrs(self, name: str, start_index: int = 0) -> Iterator[str]:
|
|
192
|
+
item = self._get_item(name)
|
|
193
|
+
is_complete = self._is_initialized(item)
|
|
194
|
+
if start_index == 0:
|
|
195
|
+
yield from item.attrs
|
|
196
|
+
else:
|
|
197
|
+
yield from list(item.attrs)[start_index:]
|
|
198
|
+
if not is_complete:
|
|
199
|
+
raise RetryError("attributes could still be added")
|
|
200
|
+
|
|
201
|
+
def _len_attrs(self, name: str) -> int:
|
|
202
|
+
item = self._get_item(name)
|
|
203
|
+
return len(item.attrs)
|
|
204
|
+
|
|
205
|
+
def _iter_item(
|
|
206
|
+
self, name: str, start_index: int = 0
|
|
207
|
+
) -> Iterator[Union[str, types.DataType]]:
|
|
208
|
+
item = self._get_item(name)
|
|
209
|
+
self._check_dataset_before_read(item)
|
|
210
|
+
if self.is_group(item):
|
|
211
|
+
is_complete = self._is_initialized(item)
|
|
212
|
+
for key in self._iter_group(item, start_index):
|
|
213
|
+
is_complete = False
|
|
214
|
+
yield key
|
|
215
|
+
if not is_complete:
|
|
216
|
+
raise RetryError("children could still be added to the group")
|
|
217
|
+
else:
|
|
218
|
+
is_complete = self._is_finished(item)
|
|
219
|
+
for data in self._iter_dataset(item, start_index):
|
|
220
|
+
is_complete = False
|
|
221
|
+
yield data
|
|
222
|
+
if not is_complete:
|
|
223
|
+
raise RetryError("dataset could still grow")
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def is_group(h5item: HDF5Item):
|
|
227
|
+
return isinstance(h5item, (h5py.Group, h5py.File))
|
|
228
|
+
|
|
229
|
+
def _iter_group(self, h5group: HDF5Group, start_index: int) -> Iterator[str]:
|
|
230
|
+
try:
|
|
231
|
+
if start_index == 0:
|
|
232
|
+
yield from h5group
|
|
233
|
+
else:
|
|
234
|
+
yield from list(h5group.keys())[start_index:]
|
|
235
|
+
except ValueError:
|
|
236
|
+
raise SoftRetryError(f"Failed accessing group {h5group}")
|
|
237
|
+
|
|
238
|
+
def _iter_dataset(
|
|
239
|
+
self, h5dataset: HDF5Dataset, start_index: int
|
|
240
|
+
) -> Iterator[types.DataType]:
|
|
241
|
+
try:
|
|
242
|
+
if start_index == 0:
|
|
243
|
+
yield from h5dataset
|
|
244
|
+
else:
|
|
245
|
+
for i in range(start_index, len(h5dataset)):
|
|
246
|
+
yield h5dataset[i]
|
|
247
|
+
except ValueError:
|
|
248
|
+
raise SoftRetryError(f"Failed accessing dataset {h5dataset}")
|
|
249
|
+
|
|
250
|
+
def _len_item(self, name: str) -> int:
|
|
251
|
+
item = self._get_item(name)
|
|
252
|
+
return len(item)
|
|
253
|
+
|
|
254
|
+
def _getattr_item(self, name: str, attr_name: str) -> Any:
|
|
255
|
+
item = self._get_item(name)
|
|
256
|
+
return getattr(item, attr_name)
|
|
257
|
+
|
|
258
|
+
def _is_initialized(self, h5item: HDF5Item) -> bool:
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
def _is_finished(self, h5item: HDF5Item) -> bool:
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _key_does_not_exist(exc: KeyError) -> bool:
|
|
266
|
+
"""
|
|
267
|
+
A real KeyError (meaning that the key does not exist) gives the following error message:
|
|
268
|
+
|
|
269
|
+
.. code
|
|
270
|
+
|
|
271
|
+
KeyError: "Unable to synchronously open object (object 'end_time' doesn't exist)"
|
|
272
|
+
|
|
273
|
+
An example of a KeyError caused by another reason
|
|
274
|
+
|
|
275
|
+
.. code
|
|
276
|
+
|
|
277
|
+
KeyError: 'Unable to synchronously open object (addr overflow, addr = 64392369, size = 96, eoa = 64365353)'
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
return "doesn't exist" in str(exc)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from silx.io import h5py_utils
|
|
6
|
+
from silx.utils.retry import RetryTimeoutError, RetryError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SoftRetryError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def retry_file_access(method: Callable) -> Callable:
|
|
14
|
+
return _reset_on_retry_failed(
|
|
15
|
+
h5py_utils.retry(retry_on_error=_retry_on_error)(_reset_before_retry(method))
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ignore_retry_timeout(method: Callable) -> Callable:
|
|
20
|
+
if inspect.isgeneratorfunction(method):
|
|
21
|
+
|
|
22
|
+
@wraps(method)
|
|
23
|
+
def wrapper(self, *args, **kw):
|
|
24
|
+
try:
|
|
25
|
+
yield from method(self, *args, **kw)
|
|
26
|
+
except RetryTimeoutError:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
else:
|
|
30
|
+
|
|
31
|
+
@wraps(method)
|
|
32
|
+
def wrapper(self, *args, **kw):
|
|
33
|
+
try:
|
|
34
|
+
return method(self, *args, **kw)
|
|
35
|
+
except RetryTimeoutError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
return wrapper
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _retry_on_error(e: BaseException) -> bool:
|
|
42
|
+
return h5py_utils._is_h5py_exception(e) or isinstance(e, RetryError)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _reset_before_retry(method: Callable) -> Callable:
|
|
46
|
+
if inspect.isgeneratorfunction(method):
|
|
47
|
+
|
|
48
|
+
@wraps(method)
|
|
49
|
+
def wrapper(self, *args, start_index: int = 0, **kw):
|
|
50
|
+
try:
|
|
51
|
+
yield from method(self, *args, start_index=start_index, **kw)
|
|
52
|
+
except SoftRetryError:
|
|
53
|
+
raise RetryError
|
|
54
|
+
except Exception as e:
|
|
55
|
+
if _retry_on_error(e):
|
|
56
|
+
self.reset()
|
|
57
|
+
raise
|
|
58
|
+
|
|
59
|
+
else:
|
|
60
|
+
|
|
61
|
+
@wraps(method)
|
|
62
|
+
def wrapper(self, *args, **kw):
|
|
63
|
+
try:
|
|
64
|
+
return method(self, *args, **kw)
|
|
65
|
+
except SoftRetryError:
|
|
66
|
+
raise RetryError
|
|
67
|
+
except Exception as e:
|
|
68
|
+
if _retry_on_error(e):
|
|
69
|
+
self.reset()
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
return wrapper
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _reset_on_retry_failed(method: Callable) -> Callable:
|
|
76
|
+
if inspect.isgeneratorfunction(method):
|
|
77
|
+
|
|
78
|
+
@wraps(method)
|
|
79
|
+
def wrapper(self, *args, start_index: int = 0, **kw):
|
|
80
|
+
kw.update(self._retry_options)
|
|
81
|
+
try:
|
|
82
|
+
yield from method(self, *args, start_index=start_index, **kw)
|
|
83
|
+
except RetryTimeoutError:
|
|
84
|
+
self.reset()
|
|
85
|
+
raise
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
|
|
89
|
+
@wraps(method)
|
|
90
|
+
def wrapper(self, *args, **kw):
|
|
91
|
+
kw.update(self._retry_options)
|
|
92
|
+
try:
|
|
93
|
+
return method(self, *args, **kw)
|
|
94
|
+
except RetryTimeoutError:
|
|
95
|
+
self.reset()
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
return wrapper
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from glob import glob
|
|
4
|
+
from collections import abc
|
|
5
|
+
from numbers import Integral
|
|
6
|
+
from typing import Optional, Tuple, Sequence, Iterator
|
|
7
|
+
|
|
8
|
+
import numpy
|
|
9
|
+
from numpy.typing import DTypeLike
|
|
10
|
+
from silx.utils.retry import RetryError
|
|
11
|
+
from silx.io import h5py_utils
|
|
12
|
+
|
|
13
|
+
from . import types
|
|
14
|
+
|
|
15
|
+
SEP = "/"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LimaGroup(abc.Mapping):
|
|
19
|
+
def __init__(self, name, *args, **kwargs) -> None:
|
|
20
|
+
self._name = name
|
|
21
|
+
self._dset_args = args
|
|
22
|
+
self._dset_kwargs = kwargs
|
|
23
|
+
self._dataset = None
|
|
24
|
+
super().__init__()
|
|
25
|
+
|
|
26
|
+
def close(self):
|
|
27
|
+
if self._dataset is None:
|
|
28
|
+
return
|
|
29
|
+
self._dataset.close()
|
|
30
|
+
self._dataset = None
|
|
31
|
+
|
|
32
|
+
def reset(self):
|
|
33
|
+
if self._dataset is None:
|
|
34
|
+
return
|
|
35
|
+
self._dataset.reset()
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def name(self) -> str:
|
|
39
|
+
return self._name
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def attrs(self) -> dict:
|
|
43
|
+
return {"type": "lima"}
|
|
44
|
+
|
|
45
|
+
def __getitem__(self, key: str):
|
|
46
|
+
if key == "data":
|
|
47
|
+
if self._dataset is None:
|
|
48
|
+
self._dataset = LimaDataset(
|
|
49
|
+
self.name + SEP + "data", *self._dset_args, **self._dset_kwargs
|
|
50
|
+
)
|
|
51
|
+
return self._dataset
|
|
52
|
+
raise KeyError
|
|
53
|
+
|
|
54
|
+
def __iter__(self):
|
|
55
|
+
yield "data"
|
|
56
|
+
|
|
57
|
+
def __len__(self) -> int:
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LimaDataset(abc.Sequence):
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
name: str,
|
|
65
|
+
dirname: str,
|
|
66
|
+
url_template: Optional[str] = None,
|
|
67
|
+
user_detector_name: Optional[str] = None,
|
|
68
|
+
user_instrument_name: Optional[str] = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
parts = [s for s in name.split(SEP) if s]
|
|
71
|
+
scan_number = int(parts[0].split(".")[0])
|
|
72
|
+
bliss_detector_name = parts[-2]
|
|
73
|
+
|
|
74
|
+
url_template = lima_url_template(
|
|
75
|
+
dirname,
|
|
76
|
+
scan_number,
|
|
77
|
+
bliss_detector_name,
|
|
78
|
+
url_template=url_template,
|
|
79
|
+
user_detector_name=user_detector_name,
|
|
80
|
+
user_instrument_name=user_instrument_name,
|
|
81
|
+
)
|
|
82
|
+
filename_template, self._path_in_file = url_template.split("::")
|
|
83
|
+
self._search_pattern = filename_template.format(file_index="*")
|
|
84
|
+
self._match_pattern = re.compile(
|
|
85
|
+
os.path.basename(filename_template.format(file_index="([0-9]+)"))
|
|
86
|
+
)
|
|
87
|
+
self._files = list()
|
|
88
|
+
|
|
89
|
+
self._first_shape = None
|
|
90
|
+
self._points_per_file = None
|
|
91
|
+
self._dtype = None
|
|
92
|
+
self._shape = None
|
|
93
|
+
|
|
94
|
+
self._active_filename = None
|
|
95
|
+
self._active_file = None
|
|
96
|
+
self._active_dset = None
|
|
97
|
+
self._name = name
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def name(self) -> str:
|
|
101
|
+
return self._name
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def attrs(self) -> dict:
|
|
105
|
+
return dict()
|
|
106
|
+
|
|
107
|
+
def reset(self):
|
|
108
|
+
"""Close the activate HDF5 file, cleanup all HDF5 objects and search for lima files"""
|
|
109
|
+
self._cleanup()
|
|
110
|
+
self.search()
|
|
111
|
+
|
|
112
|
+
def close(self):
|
|
113
|
+
"""Close the activate HDF5 file and cleanup all HDF5 objects"""
|
|
114
|
+
self._cleanup()
|
|
115
|
+
|
|
116
|
+
def _cleanup(self):
|
|
117
|
+
if self._active_file is None:
|
|
118
|
+
return
|
|
119
|
+
self._active_file.close()
|
|
120
|
+
self._active_filename = None
|
|
121
|
+
self._active_file = None
|
|
122
|
+
self._active_dset = None
|
|
123
|
+
|
|
124
|
+
def search(self):
|
|
125
|
+
lima_files = glob(self._search_pattern)
|
|
126
|
+
lima_nrs = [
|
|
127
|
+
int(self._match_pattern.search(os.path.basename(s)).group(1))
|
|
128
|
+
for s in lima_files
|
|
129
|
+
]
|
|
130
|
+
self._files = [filename for _, filename in sorted(zip(lima_nrs, lima_files))]
|
|
131
|
+
self._shape = None
|
|
132
|
+
|
|
133
|
+
def _open(self, file_index: int):
|
|
134
|
+
try:
|
|
135
|
+
filename = self._files[file_index]
|
|
136
|
+
except IndexError:
|
|
137
|
+
raise RetryError(f"lima file {file_index} does not exist (yet)")
|
|
138
|
+
if self._active_filename == filename:
|
|
139
|
+
return
|
|
140
|
+
self.close()
|
|
141
|
+
# Lima has the file open until all images are written.
|
|
142
|
+
# Lima does not flush which means the file is never readable while it is open.
|
|
143
|
+
# To be sure about the later, try locking the file when reading.
|
|
144
|
+
# If it succeeds it means lima is done with the file.
|
|
145
|
+
self._active_file = h5py_utils.File(
|
|
146
|
+
filename, mode="r"
|
|
147
|
+
) # , locking=h5py_utils.HAS_LOCKING_ARGUMENT)
|
|
148
|
+
self._active_dset = self._active_file[self._path_in_file]
|
|
149
|
+
self._active_filename = filename
|
|
150
|
+
|
|
151
|
+
def __getitem__(self, idx: types.DataIndexType) -> types.DataType:
|
|
152
|
+
if isinstance(idx, Tuple):
|
|
153
|
+
idx0 = idx[0]
|
|
154
|
+
idximage = idx[1:]
|
|
155
|
+
else:
|
|
156
|
+
idx0 = idx
|
|
157
|
+
idximage = tuple()
|
|
158
|
+
|
|
159
|
+
dim0scalar = False
|
|
160
|
+
if isinstance(idx0, Integral):
|
|
161
|
+
idx0 = numpy.asarray([idx0])
|
|
162
|
+
dim0scalar = True
|
|
163
|
+
elif isinstance(idx0, slice):
|
|
164
|
+
idx0 = numpy.array(range(*idx0.indices(self.shape[0])))
|
|
165
|
+
elif isinstance(idx0, Sequence):
|
|
166
|
+
idx0 = numpy.asarray(idx0)
|
|
167
|
+
elif idx0 is Ellipsis:
|
|
168
|
+
idx0 = numpy.array(range(self.shape[0]))
|
|
169
|
+
else:
|
|
170
|
+
raise TypeError
|
|
171
|
+
|
|
172
|
+
result = list()
|
|
173
|
+
|
|
174
|
+
for scan_index in idx0:
|
|
175
|
+
file_index = scan_index // self.point_per_file
|
|
176
|
+
self._open(file_index)
|
|
177
|
+
idx = (scan_index % self.point_per_file,) + idximage
|
|
178
|
+
try:
|
|
179
|
+
result.append(self._active_dset[idx])
|
|
180
|
+
except IndexError:
|
|
181
|
+
raise RetryError(f"Failed slice lima dataset {self._active_dset}")
|
|
182
|
+
|
|
183
|
+
if dim0scalar:
|
|
184
|
+
return result[0]
|
|
185
|
+
else:
|
|
186
|
+
return numpy.array(result)
|
|
187
|
+
|
|
188
|
+
def __iter__(self) -> Iterator[types.DataType]:
|
|
189
|
+
for file_index in range(len(self._files)):
|
|
190
|
+
self._open(file_index)
|
|
191
|
+
yield from iter(self._active_dset)
|
|
192
|
+
|
|
193
|
+
def _cache_dataset_info(self):
|
|
194
|
+
if not self._files:
|
|
195
|
+
self.reset()
|
|
196
|
+
try:
|
|
197
|
+
filename = self._files[0]
|
|
198
|
+
except IndexError:
|
|
199
|
+
raise RetryError("no lima files exists (yet)")
|
|
200
|
+
with h5py_utils.File(filename, mode="r") as f:
|
|
201
|
+
dset = f[self._path_in_file]
|
|
202
|
+
self._points_per_file = len(dset)
|
|
203
|
+
self._first_shape = dset.shape
|
|
204
|
+
self._dtype = dset.dtype
|
|
205
|
+
|
|
206
|
+
def __len__(self) -> int:
|
|
207
|
+
return self.shape[0]
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def point_per_file(self) -> int:
|
|
211
|
+
if self._points_per_file is None:
|
|
212
|
+
self._cache_dataset_info()
|
|
213
|
+
return self._points_per_file
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def dtype(self) -> DTypeLike:
|
|
217
|
+
if self._dtype is None:
|
|
218
|
+
self._cache_dataset_info()
|
|
219
|
+
return self._dtype
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def shape(self) -> Tuple[int]:
|
|
223
|
+
if self._shape is not None:
|
|
224
|
+
return self._shape
|
|
225
|
+
if self._first_shape is None:
|
|
226
|
+
self._cache_dataset_info()
|
|
227
|
+
shape = list(self._first_shape)
|
|
228
|
+
shape[0] *= len(self._files)
|
|
229
|
+
self._shape = tuple(shape)
|
|
230
|
+
return self._shape
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def size(self) -> int:
|
|
234
|
+
return numpy.prod(self.shape, dtype=int)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def ndim(self) -> int:
|
|
238
|
+
return 3
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def lima_url_template(
|
|
242
|
+
dirname: str,
|
|
243
|
+
scan_number: int,
|
|
244
|
+
bliss_detector_name: str,
|
|
245
|
+
url_template: Optional[str] = None,
|
|
246
|
+
user_detector_name: Optional[str] = None,
|
|
247
|
+
user_instrument_name: Optional[str] = None,
|
|
248
|
+
) -> str:
|
|
249
|
+
if not url_template:
|
|
250
|
+
if user_instrument_name:
|
|
251
|
+
url_template = os.path.join(
|
|
252
|
+
"{dirname}",
|
|
253
|
+
"scan{scan_number:04d}",
|
|
254
|
+
"{bliss_detector_name}_{{file_index}}.h5",
|
|
255
|
+
) + SEP.join(
|
|
256
|
+
(
|
|
257
|
+
"::",
|
|
258
|
+
"entry_0000",
|
|
259
|
+
"{user_instrument_name}",
|
|
260
|
+
"{user_detector_name}",
|
|
261
|
+
"data",
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
url_template = os.path.join(
|
|
266
|
+
"{dirname}",
|
|
267
|
+
"scan{scan_number:04d}",
|
|
268
|
+
"{bliss_detector_name}_{{file_index}}.h5",
|
|
269
|
+
) + SEP.join(("::", "entry_0000", "measurement", "data"))
|
|
270
|
+
if not user_detector_name:
|
|
271
|
+
user_detector_name = bliss_detector_name
|
|
272
|
+
kw = dict()
|
|
273
|
+
if dirname:
|
|
274
|
+
kw["dirname"] = dirname
|
|
275
|
+
if scan_number:
|
|
276
|
+
kw["scan_number"] = scan_number
|
|
277
|
+
if bliss_detector_name:
|
|
278
|
+
kw["bliss_detector_name"] = bliss_detector_name
|
|
279
|
+
if user_detector_name:
|
|
280
|
+
kw["user_detector_name"] = user_detector_name
|
|
281
|
+
if user_instrument_name:
|
|
282
|
+
kw["user_instrument_name"] = user_instrument_name
|
|
283
|
+
url_template = url_template.format(**kw)
|
|
284
|
+
if "{file_index}" not in url_template:
|
|
285
|
+
raise ValueError("A lima template URL needs '{{file_index}}'")
|
|
286
|
+
return url_template
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Tuple, Union, Sequence
|
|
2
|
+
from numbers import Number, Integral
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from types import EllipsisType
|
|
6
|
+
|
|
7
|
+
SingleDataIndexType = Union[Integral, slice, Sequence, EllipsisType]
|
|
8
|
+
except ImportError:
|
|
9
|
+
SingleDataIndexType = Union[Integral, slice, Sequence]
|
|
10
|
+
from numpy.typing import ArrayLike
|
|
11
|
+
|
|
12
|
+
DataType = Union[bytes, Number, ArrayLike]
|
|
13
|
+
DataIndexType = Union[SingleDataIndexType, Tuple[SingleDataIndexType]]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Manage Redis connections and provide a low-level API to Redis.
|
|
2
|
+
|
|
3
|
+
The low-level API exposes all Redis commands (https://redis.io/commands)
|
|
4
|
+
as methods of Redis database proxies (`RedisDbProxyBase` and derived classes).
|
|
5
|
+
|
|
6
|
+
Redis database proxies are created through a Redis connection pool (`RedisDbConnectionPool`).
|
|
7
|
+
|
|
8
|
+
Redis connection pools are created through a Redis connection manager (`RedisConnectionManager`).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .manager import RedisConnectionManager # noqa: F401
|
|
12
|
+
from .manager import RedisAddress # noqa: F401
|