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,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"
|