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.
Files changed (67) hide show
  1. blissdata/__init__.py +17 -0
  2. blissdata/beacon/__init__.py +1 -0
  3. blissdata/beacon/_base.py +106 -0
  4. blissdata/beacon/config.py +24 -0
  5. blissdata/beacon/data.py +45 -0
  6. blissdata/beacon/files.py +141 -0
  7. blissdata/client.py +62 -0
  8. blissdata/common/__init__.py +15 -0
  9. blissdata/common/utils.py +159 -0
  10. blissdata/data/__init__.py +15 -0
  11. blissdata/data/events/__init__.py +22 -0
  12. blissdata/data/events/channel.py +149 -0
  13. blissdata/data/events/lima.py +478 -0
  14. blissdata/data/events/node.py +28 -0
  15. blissdata/data/events/scan.py +49 -0
  16. blissdata/data/events/walk.py +42 -0
  17. blissdata/data/expiration.py +42 -0
  18. blissdata/data/lima_image.py +465 -0
  19. blissdata/data/node.py +1619 -0
  20. blissdata/data/nodes/__init__.py +11 -0
  21. blissdata/data/nodes/channel.py +394 -0
  22. blissdata/data/nodes/dataset.py +82 -0
  23. blissdata/data/nodes/dataset_collection.py +12 -0
  24. blissdata/data/nodes/lima.py +422 -0
  25. blissdata/data/nodes/node_ref_channel.py +34 -0
  26. blissdata/data/nodes/proposal.py +12 -0
  27. blissdata/data/nodes/scan.py +192 -0
  28. blissdata/data/nodes/scan_group.py +12 -0
  29. blissdata/data/remote_node.py +204 -0
  30. blissdata/data/scan.py +666 -0
  31. blissdata/h5api/__init__.py +1 -0
  32. blissdata/h5api/abstract.py +97 -0
  33. blissdata/h5api/dynamic_hdf5.py +153 -0
  34. blissdata/h5api/file_arguments.py +28 -0
  35. blissdata/h5api/static_hdf5.py +139 -0
  36. blissdata/h5api/utils/__init__.py +0 -0
  37. blissdata/h5api/utils/bliss.py +138 -0
  38. blissdata/h5api/utils/hdf5.py +280 -0
  39. blissdata/h5api/utils/hdf5_retry.py +98 -0
  40. blissdata/h5api/utils/lima.py +286 -0
  41. blissdata/h5api/utils/types.py +13 -0
  42. blissdata/redis/__init__.py +12 -0
  43. blissdata/redis/caching.py +390 -0
  44. blissdata/redis/connection.py +169 -0
  45. blissdata/redis/manager.py +164 -0
  46. blissdata/redis/proxy.py +971 -0
  47. blissdata/redis/scripting.py +24 -0
  48. blissdata/settings.py +1174 -0
  49. blissdata/streaming.py +819 -0
  50. blissdata/streaming_events.py +355 -0
  51. blissdata/tests/__init__.py +0 -0
  52. blissdata/tests/beacon/__init__.py +0 -0
  53. blissdata/tests/beacon/test_config.py +26 -0
  54. blissdata/tests/beacon/test_data.py +50 -0
  55. blissdata/tests/beacon/test_files.py +80 -0
  56. blissdata/tests/conftest.py +0 -0
  57. blissdata/tests/h5api/__init__.py +0 -0
  58. blissdata/tests/h5api/scanner.py +390 -0
  59. blissdata/tests/h5api/test_dynamic_files.py +309 -0
  60. blissdata/tests/h5api/test_static_files.py +169 -0
  61. blissdata/tests/redis/__init__.py +0 -0
  62. blissdata/tests/redis/manager.py +23 -0
  63. blissdata-0.3.4.dist-info/LICENSE +165 -0
  64. blissdata-0.3.4.dist-info/METADATA +79 -0
  65. blissdata-0.3.4.dist-info/RECORD +67 -0
  66. blissdata-0.3.4.dist-info/WHEEL +5 -0
  67. blissdata-0.3.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of the bliss project
