blissdata-lima2 2.0.1__tar.gz

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.
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2020-2024 Beamline Control Unit, ESRF
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: blissdata-lima2
3
+ Version: 2.0.1
4
+ Summary: Lima2 plugin for blissdata
5
+ Maintainer: BCU (ESRF)
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://gitlab.esrf.fr/bliss/blissdata-lima2
8
+ Keywords: blissdata,lima2
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE.md
14
+ Requires-Dist: blissdata~=2.3
15
+ Requires-Dist: lima2-client~=4.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: black; extra == "dev"
18
+ Requires-Dist: flake8; extra == "dev"
19
+ Requires-Dist: mypy; extra == "dev"
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Requires-Dist: pytest-cov; extra == "dev"
22
+ Requires-Dist: pytest-redis; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ ## blissdata-lima2
26
+
27
+ Lima2 plugin for blissdata.
@@ -0,0 +1,3 @@
1
+ ## blissdata-lima2
2
+
3
+ Lima2 plugin for blissdata.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "blissdata-lima2"
7
+ version = "2.0.1"
8
+ description = "Lima2 plugin for blissdata"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE.md"]
12
+ maintainers = [{ name = "BCU (ESRF)" }]
13
+ classifiers = [
14
+ "Intended Audience :: Science/Research",
15
+ "Programming Language :: Python :: 3",
16
+ ]
17
+ keywords = ["blissdata", "lima2"]
18
+ requires-python = ">=3.10"
19
+ dependencies = ["blissdata~=2.3", "lima2-client~=4.0"]
20
+
21
+ [project.urls]
22
+ Repository = "https://gitlab.esrf.fr/bliss/blissdata-lima2"
23
+
24
+ [project.optional-dependencies]
25
+ dev = ["black", "flake8", "mypy", "pytest", "pytest-cov", "pytest-redis"]
26
+
27
+ [project.entry-points.blissdata]
28
+ lima2 = "blissdata_lima2"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from .stream import Lima2Stream, Lima2View # noqa: F401
2
+
3
+ stream_cls = Lima2Stream
4
+ view_cls = Lima2View
@@ -0,0 +1,349 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of the bliss project
4
+ #
5
+ # Copyright (c) Beamline Control Unit, ESRF
6
+ # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
+
8
+ import logging
9
+ from collections.abc import Sequence
10
+
11
+ import lima2.client.services as l2s
12
+ import numpy as np
13
+ from blissdata.exceptions import (
14
+ EmptyViewException,
15
+ EndOfStream,
16
+ IndexNoMoreThereError,
17
+ IndexNotYetThereError,
18
+ IndexWontBeThereError,
19
+ )
20
+ from blissdata.h5api import dynamic_hdf5
21
+ from blissdata.lima.image_utils import ImageData
22
+ from blissdata.streams import (
23
+ BaseStream,
24
+ BaseView,
25
+ EventRange,
26
+ StreamDefinition,
27
+ )
28
+ from blissdata.streams.default import Stream
29
+ from blissdata.streams.encoding.numeric import NumericStreamEncoder
30
+ from blissdata.streams.event_stream import EventStream
31
+ from blissdata.streams.lima.stream import LimaDirectAccess
32
+ from lima2.common.devencoded.sparse_frame import SparseFrame
33
+ from numpy.typing import DTypeLike
34
+
35
+ _logger = logging.getLogger(__name__)
36
+
37
+
38
+ def get_frame(
39
+ services: l2s.ConductorServices,
40
+ acq_uuid: str,
41
+ source: str,
42
+ frame_idx: int,
43
+ ) -> ImageData:
44
+ frm = services.pipeline.get_frame(frame_idx=frame_idx, source=source, uuid=acq_uuid)
45
+
46
+ if isinstance(frm, SparseFrame):
47
+ frm = frm.densify()
48
+
49
+ return ImageData(array=frm.data, frame_id=frm.idx, acq_tag=None)
50
+
51
+
52
+ class Lima2View(BaseView):
53
+ def __init__(
54
+ self,
55
+ services: l2s.ConductorServices,
56
+ acq_uuid: str,
57
+ source: str,
58
+ start: int,
59
+ stop: int,
60
+ ) -> None:
61
+ self._services = services
62
+ """Lima2 client services."""
63
+ self._acq_uuid = acq_uuid
64
+ """Lima2 acquisition id."""
65
+ self._source = source
66
+ """Frame source name."""
67
+ self._idx_range = range(start, stop)
68
+ """Range of absolute frame indices accessible via this view."""
69
+
70
+ @property
71
+ def index(self) -> int:
72
+ return self._idx_range.start
73
+
74
+ def __len__(self) -> int:
75
+ return len(self._idx_range)
76
+
77
+ def get_data(self, start: int | None = None, stop: int | None = None) -> np.ndarray:
78
+ frames = []
79
+ for idx in self._idx_range[start:stop]:
80
+ try:
81
+ frames.append(
82
+ get_frame(
83
+ services=self._services,
84
+ acq_uuid=self._acq_uuid,
85
+ source=self._source,
86
+ frame_idx=idx,
87
+ ).array
88
+ )
89
+ except RuntimeError as e:
90
+ # Raise if any frame can't be accessed
91
+ raise IndexNoMoreThereError(
92
+ f"Can't fetch {self._source} {self._idx_range[start:stop]}: {e}"
93
+ ) from e
94
+ except ValueError as e:
95
+ # Frame source has no associated memory buffer (raw_frame)
96
+ raise IndexNoMoreThereError(
97
+ f"Can't fetch {self._source}: no associated live data"
98
+ ) from e
99
+
100
+ return np.asarray(frames)
101
+
102
+
103
+ class Lima2Stream(BaseStream, LimaDirectAccess):
104
+ """Stream of Lima2 frames.
105
+
106
+ Frames don't actually transit inside the stream. The stream length can be
107
+ queried to determine the number of accessible frames.
108
+
109
+ Indexing or slicing the stream attempts to fetch frames directly from the
110
+ Lima2 backend.
111
+ """
112
+
113
+ PROTOCOL_VERSION = 2
114
+
115
+ def __init__(self, event_stream: EventStream) -> None:
116
+ BaseStream.__init__(self, event_stream)
117
+
118
+ _logger.debug(f"Instantiate Lima2Stream with {event_stream.info=}")
119
+
120
+ if event_stream.info["protocol_version"] != Lima2Stream.PROTOCOL_VERSION:
121
+ raise RuntimeError(
122
+ f"Lima2 protocol version mismatch "
123
+ f"(expected {Lima2Stream.PROTOCOL_VERSION}, "
124
+ f"got {event_stream.info['protocol_version']})"
125
+ )
126
+
127
+ self._dtype = np.dtype(event_stream.info["dtype"])
128
+ self._shape = tuple(event_stream.info["shape"])
129
+ self._acq_uuid = str(event_stream.info["acq_uuid"])
130
+ self._source = str(event_stream.info["source_name"])
131
+ self._master_file: tuple[str, str] | None = event_stream.info["master_file"]
132
+
133
+ self._services = l2s.init(
134
+ hostname=str(event_stream.info["conductor_hostname"]),
135
+ port=int(event_stream.info["conductor_port"]),
136
+ )
137
+ """Lima2 client session."""
138
+
139
+ self._length = 0
140
+ """Current number of accessible frames."""
141
+
142
+ self._cursor = Stream(event_stream).cursor()
143
+
144
+ ####################################################################################
145
+ # BaseStream
146
+ ####################################################################################
147
+
148
+ @property
149
+ def kind(self) -> str:
150
+ return "array"
151
+
152
+ @staticmethod
153
+ def make_definition(
154
+ name: str,
155
+ source_name: str,
156
+ conductor_hostname: str,
157
+ conductor_port: int,
158
+ acq_uuid: str,
159
+ master_file: tuple[str, str] | None,
160
+ dtype: DTypeLike,
161
+ shape: Sequence[int],
162
+ ) -> StreamDefinition:
163
+ info = {
164
+ "plugin": "lima2",
165
+ "dtype": np.dtype(dtype).name,
166
+ "shape": shape,
167
+ "protocol_version": Lima2Stream.PROTOCOL_VERSION,
168
+ "acq_uuid": acq_uuid,
169
+ "source_name": source_name,
170
+ "conductor_hostname": conductor_hostname,
171
+ "conductor_port": conductor_port,
172
+ "master_file": master_file,
173
+ }
174
+
175
+ return StreamDefinition(name, info, NumericStreamEncoder(np.uint32))
176
+
177
+ @property
178
+ def plugin(self) -> str:
179
+ return "lima2"
180
+
181
+ @property
182
+ def dtype(self) -> np.dtype:
183
+ return self._dtype
184
+
185
+ @property
186
+ def shape(self) -> tuple[int, ...]:
187
+ return self._shape
188
+
189
+ def __len__(self) -> int:
190
+ try:
191
+ view = self._cursor.read(block=False, last_only=True)
192
+ except EndOfStream:
193
+ view = None
194
+
195
+ if view is not None:
196
+ last_status = view.get_data()[0]
197
+ self._length = int(last_status)
198
+ return self._length
199
+
200
+ def __getitem__(self, key: int | slice) -> np.ndarray:
201
+ sealed = self.is_sealed()
202
+ if isinstance(key, int):
203
+ size = len(self)
204
+ if key < 0:
205
+ if not sealed:
206
+ raise IndexNotYetThereError(
207
+ "Can't index from end of stream until it is sealed"
208
+ )
209
+ else:
210
+ key += size
211
+
212
+ if not 0 <= key < size:
213
+ # Fail early if we're out of bounds
214
+ if sealed:
215
+ raise IndexWontBeThereError(
216
+ f"Frame {key} is out of range (0..{size-1})"
217
+ )
218
+ else:
219
+ raise IndexNotYetThereError(
220
+ f"Frame {key} is out of range (0..{size-1})"
221
+ )
222
+
223
+ return self._fetch_frames(start=key, stop=key + 1, step=1)[0]
224
+
225
+ elif isinstance(key, slice):
226
+ if (key.start or 0) < 0 or (key.stop or 0) < 0:
227
+ if not sealed:
228
+ raise IndexNotYetThereError(
229
+ "Can't slice from end of stream until it is sealed"
230
+ )
231
+
232
+ # Resolve the slice in the standard way
233
+ start, stop, step = key.indices(len(self))
234
+
235
+ return self._fetch_frames(start=start, stop=stop, step=step)
236
+
237
+ else:
238
+ raise TypeError(f"{type(key)}")
239
+
240
+ def _need_last_only(self, last_only: bool) -> bool:
241
+ # Lima2 event stream represents current progress
242
+ # -> only the latest one is relevant.
243
+ return True
244
+
245
+ def _build_view_from_events(
246
+ self, index: int, events: EventRange, last_only: bool
247
+ ) -> Lima2View:
248
+ """
249
+ Build a Lima2View to access a slice of frames which starts at `index`,
250
+ and ends at the most recent frame according to `events`.
251
+ """
252
+ _logger.debug(f"{self.name}: {index=} -> {events=}")
253
+
254
+ # events.data[-1] corresponds to the current number of contiguous frames
255
+ # accessible from the lima2 backend.
256
+ stop_idx = events.data[-1]
257
+
258
+ if stop_idx <= index:
259
+ # no new image despite new events
260
+ raise EmptyViewException
261
+
262
+ return Lima2View(
263
+ services=self._services,
264
+ acq_uuid=self._acq_uuid,
265
+ source=self._source,
266
+ start=stop_idx - 1 if last_only else index,
267
+ stop=stop_idx,
268
+ )
269
+
270
+ ####################################################################################
271
+ # LimaDirectAccess
272
+ ####################################################################################
273
+
274
+ def get_last_live_image(self) -> ImageData:
275
+ return get_frame(
276
+ services=self._services,
277
+ acq_uuid=self._acq_uuid,
278
+ source=self._source,
279
+ frame_idx=-1,
280
+ )
281
+
282
+ ####################################################################################
283
+ # Private API
284
+ ####################################################################################
285
+
286
+ def _fetch_frames(self, start: int, stop: int, step: int) -> np.ndarray:
287
+ """Get frame data online or offline, depending on the current state."""
288
+ if self.is_sealed():
289
+ if self._master_file is not None:
290
+ # Default to an offline lookup
291
+ filepath, datapath = self._master_file
292
+ return self._fetch_from_disk(
293
+ filepath=filepath,
294
+ datapath=datapath,
295
+ start=start,
296
+ stop=stop,
297
+ step=step,
298
+ )
299
+ else:
300
+ # Try online, in case backends still have the frames we want
301
+ _logger.warning(
302
+ f"Requesting frames {start}:{stop}:{step} after stream is sealed, "
303
+ f"but this frame source ({self._source}) isn't persistent."
304
+ )
305
+ return self._fetch_online(start=start, stop=stop, step=step)
306
+
307
+ else:
308
+ return self._fetch_online(start=start, stop=stop, step=step)
309
+
310
+ def _fetch_online(self, start: int, stop: int, step: int) -> np.ndarray:
311
+ """Get frame data directly from the Lima2 devices."""
312
+ _logger.info(
313
+ f"Fetching {self._source} {start}:{stop}:{step} from lima2 backend"
314
+ )
315
+
316
+ frames = []
317
+ for idx in range(start, stop, step):
318
+ try:
319
+ frames.append(
320
+ get_frame(
321
+ services=self._services,
322
+ acq_uuid=self._acq_uuid,
323
+ source=self._source,
324
+ frame_idx=idx,
325
+ ).array
326
+ )
327
+ except RuntimeError as e:
328
+ # Raise if any frame can't be accessed
329
+ raise IndexNoMoreThereError(
330
+ f"Can't fetch {self._source} {range(start, stop, step)}: {e}"
331
+ ) from e
332
+ except ValueError as e:
333
+ # Frame source has no associated memory buffer (raw_frame)
334
+ raise IndexNoMoreThereError(
335
+ f"Can't fetch {self._source}: no associated live data"
336
+ ) from e
337
+
338
+ return np.asarray(frames)
339
+
340
+ def _fetch_from_disk(
341
+ self, filepath: str, datapath: str, start: int, stop: int, step: int
342
+ ) -> np.ndarray:
343
+ """Get frame data from a file (i.e. "offline")."""
344
+ _logger.info(
345
+ f"Fetching {self._source} {start}:{stop}:{step} from disk via {filepath}"
346
+ )
347
+
348
+ with dynamic_hdf5.File(filepath, retry_timeout=10, retry_period=1) as file:
349
+ return file[datapath][start:stop:step]
@@ -0,0 +1 @@
1
+ from blissdata.tests.conftest import redis_db, redis_url, data_store # noqa F401
@@ -0,0 +1,448 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of the bliss project
4
+ #
5
+ # Copyright (c) Beamline Control Unit, ESRF
6
+ # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
+
8
+ """Unit test suite for Lima2 stream and view (streams/lima2.py)."""
9
+
10
+ from types import SimpleNamespace
11
+ from unittest.mock import MagicMock, Mock, call
12
+ from uuid import uuid1
13
+
14
+ import numpy as np
15
+ import pytest
16
+ from blissdata.exceptions import (
17
+ EmptyViewException,
18
+ IndexNoMoreThereError,
19
+ IndexNotYetThereError,
20
+ IndexWontBeThereError,
21
+ )
22
+ from blissdata.lima.image_utils import ImageData
23
+ from blissdata.streams import EventRange, EventStream
24
+ from lima2.common.devencoded.sparse_frame import SparseFrame
25
+
26
+ from blissdata_lima2 import Lima2Stream, Lima2View
27
+ from blissdata_lima2.stream import get_frame
28
+
29
+
30
+ def test_lima2_get_frame():
31
+ services = Mock()
32
+ uuid = str(uuid1())
33
+ frm = get_frame(services=services, acq_uuid=uuid, source="cafe", frame_idx=123)
34
+ services.pipeline.get_frame.assert_called_with(
35
+ frame_idx=123, source="cafe", uuid=uuid
36
+ )
37
+ assert type(frm) is ImageData
38
+
39
+
40
+ def test_lima2_get_sparse_frame():
41
+ mock_frame = Mock(spec=SparseFrame)
42
+
43
+ def mock_pipeline_get_frame(frame_idx, source, uuid):
44
+ return mock_frame
45
+
46
+ mock_services = SimpleNamespace(
47
+ pipeline=SimpleNamespace(get_frame=mock_pipeline_get_frame)
48
+ )
49
+
50
+ uuid = str(uuid1())
51
+ img_data = get_frame(
52
+ services=mock_services, acq_uuid=uuid, source="cafe", frame_idx=123
53
+ )
54
+ mock_frame.densify.assert_called_once()
55
+ assert type(img_data) is ImageData
56
+
57
+
58
+ def test_lima2_view(monkeypatch):
59
+ services = Mock()
60
+ uuid = str(uuid1())
61
+ view = Lima2View(
62
+ services=services,
63
+ acq_uuid=uuid,
64
+ source="cafe",
65
+ start=0,
66
+ stop=42,
67
+ )
68
+ assert len(view) == 42
69
+ assert view.index == 0
70
+
71
+ frames = view.get_data()
72
+ assert len(frames) == len(view)
73
+
74
+ services.pipeline.get_frame.assert_has_calls(
75
+ [call(uuid=uuid, source="cafe", frame_idx=i) for i in range(42)]
76
+ )
77
+
78
+ # RuntimeError
79
+ def mock_get_frame_runtime_error(services, acq_uuid, source, frame_idx):
80
+ raise RuntimeError("frame has left the buffer")
81
+
82
+ monkeypatch.setattr(
83
+ "blissdata_lima2.stream.get_frame", mock_get_frame_runtime_error
84
+ )
85
+
86
+ with pytest.raises(IndexNoMoreThereError):
87
+ frames = view.get_data()
88
+
89
+ # ValueError
90
+ def mock_get_frame_value_error(services, acq_uuid, source, frame_idx):
91
+ raise ValueError("no such frame buffer")
92
+
93
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame_value_error)
94
+
95
+ with pytest.raises(IndexNoMoreThereError):
96
+ frames = view.get_data()
97
+
98
+
99
+ def test_lima2_protocol(data_store):
100
+ uuid = str(uuid1())
101
+
102
+ stream_def = Lima2Stream.make_definition(
103
+ name="device:cafe",
104
+ source_name="cafe",
105
+ conductor_hostname="www.lima2.org",
106
+ conductor_port=12345,
107
+ acq_uuid=uuid,
108
+ master_file=None,
109
+ dtype=np.float128, # fat pixels >:)
110
+ shape=(4, 1024, 512),
111
+ )
112
+ model = data_store._stream_model(
113
+ encoding=stream_def.encoder.info(), info=stream_def.info
114
+ )
115
+ model.info["protocol_version"] = 1 # hack the protocol number
116
+ event_stream = EventStream.create(data_store, stream_def.name, model)
117
+
118
+ with pytest.raises(RuntimeError):
119
+ _ = Lima2Stream(event_stream=event_stream)
120
+
121
+
122
+ @pytest.fixture
123
+ def lima2_stream(data_store):
124
+ uuid = str(uuid1())
125
+
126
+ stream_def = Lima2Stream.make_definition(
127
+ name="device:cafe",
128
+ source_name="cafe",
129
+ conductor_hostname="www.lima2.org",
130
+ conductor_port=12345,
131
+ acq_uuid=uuid,
132
+ master_file=None,
133
+ dtype=np.float128, # fat pixels >:)
134
+ shape=(4, 1024, 512),
135
+ )
136
+ model = data_store._stream_model(
137
+ encoding=stream_def.encoder.info(), info=stream_def.info
138
+ )
139
+
140
+ event_stream = EventStream.create(data_store, stream_def.name, model)
141
+ stream = Lima2Stream(event_stream=event_stream)
142
+
143
+ return (stream_def, event_stream, stream)
144
+
145
+
146
+ def test_lima2_stream_attributes(lima2_stream):
147
+ stream_def, event_stream, stream = lima2_stream
148
+
149
+ assert stream.plugin == "lima2"
150
+ assert stream.shape == stream_def.info["shape"]
151
+ assert stream.kind == "array"
152
+ assert stream.dtype == stream._dtype
153
+ assert stream._need_last_only(last_only=True)
154
+ assert stream._need_last_only(last_only=False)
155
+
156
+
157
+ def test_lima2_stream_indexing(lima2_stream, monkeypatch):
158
+ stream_def, event_stream, stream = lima2_stream
159
+
160
+ # Feed the event stream
161
+ event_stream.send(np.uint32(42))
162
+ event_stream.join()
163
+ assert len(stream) == 42
164
+
165
+ event_stream.send(np.uint32(123))
166
+ event_stream.join()
167
+ assert len(stream) == 123
168
+
169
+ mock_get_frame = Mock()
170
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
171
+
172
+ # Indexing
173
+ _ = stream[0]
174
+ mock_get_frame.assert_called_with(
175
+ services=stream._services,
176
+ acq_uuid=stream._acq_uuid,
177
+ source="cafe",
178
+ frame_idx=0,
179
+ )
180
+
181
+ with pytest.raises(IndexNotYetThereError):
182
+ _ = stream[-1]
183
+
184
+ with pytest.raises(IndexNotYetThereError):
185
+ _ = stream[123123] # wayyy out of bounds, but we're not sealed yet
186
+
187
+ with monkeypatch.context() as mpc:
188
+ mpc.setattr(stream, "is_sealed", lambda: True)
189
+
190
+ with pytest.raises(IndexWontBeThereError):
191
+ _ = stream[123123] # wayyy out of bounds and we're sealed
192
+
193
+ event_stream.seal()
194
+ event_stream.join()
195
+
196
+ _ = stream[-3]
197
+ assert mock_get_frame.mock_calls[-1] == call(
198
+ services=stream._services,
199
+ acq_uuid=stream._acq_uuid,
200
+ source="cafe",
201
+ frame_idx=123 - 3,
202
+ )
203
+
204
+
205
+ def test_lima2_stream_slicing(lima2_stream, monkeypatch):
206
+ stream_def, event_stream, stream = lima2_stream
207
+
208
+ # Feed the event stream
209
+ event_stream.send(np.uint32(42))
210
+ event_stream.join()
211
+ assert len(stream) == 42
212
+
213
+ event_stream.send(np.uint32(123))
214
+ event_stream.join()
215
+ assert len(stream) == 123
216
+
217
+ mock_get_frame = Mock()
218
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
219
+
220
+ # Slicing
221
+ _ = stream[:3]
222
+ assert mock_get_frame.mock_calls[-3:] == [
223
+ call(
224
+ services=stream._services,
225
+ acq_uuid=stream._acq_uuid,
226
+ source="cafe",
227
+ frame_idx=0,
228
+ ),
229
+ call(
230
+ services=stream._services,
231
+ acq_uuid=stream._acq_uuid,
232
+ source="cafe",
233
+ frame_idx=1,
234
+ ),
235
+ call(
236
+ services=stream._services,
237
+ acq_uuid=stream._acq_uuid,
238
+ source="cafe",
239
+ frame_idx=2,
240
+ ),
241
+ ]
242
+
243
+ with pytest.raises(IndexNotYetThereError):
244
+ _ = stream[-2:]
245
+
246
+ with pytest.raises(IndexNotYetThereError):
247
+ _ = stream[:-2]
248
+
249
+ with pytest.raises(TypeError):
250
+ _ = stream["hi :)"]
251
+
252
+ event_stream.seal()
253
+ event_stream.join()
254
+
255
+ # Now slicing/indexing from end is ok
256
+ _ = stream[-2:]
257
+ assert mock_get_frame.mock_calls[-2:] == [
258
+ call(
259
+ services=stream._services,
260
+ acq_uuid=stream._acq_uuid,
261
+ source="cafe",
262
+ frame_idx=123 - 2,
263
+ ),
264
+ call(
265
+ services=stream._services,
266
+ acq_uuid=stream._acq_uuid,
267
+ source="cafe",
268
+ frame_idx=123 - 1,
269
+ ),
270
+ ]
271
+
272
+ _ = stream[:]
273
+ assert mock_get_frame.mock_calls[-123:] == [
274
+ call(
275
+ services=stream._services,
276
+ acq_uuid=stream._acq_uuid,
277
+ source="cafe",
278
+ frame_idx=i,
279
+ )
280
+ for i in range(123)
281
+ ]
282
+
283
+ _ = stream.get_last_live_image()
284
+ mock_get_frame.assert_called_with(
285
+ services=stream._services,
286
+ acq_uuid=stream._acq_uuid,
287
+ source="cafe",
288
+ frame_idx=-1,
289
+ )
290
+
291
+
292
+ def test_lima2_stream_build_view(lima2_stream, monkeypatch):
293
+ stream_def, event_stream, stream = lima2_stream
294
+
295
+ mock_get_frame = Mock()
296
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
297
+
298
+ view = stream._build_view_from_events(
299
+ index=0,
300
+ events=EventRange(
301
+ index=0,
302
+ nb_expired=0,
303
+ data=[np.uint32(5), np.uint32(24), np.uint32(42)],
304
+ end_of_stream=False,
305
+ ),
306
+ last_only=False,
307
+ )
308
+ assert view._idx_range == range(0, 42)
309
+
310
+ view = stream._build_view_from_events(
311
+ index=0,
312
+ events=EventRange(
313
+ index=0,
314
+ nb_expired=0,
315
+ data=[np.uint32(5), np.uint32(24), np.uint32(42)],
316
+ end_of_stream=False,
317
+ ),
318
+ last_only=True,
319
+ )
320
+ assert view._idx_range == range(41, 42)
321
+
322
+ with pytest.raises(EmptyViewException):
323
+ # index >= data[-1]
324
+ view = stream._build_view_from_events(
325
+ index=42,
326
+ events=EventRange(
327
+ index=0,
328
+ nb_expired=0,
329
+ data=[np.uint32(5), np.uint32(24), np.uint32(42)],
330
+ end_of_stream=False,
331
+ ),
332
+ last_only=False,
333
+ )
334
+
335
+
336
+ def test_lima2_stream_fetch_frames(lima2_stream, monkeypatch):
337
+ stream_def, event_stream, stream = lima2_stream
338
+
339
+ mock_get_frame = Mock()
340
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
341
+
342
+ with monkeypatch.context() as mpc:
343
+ # Stream sealed, has master file
344
+ mock_fetch = Mock()
345
+ mpc.setattr(stream, "_fetch_from_disk", mock_fetch)
346
+ mpc.setattr(stream, "is_sealed", lambda: True)
347
+ mpc.setattr(
348
+ stream,
349
+ "_master_file",
350
+ ("/path/to/master.h5", "entry/instrument/detector/dataset"),
351
+ )
352
+
353
+ stream._fetch_frames(start=0, stop=42, step=3)
354
+
355
+ mock_fetch.assert_called_once_with(
356
+ filepath="/path/to/master.h5",
357
+ datapath="entry/instrument/detector/dataset",
358
+ start=0,
359
+ stop=42,
360
+ step=3,
361
+ )
362
+
363
+ with monkeypatch.context() as mpc:
364
+ # Stream sealed, no master file
365
+ mock_fetch = Mock()
366
+ mpc.setattr(stream, "_fetch_online", mock_fetch)
367
+ mpc.setattr(stream, "is_sealed", lambda: True)
368
+ mpc.setattr(stream, "_master_file", None)
369
+
370
+ stream._fetch_frames(start=0, stop=42, step=3)
371
+
372
+ mock_fetch.assert_called_once_with(
373
+ start=0,
374
+ stop=42,
375
+ step=3,
376
+ )
377
+
378
+ with monkeypatch.context() as mpc:
379
+ # Stream not sealed
380
+ mock_fetch = Mock()
381
+ mpc.setattr(stream, "_fetch_online", mock_fetch)
382
+ mpc.setattr(stream, "is_sealed", lambda: False)
383
+
384
+ stream._fetch_frames(start=0, stop=42, step=3)
385
+
386
+ mock_fetch.assert_called_once_with(
387
+ start=0,
388
+ stop=42,
389
+ step=3,
390
+ )
391
+
392
+
393
+ def test_lima2_stream_fetch_online(lima2_stream, monkeypatch):
394
+ stream_def, event_stream, stream = lima2_stream
395
+
396
+ mock_get_frame = Mock()
397
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
398
+
399
+ stream._fetch_online(start=0, stop=42, step=3)
400
+
401
+ mock_get_frame.assert_has_calls(
402
+ [
403
+ call(
404
+ services=stream._services,
405
+ acq_uuid=stream._acq_uuid,
406
+ source=stream._source,
407
+ frame_idx=idx,
408
+ )
409
+ for idx in range(0, 42, 3)
410
+ ]
411
+ )
412
+
413
+ # RuntimeError
414
+ def mock_get_frame_runtime_error(services, acq_uuid, source, frame_idx):
415
+ raise RuntimeError("oh no :(")
416
+
417
+ monkeypatch.setattr(
418
+ "blissdata_lima2.stream.get_frame", mock_get_frame_runtime_error
419
+ )
420
+
421
+ with pytest.raises(IndexNoMoreThereError):
422
+ _ = stream._fetch_online(start=0, stop=1, step=1)
423
+
424
+ # ValueError
425
+ def mock_get_frame_value_error(services, acq_uuid, source, frame_idx):
426
+ raise ValueError("oh no :(")
427
+
428
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame_value_error)
429
+
430
+ with pytest.raises(IndexNoMoreThereError):
431
+ _ = stream._fetch_online(start=0, stop=1, step=1)
432
+
433
+
434
+ def test_lima2_stream_fetch_offline(lima2_stream, monkeypatch):
435
+ stream_def, event_stream, stream = lima2_stream
436
+
437
+ mock_dynamic_hdf5 = MagicMock()
438
+ monkeypatch.setattr("blissdata_lima2.stream.dynamic_hdf5", mock_dynamic_hdf5)
439
+
440
+ stream._fetch_from_disk(
441
+ filepath="/path/to/master.h5",
442
+ datapath="entry/instrument/detector/dataset",
443
+ start=0,
444
+ stop=42,
445
+ step=3,
446
+ )
447
+
448
+ mock_dynamic_hdf5.File.assert_called_once()
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: blissdata-lima2
3
+ Version: 2.0.1
4
+ Summary: Lima2 plugin for blissdata
5
+ Maintainer: BCU (ESRF)
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://gitlab.esrf.fr/bliss/blissdata-lima2
8
+ Keywords: blissdata,lima2
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE.md
14
+ Requires-Dist: blissdata~=2.3
15
+ Requires-Dist: lima2-client~=4.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: black; extra == "dev"
18
+ Requires-Dist: flake8; extra == "dev"
19
+ Requires-Dist: mypy; extra == "dev"
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Requires-Dist: pytest-cov; extra == "dev"
22
+ Requires-Dist: pytest-redis; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ ## blissdata-lima2
26
+
27
+ Lima2 plugin for blissdata.
@@ -0,0 +1,14 @@
1
+ LICENSE.md
2
+ README.md
3
+ pyproject.toml
4
+ src/blissdata_lima2/__init__.py
5
+ src/blissdata_lima2/stream.py
6
+ src/blissdata_lima2.egg-info/PKG-INFO
7
+ src/blissdata_lima2.egg-info/SOURCES.txt
8
+ src/blissdata_lima2.egg-info/dependency_links.txt
9
+ src/blissdata_lima2.egg-info/entry_points.txt
10
+ src/blissdata_lima2.egg-info/requires.txt
11
+ src/blissdata_lima2.egg-info/top_level.txt
12
+ src/blissdata_lima2/tests/__init__.py
13
+ src/blissdata_lima2/tests/conftest.py
14
+ src/blissdata_lima2/tests/test_lima2_stream.py
@@ -0,0 +1,2 @@
1
+ [blissdata]
2
+ lima2 = blissdata_lima2
@@ -0,0 +1,10 @@
1
+ blissdata~=2.3
2
+ lima2-client~=4.0
3
+
4
+ [dev]
5
+ black
6
+ flake8
7
+ mypy
8
+ pytest
9
+ pytest-cov
10
+ pytest-redis
@@ -0,0 +1 @@
1
+ blissdata_lima2