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,355 @@
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
+ Events that are added or read from a Redis stream have
9
+ a stream ID and carry a payload of type `dict`. The keys
10
+ and values need to be of type `bytes`.
11
+
12
+ To encode a custom data type to a raw stream payload
13
+ (and decode again when reading) a class can to be derived
14
+ from `StreamEvent`
15
+
16
+ class MyStreamEvent(StreamEvent):
17
+ TYPE = b"MYTYPE"
18
+
19
+ def init(self, var1, var2=None):
20
+ ...
21
+
22
+ def _encode(self):
23
+ raw = super()._encode()
24
+ ... # add var1 and var2 to raw
25
+ return raw
26
+
27
+ def _decode(self, raw):
28
+ super()._decode(raw)
29
+ ... # get var1 and var2 from raw
30
+
31
+ To prepare data to be added to a Redis stream:
32
+
33
+ event = MyStreamEvent(var1, var2=...)
34
+ raw = event.encode()
35
+
36
+ The data of a Redis stream can be decoded as follows:
37
+
38
+ event = StreamEvent.factory(raw)
39
+
40
+ The factory method allows up-casting:
41
+
42
+ event = MyStreamEvent(var1, var2=...)
43
+ event = StreamEvent.factory(event.encode())
44
+ assert isinstance(event, MyStreamEvent)
45
+
46
+ If you don't want up-casting (avoid unnecessary decoding):
47
+
48
+ event = MyStreamEvent(raw={...})
49
+
50
+ Down-casting raises an exception (missing data):
51
+
52
+ event = StreamEvent()
53
+ try:
54
+ event = MyStreamEvent.factory(event.encode())
55
+ except StreamDecodeError:
56
+ ...
57
+
58
+ To know whether data from a Redis stream is of a
59
+ particular type without decoding it:
60
+
61
+ if MyStreamEvent.istype(raw):
62
+ ...
63
+
64
+ To know whether an event is of a particular type
65
+
66
+ if event.TYPE == event.types.MYTYPE:
67
+ ...
68
+
69
+ if isinstance(event, MyStreamEvent):
70
+ ...
71
+ """
72
+
73
+ import pickle
74
+ import datetime
75
+ import numbers
76
+ import functools
77
+
78
+
79
+ class StreamError(Exception):
80
+ pass
81
+
82
+
83
+ class StreamEncodeError(StreamError):
84
+ pass
85
+
86
+
87
+ class StreamDecodeError(StreamError):
88
+ pass
89
+
90
+
91
+ class StreamEventMeta(type):
92
+ """Checks whether StreamEvent is being derived properly.
93
+ Makes sure the class gets up-casted upon instantiation
94
+ with data from a DataStream.
95
+ """
96
+
97
+ def __new__(self, name, bases, attr):
98
+ """Register class when defined"""
99
+ cls = super().__new__(self, name, bases, attr)
100
+ if hasattr(cls, "_SUBCLASS_REGISTRY"):
101
+ # Register the subclass
102
+ event_type = cls.TYPE
103
+ ecls = cls._SUBCLASS_REGISTRY.get(event_type)
104
+ if ecls is not None:
105
+ raise NotImplementedError(
106
+ f"Event type {event_type} is already taken by class {ecls.__name__}"
107
+ )
108
+ if not isinstance(event_type, bytes):
109
+ raise NotImplementedError("{cls.__name__}.TYPE must be of type 'bytes'")
110
+ cls._SUBCLASS_REGISTRY[event_type] = cls
111
+ else:
112
+ # Register the base class
113
+ cls._SUBCLASS_REGISTRY = {None: cls, cls.TYPE: cls}
114
+ return cls
115
+
116
+
117
+ class _EventTypes:
118
+ def __getattr__(self, attr):
119
+ attr = attr.upper().encode()
120
+ if attr in StreamEvent._SUBCLASS_REGISTRY:
121
+ return attr
122
+ raise AttributeError(attr)
123
+
124
+
125
+ class StreamEvent(metaclass=StreamEventMeta):
126
+ """Base class to encode/decode DataStream events."""
127
+
128
+ TYPE = b"UNKNOWN"
129
+ TYPE_KEY = b"__EVENT__"
130
+ types = _EventTypes()
131
+
132
+ @classmethod
133
+ def _use_class(cls, raw):
134
+ event_type = raw.get(cls.TYPE_KEY, b"")
135
+ try:
136
+ # Get the proper class for this event type
137
+ return cls._SUBCLASS_REGISTRY[event_type]
138
+ except KeyError:
139
+ # Event type is missing or unknown:
140
+ # use the base class
141
+ return cls._SUBCLASS_REGISTRY[None]
142
+
143
+ @classmethod
144
+ def istype(cls, raw):
145
+ usecls = cls._use_class(raw)
146
+ # issubclass does no work?
147
+ # return issubclass(cls, usecls)
148
+ return cls in usecls.__mro__
149
+
150
+ @classmethod
151
+ def isstricttype(cls, raw):
152
+ return raw.get(cls.TYPE_KEY, b"") == cls.TYPE
153
+
154
+ @classmethod
155
+ def class_factory(cls, raw):
156
+ """Returns the class StreamEvent or one of the derived
157
+ classes, depending of the event type.
158
+
159
+ :param dict raw: DataStream event data
160
+ :raises StreamDecodeError:
161
+ :returns type:
162
+ """
163
+ usecls = cls._use_class(raw)
164
+ # issubclass does not work???
165
+ # if not issubclass(cls, usecls):
166
+ if cls not in usecls.__mro__:
167
+ # Do not allow down-casting (up-casting is allowed)
168
+ raise StreamDecodeError(f"Event not of type {cls.TYPE}: {raw}")
169
+ return usecls
170
+
171
+ @classmethod
172
+ def factory(cls, raw):
173
+ """The returned object is on instance of
174
+ this class or a derived classes, depending
175
+ of the event type.
176
+
177
+ :param dict raw: DataStream event data
178
+ :raises StreamDecodeError:
179
+ :returns StreamEvent:
180
+ """
181
+ usecls = cls.class_factory(raw=raw)
182
+ return usecls(raw=raw)
183
+
184
+ @classmethod
185
+ def merge_factory(cls, events):
186
+ """
187
+ :param list((index, raw)) events:
188
+ :returns StreamEvent:
189
+ """
190
+ if not events:
191
+ return
192
+ usecls = cls.class_factory(events[0][1])
193
+ return usecls.merge(events)
194
+
195
+ def __init__(self, *args, raw=None, **kw):
196
+ if raw is None:
197
+ self.init(*args, **kw)
198
+ else:
199
+ self.decode(raw)
200
+
201
+ def __str__(self):
202
+ return f"{self.__class__.__name__}({self.TYPE})"
203
+
204
+ def init(self):
205
+ pass
206
+
207
+ def encode(self):
208
+ """
209
+ :raises StreamEncodeError:
210
+ :returns dict: DataStream event data
211
+ """
212
+ try:
213
+ return self._encode()
214
+ except Exception as e:
215
+ raise StreamEncodeError from e
216
+
217
+ def decode(self, raw):
218
+ """
219
+ :param dict raw: DataStream event data
220
+ :raises StreamDecodeError:
221
+ """
222
+ try:
223
+ self._decode(raw)
224
+ except Exception as e:
225
+ raise StreamDecodeError from e
226
+
227
+ def _encode(self):
228
+ return {self.TYPE_KEY: self.TYPE}
229
+
230
+ def _decode(self, raw):
231
+ if not self.istype(raw):
232
+ raise TypeError(f"Event not of type {self.TYPE}: {raw}")
233
+
234
+ @staticmethod
235
+ def generic_encode(data, **kw):
236
+ """
237
+ :param Any data:
238
+ :returns bytes:
239
+ """
240
+ return pickle.dumps(data, **kw)
241
+
242
+ @staticmethod
243
+ def generic_decode(data, **kw):
244
+ """
245
+ :param Any data:
246
+ :returns Any:
247
+ """
248
+ return pickle.loads(data, **kw)
249
+
250
+ @classmethod
251
+ def encode_bytes(cls, data):
252
+ return data
253
+
254
+ @classmethod
255
+ def decode_bytes(cls, data):
256
+ return data
257
+
258
+ @classmethod
259
+ def encode_string(cls, data):
260
+ return data.encode()
261
+
262
+ @classmethod
263
+ def decode_string(cls, data):
264
+ return data.decode()
265
+
266
+ @classmethod
267
+ def encode_integral(cls, data):
268
+ return b"%d" % data
269
+
270
+ @classmethod
271
+ def decode_integral(cls, data, int_type=int):
272
+ return int_type(data.decode())
273
+
274
+ @classmethod
275
+ def encode_decode_methods(cls, data_type):
276
+ if issubclass(data_type, bytes):
277
+ return cls.encode_bytes, cls.decode_bytes
278
+ elif issubclass(data_type, str):
279
+ return cls.encode_string, cls.decode_string
280
+ elif issubclass(data_type, numbers.Integral):
281
+ decode = functools.partial(cls.decode_integral, int_type=data_type)
282
+ return cls.encode_integral, decode
283
+ else:
284
+ return cls.generic_encode, cls.generic_decode
285
+
286
+ @classmethod
287
+ def merge(cls, events):
288
+ """
289
+ :param list((index, raw)) events:
290
+ :returns StreamEvent:
291
+ """
292
+ raise NotImplementedError
293
+
294
+
295
+ class TimeEvent(StreamEvent):
296
+
297
+ TYPE = b"TIME"
298
+ TIMESTAMP_KEY = b"__TIMESTAMP__"
299
+ STRTIME_KEY = b"__STRTIME__"
300
+
301
+ STRTIME_FMT = "%a %b %d %H:%M:%S %Y"
302
+
303
+ def init(self):
304
+ self.time = datetime.datetime.now()
305
+
306
+ @property
307
+ def timestamp(self):
308
+ return self.time.timestamp()
309
+
310
+ @timestamp.setter
311
+ def timestamp(self, epoch):
312
+ self.time = datetime.datetime.fromtimestamp(float(epoch))
313
+
314
+ @property
315
+ def strftime(self):
316
+ return self.time.strftime(self.STRTIME_FMT)
317
+
318
+ def _encode(self):
319
+ raw = super()._encode()
320
+ raw[self.TIMESTAMP_KEY] = self.generic_encode(self.timestamp)
321
+ raw[self.STRTIME_KEY] = self.encode_string(self.strftime)
322
+ return raw
323
+
324
+ def _decode(self, raw):
325
+ super()._decode(raw)
326
+ self.timestamp = self.generic_decode(raw[self.TIMESTAMP_KEY])
327
+
328
+
329
+ class StartEvent(TimeEvent):
330
+
331
+ TYPE = b"START"
332
+
333
+
334
+ class PreparedEvent(TimeEvent):
335
+
336
+ TYPE = b"PREPARED"
337
+
338
+
339
+ class EndEvent(TimeEvent):
340
+
341
+ TYPE = b"END"
342
+ EXCEPTION_KEY = b"__EXCEPTION__"
343
+
344
+ def init(self, exception=None, **kw):
345
+ super().init(**kw)
346
+ self.exception = exception
347
+
348
+ def _encode(self):
349
+ raw = super()._encode()
350
+ raw[self.EXCEPTION_KEY] = self.generic_encode(self.exception)
351
+ return raw
352
+
353
+ def _decode(self, raw):
354
+ super()._decode(raw)
355
+ self.exception = self.generic_decode(raw[self.EXCEPTION_KEY])
File without changes
File without changes
@@ -0,0 +1,26 @@
1
+ import pytest
2
+ from blissdata.beacon import config
3
+
4
+
5
+ def test_beacon_address(mocker):
6
+ mock_environ = mocker.patch("os.environ.get", return_value="foo:200")
7
+ result = config.get_beacon_address()
8
+ assert result == ("foo", 200)
9
+ mock_environ.assert_called_with("BEACON_HOST")
10
+
11
+
12
+ def test_missing_beacon_address(mocker):
13
+ mock_environ = mocker.patch("os.environ.get", return_value=None)
14
+ with pytest.raises(ValueError) as excinfo:
15
+ config.get_beacon_address()
16
+ assert "BEACON_HOST" in excinfo.value.args[0]
17
+ mock_environ.assert_called_with("BEACON_HOST")
18
+
19
+
20
+ def test_wrong_beacon_address_formatting(mocker):
21
+ mock_environ = mocker.patch("os.environ.get", return_value="foo")
22
+ with pytest.raises(ValueError) as excinfo:
23
+ config.get_beacon_address()
24
+ assert "BEACON_HOST" in excinfo.value.args[0]
25
+ assert "foo" in excinfo.value.args[0]
26
+ mock_environ.assert_called_with("BEACON_HOST")
@@ -0,0 +1,50 @@
1
+ from blissdata.beacon.data import BeaconData
2
+
3
+
4
+ def test_empty_constructor(mocker):
5
+ mock_socket = mocker.patch("socket.socket")
6
+ mock_config = mocker.patch(
7
+ "blissdata.beacon.config.get_beacon_address", return_value=("foo", 100)
8
+ )
9
+ _ = BeaconData()
10
+ mock_config.assert_called_once()
11
+ mock_socket.return_value.connect.assert_called_with(("foo", 100))
12
+
13
+
14
+ def test_param_constructor(mocker):
15
+ mock_socket = mocker.patch("socket.socket")
16
+ mock_config = mocker.patch(
17
+ "blissdata.beacon.config.get_beacon_address", return_value=("foo", 100)
18
+ )
19
+ _ = BeaconData("foo", 200)
20
+ mock_config.assert_not_called()
21
+ mock_socket.return_value.connect.assert_called_with(("foo", 200))
22
+
23
+
24
+ def test_get_redis_db(mocker):
25
+ RESPONSE1 = b"\x1f\x00\x00\x00C\x00\x00\x00localhost:/tmp/demo_resourcesnd1ti3cr/configuration/redis_demo.sock"
26
+ mock_socket = mocker.patch("socket.socket")
27
+ mock_socket.return_value.recv.side_effect = [RESPONSE1]
28
+ service = BeaconData("foo", 200)
29
+ result = service.get_redis_db()
30
+ mock_socket.return_value.sendall.assert_called_once_with(
31
+ b"\x1e\x00\x00\x00\x00\x00\x00\x00"
32
+ )
33
+ assert (
34
+ result == "localhost:/tmp/demo_resourcesnd1ti3cr/configuration/redis_demo.sock"
35
+ )
36
+
37
+
38
+ def test_get_redis_data_db(mocker):
39
+ RESPONSE1 = b'"\x00\x00\x00J\x00\x00\x001|localhost|/tmp/demo_resourcesnd1ti3cr/configuration/redis_data_demo.sock'
40
+ mock_socket = mocker.patch("socket.socket")
41
+ mock_socket.return_value.recv.side_effect = [RESPONSE1]
42
+ service = BeaconData("foo", 200)
43
+ result = service.get_redis_data_db()
44
+ mock_socket.return_value.sendall.assert_called_once_with(
45
+ b"\x20\x00\x00\x00\x02\x00\x00\x001|"
46
+ )
47
+ assert (
48
+ result
49
+ == "localhost:/tmp/demo_resourcesnd1ti3cr/configuration/redis_data_demo.sock"
50
+ )
@@ -0,0 +1,80 @@
1
+ import pytest
2
+ from blissdata.beacon.files import BeaconFiles, read_config, yaml_load
3
+
4
+
5
+ def test_empty_constructor(mocker):
6
+ mock_socket = mocker.patch("socket.socket")
7
+ mock_config = mocker.patch(
8
+ "blissdata.beacon.config.get_beacon_address", return_value=("foo", 100)
9
+ )
10
+ _ = BeaconFiles()
11
+ mock_config.assert_called_once()
12
+ mock_socket.return_value.connect.assert_called_with(("foo", 100))
13
+
14
+
15
+ def test_param_constructor(mocker):
16
+ mock_socket = mocker.patch("socket.socket")
17
+ mock_config = mocker.patch(
18
+ "blissdata.beacon.config.get_beacon_address", return_value=("foo", 100)
19
+ )
20
+ _ = BeaconFiles("foo", 200)
21
+ mock_config.assert_not_called()
22
+ mock_socket.return_value.connect.assert_called_with(("foo", 200))
23
+
24
+
25
+ def test_get_file(mocker):
26
+ RESPONSE1 = b"\x34\x00\x00\x00\x0E\x00\x00\x001|MY FILE DATA"
27
+ mock_socket = mocker.patch("socket.socket")
28
+ mock_socket.return_value.recv.side_effect = [RESPONSE1]
29
+ service = BeaconFiles("foo", 200)
30
+ result = service.get_file("sessions/__init__.yml")
31
+ mock_socket.return_value.sendall.assert_called_once_with(
32
+ b"2\x00\x00\x00\x17\x00\x00\x001|sessions/__init__.yml"
33
+ )
34
+ assert result == b"MY FILE DATA"
35
+
36
+
37
+ def test_get_tree(mocker):
38
+ RESPONSE1 = b"""\x58\x00\x00\x00\x2C\x00\x00\x001|{"afile":null,"adir":{"anotherfile":null}}"""
39
+ mock_socket = mocker.patch("socket.socket")
40
+ mock_socket.return_value.recv.side_effect = [RESPONSE1]
41
+ service = BeaconFiles("foo", 200)
42
+ result = service.get_tree("")
43
+ mock_socket.return_value.sendall.assert_called_once_with(
44
+ b"V\x00\x00\x00\x02\x00\x00\x001|"
45
+ )
46
+ assert result == {"adir": {"anotherfile": None}, "afile": None}
47
+
48
+
49
+ @pytest.mark.skipif(yaml_load is None, reason="No yaml lib found")
50
+ def test_read_config__from_file(tmp_path):
51
+ filename = str(tmp_path / "test.yml")
52
+ with open(filename, "wt") as f:
53
+ f.write("foo:\n bar: 2000\n")
54
+ result = read_config(filename)
55
+ assert result == {"foo": {"bar": 2000}}
56
+
57
+
58
+ @pytest.mark.skipif(yaml_load is None, reason="No yaml lib found")
59
+ def test_read_config__from_beacon(mocker, tmp_path):
60
+ RESPONSE1 = b"\x34\x00\x00\x00\x15\x00\x00\x001|foo:\n bar: 2000\n"
61
+ mock_socket = mocker.patch("socket.socket")
62
+ mock_socket.return_value.recv.side_effect = [RESPONSE1]
63
+ mock_config = mocker.patch("blissdata.beacon.config.get_beacon_address")
64
+ mock_config.return_value = ("foo", 100)
65
+ result = read_config("beacon:///foo.yml")
66
+ assert result == {"foo": {"bar": 2000}}
67
+ mock_config.assert_called_once()
68
+ mock_socket.return_value.connect.assert_called_with(("foo", 100))
69
+
70
+
71
+ @pytest.mark.skipif(yaml_load is None, reason="No yaml lib found")
72
+ def test_read_config__from_explicit_beacon(mocker, tmp_path):
73
+ RESPONSE1 = b"\x34\x00\x00\x00\x15\x00\x00\x001|foo:\n bar: 2000\n"
74
+ mock_socket = mocker.patch("socket.socket")
75
+ mock_socket.return_value.recv.side_effect = [RESPONSE1]
76
+ mock_config = mocker.patch("blissdata.beacon.config.get_beacon_address")
77
+ result = read_config("beacon://foo:200/foo.yml")
78
+ assert result == {"foo": {"bar": 2000}}
79
+ mock_config.assert_not_called()
80
+ mock_socket.return_value.connect.assert_called_with(("foo", 200))
File without changes
File without changes