4
+ #
5
+ # Copyright (c) 2015-2023 Beamline Control Unit, ESRF
6
+ # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
+
8
+ """Data management
9
+
10
+ node data types that map to structure in redis
11
+ """
@@ -0,0 +1,394 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of the bliss project
4
+ #
5
+ # Copyright (c) 2015-2023 Beamline Control Unit, ESRF
6
+ # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
+
8
+ import numpy
9
+ import functools
10
+ from blissdata.data.node import DataNode
11
+ from blissdata.data.events import EventData, ChannelDataEvent
12
+
13
+
14
+ # Default length of published channels
15
+ CHANNEL_MAX_LEN = 2048
16
+
17
+
18
+ class RedisDataExpiredError(Exception):
19
+ pass
20
+
21
+
22
+ class _ChannelDataNodeBase(DataNode):
23
+ _NODE_TYPE = NotImplemented
24
+
25
+ def __init__(self, name, **kwargs):
26
+ super().__init__(self._NODE_TYPE, name, **kwargs)
27
+ self._queue = self._create_stream("data", maxlen=CHANNEL_MAX_LEN)
28
+ self._register_stream_priority(f"{self.db_name}_data", 2)
29
+ self._last_index = self._idx_to_streamid(0)
30
+
31
+ @classmethod
32
+ def _idx_to_streamid(cls, idx):
33
+ """Get the Redis stream ID from the sequence index
34
+
35
+ :param int idx:
36
+ :returns int:
37
+ """
38
+ # Redis can't has a stream ID 0
39
+ return idx + 1
40
+
41
+ @classmethod
42
+ def _streamid_to_idx(cls, streamID):
43
+ """
44
+ :param bytes streamID:
45
+ :returns int:
46
+ """
47
+ return super()._streamid_to_idx(streamID) - 1
48
+
49
+ def _init_info(self, **kwargs):
50
+ # This is a hack, just for self._create_struct. The name of this
51
+ # DataNode (which is used to create the db_name) will be replaced
52
+ # by the channel name after composing the db_name.
53
+ self.__channel_name = kwargs.get("channel_name", None)
54
+
55
+ # Take specific arguments to populate `info`
56
+ shape = kwargs.get("shape", None)
57
+ dtype = kwargs.get("dtype", None)
58
+ unit = kwargs.get("unit", None)
59
+ fullname = kwargs.get("fullname", None)
60
+ info = kwargs.get("info", {})
61
+ if kwargs.get("create", False):
62
+ if shape is not None:
63
+ info["shape"] = shape
64
+ if dtype is not None:
65
+ info["dtype"] = dtype
66
+ info["fullname"] = fullname
67
+ info["unit"] = unit
68
+ return info
69
+
70
+ def _subscribe_stream(self, stream_suffix, reader, first_index=None, **kw):
71
+ """Subscribe to a particular stream associated with this node.
72
+
73
+ :param str stream_suffix: stream to add is "{db_name}_{stream_suffix}"
74
+ :param DataStreamReader reader:
75
+ :param str or int first_index: Redis stream index (None is now)
76
+ """
77
+ if stream_suffix == "data":
78
+ # This stream has position indexing, not time indexing.
79
+ # No limit on the start index, so start from 0.
80
+ first_index = 0
81
+ super()._subscribe_stream(stream_suffix, reader, first_index=first_index, **kw)
82
+
83
+ def _subscribe_streams(self, reader, yield_events=False, **kw):
84
+ """Subscribe to all associated streams of this node.
85
+
86
+ :param DataStreamReader reader:
87
+ :param bool yield_events: yield Event or DataNode
88
+ :param **kw: see DataNode
89
+ """
90
+ super()._subscribe_streams(reader, yield_events=yield_events, **kw)
91
+ if yield_events:
92
+ self._subscribe_stream(
93
+ "data", reader, first_index=0, create=True, ignore_excluded=True
94
+ )
95
+
96
+ def __getitem__(self, idx):
97
+ """
98
+ :param int or slice idx: supports only slices with stride +1
99
+ """
100
+ if isinstance(idx, slice):
101
+ if idx.step not in (1, None):
102
+ raise IndexError("Stride not supported")
103
+ n = len(self)
104
+
105
+ if idx.start is None:
106
+ from_index = 0
107
+ else:
108
+ from_index = idx.start
109
+ if from_index < 0:
110
+ from_index += n
111
+
112
+ if idx.stop is None:
113
+ to_index = n
114
+ else:
115
+ to_index = idx.stop
116
+ if to_index < 0:
117
+ to_index += n
118
+
119
+ if from_index > to_index:
120
+ raise IndexError("Reverse order not supported")
121
+ elif from_index == to_index:
122
+ return numpy.array([])
123
+ to_index -= 1
124
+ elif idx is Ellipsis:
125
+ from_index = 0
126
+ to_index = -1
127
+ else:
128
+ try:
129
+ idx = int(idx)
130
+ except Exception as e:
131
+ raise IndexError from e
132
+ if idx < 0:
133
+ from_index = idx + len(self)
134
+ else:
135
+ from_index = idx
136
+ to_index = None
137
+ ret = self.get(from_index, to_index)
138
+ if to_index is None:
139
+ try:
140
+ if not ret:
141
+ raise IndexError("index out of range")
142
+ except ValueError:
143
+ # non-empty numpy.ndarray
144
+ pass
145
+ return ret
146
+
147
+ @property
148
+ def shape(self):
149
+ return self.info.get("shape")
150
+
151
+ @property
152
+ def dtype(self):
153
+ return self.info.get("dtype")
154
+
155
+ @property
156
+ def fullname(self):
157
+ """Same as AcquisitionChannel.fullname"""
158
+ return self.info.get("fullname")
159
+
160
+ @property
161
+ def short_name(self):
162
+ """Same as AcquisitionChannel.short_name"""
163
+ _, _, last_part = self.name.rpartition(":")
164
+ return last_part
165
+
166
+ def _create_struct(self, db_name, node_type):
167
+ node_struct = super()._create_struct(
168
+ db_name, node_type, name=self.__channel_name
169
+ )
170
+ return node_struct
171
+
172
+ @property
173
+ def unit(self):
174
+ return self.info.get("unit")
175
+
176
+ def get_db_names(self, **kw):
177
+ db_names = super().get_db_names(**kw)
178
+ db_names.append(self.db_name + "_data")
179
+ return db_names
180
+
181
+ def get_settings(self):
182
+ return super().get_settings() + [self._queue]
183
+
184
+ def store(self, event_dict, cnx=None):
185
+ """Publish channel data in Redis"""
186
+ raise NotImplementedError
187
+
188
+ def get(self, from_index, to_index=None):
189
+ """Returns an item or a slice.
190
+
191
+ :param int from_index: >= 0 (item at this index)
192
+ < 0 (last item (to_index is None), first item (to_index is not None))
193
+ None (first item)
194
+ :param int to_index: >= 0 (get slice until and including this index),
195
+ < 0 (get slice until the end)
196
+ None (get item at index from_index)
197
+ :returns numpy.ndarray, list, scalar, None or callable:
198
+ :raises IndexError: out of range when slicing and
199
+ from_index>0 and to_index<0
200
+ to_index>0
201
+ otherwise returns None or []
202
+ """
203
+ raise NotImplementedError
204
+
205
+ def get_as_array(self, from_index, to_index=None):
206
+ """Like `get` but ensures the result is a numpy array."""
207
+ return numpy.asarray(self.get(from_index, to_index), self.dtype)
208
+
209
+ def decode_raw_events(self, events):
210
+ """Decode raw stream data
211
+
212
+ :param list((index, raw)) events:
213
+ :returns EventData:
214
+ """
215
+ raise NotImplementedError
216
+
217
+
218
+ class ChannelDataNode(_ChannelDataNodeBase):
219
+ _NODE_TYPE = "channel"
220
+
221
+ def store(self, event_dict, cnx=None):
222
+ """Publish channel data in Redis"""
223
+ ev = ChannelDataEvent(event_dict.get("data"), event_dict["description"])
224
+ self._queue.add_event(ev, id=self._last_index, cnx=cnx)
225
+ self._last_index += ev.npoints
226
+
227
+ def get(self, from_index, to_index=None):
228
+ """Returns an item or a slice.
229
+
230
+ :param int from_index:
231
+ :param int to_index:
232
+ :returns numpy.ndarray, list, scalar, None or callable: only a list when no data
233
+ """
234
+ if from_index is None:
235
+ from_index = 0
236
+ if to_index is None:
237
+ return self._get_item(from_index)
238
+ else:
239
+ from_index = max(from_index, 0)
240
+ return self._get_slice(from_index, to_index)
241
+
242
+ def _get_item(self, index):
243
+ """Get data from a single point (None when the index does not exist).
244
+
245
+ :param int index: < 0: last item
246
+ >= 0: item at this index
247
+ :returns scalar, None or callable: None instead of IndexError
248
+ """
249
+ if index < 0:
250
+ events = self._queue.rev_range(count=1, cnx=self.db_connection)
251
+ else:
252
+ redis_index = self._idx_to_streamid(index)
253
+ events = self._queue_range(redis_index, redis_index)
254
+ return self._get_return(self._event_to_data, events, index)
255
+
256
+ def _get_slice(self, from_index, to_index):
257
+ """Get a data slice.
258
+
259
+ :param int from_index: positive integer
260
+ :param int to_index: < 0: till the end
261
+ >= 0: until and including this index
262
+ :returns numpy.ndarray, list, scalar or callable: a list when no data
263
+ """
264
+ if to_index < 0:
265
+ redis_to_index = "+" # means stream end
266
+ else:
267
+ redis_to_index = self._idx_to_streamid(to_index)
268
+ redis_from_index = self._idx_to_streamid(from_index)
269
+ events = self._queue_range(redis_from_index, redis_to_index)
270
+ return self._get_return(self._events_to_data, events, from_index, to_index)
271
+
272
+ def _get_return(self, events_to_data, events, *args):
273
+ """
274
+ :param callable or Any events_to_data:
275
+ """
276
+ if isinstance(events, list):
277
+ return events_to_data(*args, events)
278
+ else:
279
+ # pipeline case: should return the conversion function
280
+ return functools.partial(events_to_data, *args)
281
+
282
+ def _queue_range(self, from_index, to_index):
283
+ """The result includes `from_index` and `to_index` but
284
+ can be larger on both sides due to the block size.
285
+
286
+ :param int from_index:
287
+ :param int or str to_index:
288
+ :returns list(2-tuple) or callable:
289
+ :raises RuntimeError: when using a Redis pipeline to
290
+ get partial queue events
291
+ """
292
+ if from_index in [0, 1] and to_index == "+":
293
+ return self._queue.range(from_index, to_index, cnx=self.db_connection)
294
+ org_from_index = from_index
295
+ blocksize = 0
296
+ result = []
297
+ while True:
298
+ from_index = max(from_index - blocksize, 0)
299
+ events = self._queue.range(from_index, to_index, cnx=self.db_connection)
300
+ if not isinstance(events, list):
301
+ raise RuntimeError(
302
+ "Redis pipelines can only be used when retrieving the full queue range."
303
+ )
304
+ if events:
305
+ result = events + result
306
+ idx, raw = events[0]
307
+ first_index = self._idx_to_streamid(self._streamid_to_idx(idx))
308
+ if first_index <= org_from_index or from_index == 0:
309
+ break
310
+ to_index = first_index - 1
311
+ from_index = first_index
312
+ blocksize = ChannelDataEvent.decode_npoints(raw)
313
+ elif from_index == 0:
314
+ break
315
+ blocksize = max(blocksize, 1)
316
+ return result
317
+
318
+ def _events_to_data(self, from_index, to_index, events, single=False):
319
+ """
320
+ :param int from_index:
321
+ :param int to_index:
322
+ :param list((index, raw)) events:
323
+ :param bool single: requested a single value, not a slice
324
+ :returns numpy.ndarray or list: only a list when no data
325
+ """
326
+ event_data = self.decode_raw_events(events)
327
+ data = event_data.data
328
+ ndata = len(data)
329
+ first_index = event_data.first_index
330
+
331
+ # The last index is ALWAYS allowed to be higher than the available data
332
+ # The first index is SOMETIMES allowed to be lower than the available data
333
+ allow_lower = (to_index < 0 and from_index == 0) or single
334
+ is_lower = from_index < first_index or first_index < 0
335
+ if is_lower and not allow_lower:
336
+ raise IndexError(
337
+ "Data is not anymore available first_index:"
338
+ f"{first_index} request_index:{from_index}"
339
+ )
340
+
341
+ # Data can be larger on both sides (see _queue_range)
342
+ start = max(from_index - first_index, 0)
343
+ if to_index < 0:
344
+ stop = ndata
345
+ else:
346
+ nrequested = to_index - from_index + 1
347
+ stop = min(start + nrequested, ndata)
348
+ if stop - start == ndata:
349
+ return data
350
+ else:
351
+ return data[start:stop]
352
+
353
+ def _event_to_data(self, index, events):
354
+ """
355
+ :param int index:
356
+ :param list((index, raw)) events:
357
+ :returns scalar or None:
358
+ """
359
+ data = self._events_to_data(index, index, events, single=True)
360
+ try:
361
+ return data[-1]
362
+ except IndexError:
363
+ return None
364
+
365
+ def decode_raw_events(self, events):
366
+ """Decode and concatenate raw stream data
367
+
368
+ :param list((index, raw)) events:
369
+ :returns EventData:
370
+ """
371
+ data = list()
372
+ first_index = -1
373
+ description = None
374
+ block_size = 0
375
+ if events:
376
+ first_index = self._streamid_to_idx(events[0][0])
377
+ ev = ChannelDataEvent.merge(events)
378
+ data = ev.array
379
+ description = ev.description
380
+ block_size = ev.npoints
381
+ return EventData(
382
+ first_index=first_index,
383
+ data=data,
384
+ description=description,
385
+ block_size=block_size,
386
+ )
387
+
388
+ def __len__(self):
389
+ events = self._queue.rev_range(count=1)
390
+ if events:
391
+ evdata = self.decode_raw_events(events)
392
+ return evdata.first_index + evdata.block_size
393
+ else:
394
+ return 0
@@ -0,0 +1,82 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of the bliss project
4
+ #
5
+ # Copyright (c) 2015-2023 Beamline Control Unit, ESRF
6
+ # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
+
8
+ import re
9
+ from typing import Optional
10
+ import datetime
11
+ from blissdata.data.node import DataNodeContainer
12
+
13
+
14
+ class _DataPolicyNode(DataNodeContainer):
15
+ _NODE_TYPE = NotImplemented
16
+
17
+ def __init__(self, name, **kwargs):
18
+ super().__init__(self._NODE_TYPE, name, **kwargs)
19
+
20
+ @property
21
+ def path(self):
22
+ return self.info.get("__path__", None)
23
+
24
+ @property
25
+ def metadata(self):
26
+ return self.get_metadata()
27
+
28
+ @property
29
+ def metadata_fields(self):
30
+ return self.get_metadata_fields()
31
+
32
+ def get_metadata(self, pattern=None):
33
+ """
34
+ :param str pattern: regex pattern for field name
35
+ :returns dict:
36
+ """
37
+ is_valid = self._field_name_filter(pattern=pattern)
38
+ return {k: v for k, v in self.info.items() if is_valid(k)}
39
+
40
+ def get_metadata_fields(self, pattern=None):
41
+ """
42
+ :param str pattern: regex pattern for field name
43
+ :returns set:
44
+ """
45
+ is_valid = self._field_name_filter(pattern=pattern)
46
+ return {k for k in self.info.keys() if is_valid(k)}
47
+
48
+ def _field_name_filter(self, pattern=None):
49
+ """
50
+ :param str pattern: regex pattern for field name
51
+ :returns callable:
52
+ """
53
+ if pattern:
54
+ pattern_obj = re.compile(pattern)
55
+ return lambda name: not name.startswith("__") and pattern_obj.match(name)
56
+ else:
57
+ return lambda name: not name.startswith("__")
58
+
59
+
60
+ class DatasetNode(_DataPolicyNode):
61
+ _NODE_TYPE = "dataset"
62
+
63
+ def __init__(self, name, create=False, **kwargs):
64
+ super().__init__(name, create=create, **kwargs)
65
+ if create and self.start_date is None:
66
+ self.info["startDate"] = datetime.datetime.now()
67
+
68
+ @property
69
+ def is_closed(self):
70
+ return self.info.get("__closed__", False)
71
+
72
+ @property
73
+ def is_registered(self):
74
+ return self.info.get("__registered__", False)
75
+
76
+ @property
77
+ def start_date(self) -> Optional[datetime.datetime]:
78
+ return self.info.get("startDate")
79
+
80
+ @property
81
+ def end_date(self) -> Optional[datetime.datetime]:
82
+ return self.info.get("endDate")
@@ -0,0 +1,12 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of the bliss project
4
+ #
5
+ # Copyright (c) 2015-2023 Beamline Control Unit, ESRF
6
+ # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
+
8
+ from blissdata.data.nodes.dataset import _DataPolicyNode
9
+
10
+
11
+ class DatasetCollectionNode(_DataPolicyNode):
12
+ _NODE_TYPE = "dataset_collection"