blissdata-lima2 2.0__tar.gz → 2.0.2__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.
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blissdata-lima2
3
- Version: 2.0
3
+ Version: 2.0.2
4
4
  Summary: Lima2 plugin for blissdata
5
5
  Maintainer: BCU (ESRF)
6
6
  License-Expression: MIT
7
+ Project-URL: Repository, https://gitlab.esrf.fr/bliss/blissdata-lima2
7
8
  Keywords: blissdata,lima2
8
9
  Classifier: Intended Audience :: Science/Research
9
10
  Classifier: Programming Language :: Python :: 3
@@ -11,11 +12,13 @@ Requires-Python: >=3.10
11
12
  Description-Content-Type: text/markdown
12
13
  License-File: LICENSE.md
13
14
  Requires-Dist: blissdata~=2.3
14
- Requires-Dist: lima2-client~=4.0
15
+ Requires-Dist: lima2-client~=4.1
15
16
  Provides-Extra: dev
16
17
  Requires-Dist: black; extra == "dev"
17
18
  Requires-Dist: flake8; extra == "dev"
19
+ Requires-Dist: mypy; extra == "dev"
18
20
  Requires-Dist: pytest; extra == "dev"
21
+ Requires-Dist: pytest-cov; extra == "dev"
19
22
  Requires-Dist: pytest-redis; extra == "dev"
20
23
  Dynamic: license-file
21
24
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "blissdata-lima2"
7
- version = "2.0"
7
+ version = "2.0.2"
8
8
  description = "Lima2 plugin for blissdata"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -16,10 +16,13 @@ classifiers = [
16
16
  ]
17
17
  keywords = ["blissdata", "lima2"]
18
18
  requires-python = ">=3.10"
19
- dependencies = ["blissdata~=2.3", "lima2-client~=4.0"]
19
+ dependencies = ["blissdata~=2.3", "lima2-client~=4.1"]
20
+
21
+ [project.urls]
22
+ Repository = "https://gitlab.esrf.fr/bliss/blissdata-lima2"
20
23
 
21
24
  [project.optional-dependencies]
22
- dev = ["black", "flake8", "pytest", "pytest-redis"]
25
+ dev = ["black", "flake8", "mypy", "pytest", "pytest-cov", "pytest-redis"]
23
26
 
24
27
  [project.entry-points.blissdata]
25
28
  lima2 = "blissdata_lima2"
@@ -15,17 +15,19 @@ from blissdata.exceptions import (
15
15
  EndOfStream,
16
16
  IndexNoMoreThereError,
17
17
  IndexNotYetThereError,
18
+ IndexWontBeThereError,
18
19
  )
20
+ from blissdata.h5api import dynamic_hdf5
19
21
  from blissdata.lima.image_utils import ImageData
20
22
  from blissdata.streams import (
21
23
  BaseStream,
22
24
  BaseView,
23
25
  EventRange,
24
- EventStream,
25
26
  StreamDefinition,
26
27
  )
27
28
  from blissdata.streams.default import Stream
28
29
  from blissdata.streams.encoding.numeric import NumericStreamEncoder
30
+ from blissdata.streams.event_stream import EventStream
29
31
  from blissdata.streams.lima.stream import LimaDirectAccess
30
32
  from lima2.common.devencoded.sparse_frame import SparseFrame
31
33
  from numpy.typing import DTypeLike
@@ -72,24 +74,30 @@ class Lima2View(BaseView):
72
74
  def __len__(self) -> int:
73
75
  return len(self._idx_range)
74
76
 
75
- def get_data(
76
- self, start: int | None = None, stop: int | None = None
77
- ) -> list[ImageData]:
78
- try:
79
- return [
80
- get_frame(
81
- services=self._services,
82
- acq_uuid=self._acq_uuid,
83
- source=self._source,
84
- frame_idx=idx,
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
85
88
  )
86
- for idx in self._idx_range[start:stop]
87
- ]
88
- except RuntimeError as e:
89
- raise IndexNoMoreThereError(
90
- f"Can't fetch {self._source} {self._idx_range[start:stop].start} "
91
- f"to {self._idx_range[start:stop].stop}: {e}"
92
- ) from e
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)
93
101
 
94
102
 
95
103
  class Lima2Stream(BaseStream, LimaDirectAccess):
@@ -120,6 +128,7 @@ class Lima2Stream(BaseStream, LimaDirectAccess):
120
128
  self._shape = tuple(event_stream.info["shape"])
121
129
  self._acq_uuid = str(event_stream.info["acq_uuid"])
122
130
  self._source = str(event_stream.info["source_name"])
131
+ self._master_file: tuple[str, str] | None = event_stream.info["master_file"]
123
132
 
124
133
  self._services = l2s.init(
125
134
  hostname=str(event_stream.info["conductor_hostname"]),
@@ -132,8 +141,14 @@ class Lima2Stream(BaseStream, LimaDirectAccess):
132
141
 
133
142
  self._cursor = Stream(event_stream).cursor()
134
143
 
144
+ self._services.handshake()
145
+
146
+ ####################################################################################
147
+ # BaseStream
148
+ ####################################################################################
149
+
135
150
  @property
136
- def kind(self):
151
+ def kind(self) -> str:
137
152
  return "array"
138
153
 
139
154
  @staticmethod
@@ -145,7 +160,7 @@ class Lima2Stream(BaseStream, LimaDirectAccess):
145
160
  acq_uuid: str,
146
161
  master_file: tuple[str, str] | None,
147
162
  dtype: DTypeLike,
148
- shape: Sequence,
163
+ shape: Sequence[int],
149
164
  ) -> StreamDefinition:
150
165
  info = {
151
166
  "plugin": "lima2",
@@ -184,34 +199,43 @@ class Lima2Stream(BaseStream, LimaDirectAccess):
184
199
  self._length = int(last_status)
185
200
  return self._length
186
201
 
187
- def __getitem__(self, key: int | slice) -> ImageData | list[ImageData]:
188
- idx_range = range(len(self))
189
- if type(key) is int:
190
- if key < 0 and not self.is_sealed():
191
- raise IndexNotYetThereError(
192
- "Can't index from end of stream until it is sealed"
193
- )
194
- return get_frame(
195
- services=self._services,
196
- acq_uuid=self._acq_uuid,
197
- source=self._source,
198
- frame_idx=idx_range[key],
199
- )
200
- elif type(key) is slice:
201
- if not self.is_sealed() and ((key.start or 0) < 0 or (key.stop or 0) < 0):
202
- raise IndexNotYetThereError(
203
- "Can't slice from end of stream until it is sealed"
204
- )
202
+ def __getitem__(self, key: int | slice) -> np.ndarray:
203
+ sealed = self.is_sealed()
204
+ if isinstance(key, int):
205
+ size = len(self)
206
+ if key < 0:
207
+ if not sealed:
208
+ raise IndexNotYetThereError(
209
+ "Can't index from end of stream until it is sealed"
210
+ )
211
+ else:
212
+ key += size
213
+
214
+ if not 0 <= key < size:
215
+ # Fail early if we're out of bounds
216
+ if sealed:
217
+ raise IndexWontBeThereError(
218
+ f"Frame {key} is out of range (0..{size-1})"
219
+ )
220
+ else:
221
+ raise IndexNotYetThereError(
222
+ f"Frame {key} is out of range (0..{size-1})"
223
+ )
224
+
225
+ return self._fetch_frames(start=key, stop=key + 1, step=1)[0]
226
+
227
+ elif isinstance(key, slice):
228
+ if (key.start or 0) < 0 or (key.stop or 0) < 0:
229
+ if not sealed:
230
+ raise IndexNotYetThereError(
231
+ "Can't slice from end of stream until it is sealed"
232
+ )
233
+
234
+ # Resolve the slice in the standard way
235
+ start, stop, step = key.indices(len(self))
236
+
237
+ return self._fetch_frames(start=start, stop=stop, step=step)
205
238
 
206
- return [
207
- get_frame(
208
- services=self._services,
209
- acq_uuid=self._acq_uuid,
210
- source=self._source,
211
- frame_idx=idx,
212
- )
213
- for idx in idx_range[key]
214
- ]
215
239
  else:
216
240
  raise TypeError(f"{type(key)}")
217
241
 
@@ -245,6 +269,10 @@ class Lima2Stream(BaseStream, LimaDirectAccess):
245
269
  stop=stop_idx,
246
270
  )
247
271
 
272
+ ####################################################################################
273
+ # LimaDirectAccess
274
+ ####################################################################################
275
+
248
276
  def get_last_live_image(self) -> ImageData:
249
277
  return get_frame(
250
278
  services=self._services,
@@ -252,3 +280,72 @@ class Lima2Stream(BaseStream, LimaDirectAccess):
252
280
  source=self._source,
253
281
  frame_idx=-1,
254
282
  )
283
+
284
+ ####################################################################################
285
+ # Private API
286
+ ####################################################################################
287
+
288
+ def _fetch_frames(self, start: int, stop: int, step: int) -> np.ndarray:
289
+ """Get frame data online or offline, depending on the current state."""
290
+ if self.is_sealed():
291
+ if self._master_file is not None:
292
+ # Default to an offline lookup
293
+ filepath, datapath = self._master_file
294
+ return self._fetch_from_disk(
295
+ filepath=filepath,
296
+ datapath=datapath,
297
+ start=start,
298
+ stop=stop,
299
+ step=step,
300
+ )
301
+ else:
302
+ # Try online, in case backends still have the frames we want
303
+ _logger.warning(
304
+ f"Requesting frames {start}:{stop}:{step} after stream is sealed, "
305
+ f"but this frame source ({self._source}) isn't persistent."
306
+ )
307
+ return self._fetch_online(start=start, stop=stop, step=step)
308
+
309
+ else:
310
+ return self._fetch_online(start=start, stop=stop, step=step)
311
+
312
+ def _fetch_online(self, start: int, stop: int, step: int) -> np.ndarray:
313
+ """Get frame data directly from the Lima2 devices."""
314
+ _logger.info(
315
+ f"Fetching {self._source} {start}:{stop}:{step} from lima2 backend"
316
+ )
317
+
318
+ frames = []
319
+ for idx in range(start, stop, step):
320
+ try:
321
+ frames.append(
322
+ get_frame(
323
+ services=self._services,
324
+ acq_uuid=self._acq_uuid,
325
+ source=self._source,
326
+ frame_idx=idx,
327
+ ).array
328
+ )
329
+ except RuntimeError as e:
330
+ # Raise if any frame can't be accessed
331
+ raise IndexNoMoreThereError(
332
+ f"Can't fetch {self._source} {range(start, stop, step)}: {e}"
333
+ ) from e
334
+ except ValueError as e:
335
+ # Frame source has no associated memory buffer (raw_frame)
336
+ raise IndexNoMoreThereError(
337
+ f"Can't fetch {self._source}: no associated live data"
338
+ ) from e
339
+
340
+ return np.asarray(frames)
341
+
342
+ def _fetch_from_disk(
343
+ self, filepath: str, datapath: str, start: int, stop: int, step: int
344
+ ) -> np.ndarray:
345
+ """Get frame data from a file (i.e. "offline")."""
346
+ _logger.info(
347
+ f"Fetching {self._source} {start}:{stop}:{step} from disk via {filepath}"
348
+ )
349
+
350
+ with dynamic_hdf5.File(filepath, retry_timeout=10, retry_period=1) as file:
351
+ return file[datapath][start:stop:step]
@@ -0,0 +1,458 @@
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
+ @pytest.fixture
31
+ def mock_handshake(monkeypatch):
32
+ def mock_handshake(self):
33
+ pass
34
+
35
+ monkeypatch.setattr(
36
+ "lima2.client.services.ConductorServices.handshake", mock_handshake
37
+ )
38
+
39
+
40
+ def test_lima2_get_frame():
41
+ services = Mock()
42
+ uuid = str(uuid1())
43
+ frm = get_frame(services=services, acq_uuid=uuid, source="cafe", frame_idx=123)
44
+ services.pipeline.get_frame.assert_called_with(
45
+ frame_idx=123, source="cafe", uuid=uuid
46
+ )
47
+ assert type(frm) is ImageData
48
+
49
+
50
+ def test_lima2_get_sparse_frame():
51
+ mock_frame = Mock(spec=SparseFrame)
52
+
53
+ def mock_pipeline_get_frame(frame_idx, source, uuid):
54
+ return mock_frame
55
+
56
+ mock_services = SimpleNamespace(
57
+ pipeline=SimpleNamespace(get_frame=mock_pipeline_get_frame)
58
+ )
59
+
60
+ uuid = str(uuid1())
61
+ img_data = get_frame(
62
+ services=mock_services, acq_uuid=uuid, source="cafe", frame_idx=123
63
+ )
64
+ mock_frame.densify.assert_called_once()
65
+ assert type(img_data) is ImageData
66
+
67
+
68
+ def test_lima2_view(monkeypatch):
69
+ services = Mock()
70
+ uuid = str(uuid1())
71
+ view = Lima2View(
72
+ services=services,
73
+ acq_uuid=uuid,
74
+ source="cafe",
75
+ start=0,
76
+ stop=42,
77
+ )
78
+ assert len(view) == 42
79
+ assert view.index == 0
80
+
81
+ frames = view.get_data()
82
+ assert len(frames) == len(view)
83
+
84
+ services.pipeline.get_frame.assert_has_calls(
85
+ [call(uuid=uuid, source="cafe", frame_idx=i) for i in range(42)]
86
+ )
87
+
88
+ # RuntimeError
89
+ def mock_get_frame_runtime_error(services, acq_uuid, source, frame_idx):
90
+ raise RuntimeError("frame has left the buffer")
91
+
92
+ monkeypatch.setattr(
93
+ "blissdata_lima2.stream.get_frame", mock_get_frame_runtime_error
94
+ )
95
+
96
+ with pytest.raises(IndexNoMoreThereError):
97
+ frames = view.get_data()
98
+
99
+ # ValueError
100
+ def mock_get_frame_value_error(services, acq_uuid, source, frame_idx):
101
+ raise ValueError("no such frame buffer")
102
+
103
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame_value_error)
104
+
105
+ with pytest.raises(IndexNoMoreThereError):
106
+ frames = view.get_data()
107
+
108
+
109
+ def test_lima2_protocol(data_store):
110
+ uuid = str(uuid1())
111
+
112
+ stream_def = Lima2Stream.make_definition(
113
+ name="device:cafe",
114
+ source_name="cafe",
115
+ conductor_hostname="www.lima2.org",
116
+ conductor_port=12345,
117
+ acq_uuid=uuid,
118
+ master_file=None,
119
+ dtype=np.float128, # fat pixels >:)
120
+ shape=(4, 1024, 512),
121
+ )
122
+ model = data_store._stream_model(
123
+ encoding=stream_def.encoder.info(), info=stream_def.info
124
+ )
125
+ model.info["protocol_version"] = 1 # hack the protocol number
126
+ event_stream = EventStream.create(data_store, stream_def.name, model)
127
+
128
+ with pytest.raises(RuntimeError):
129
+ _ = Lima2Stream(event_stream=event_stream)
130
+
131
+
132
+ @pytest.fixture
133
+ def lima2_stream(data_store, mock_handshake):
134
+ uuid = str(uuid1())
135
+
136
+ stream_def = Lima2Stream.make_definition(
137
+ name="device:cafe",
138
+ source_name="cafe",
139
+ conductor_hostname="www.lima2.org",
140
+ conductor_port=12345,
141
+ acq_uuid=uuid,
142
+ master_file=None,
143
+ dtype=np.float128, # fat pixels >:)
144
+ shape=(4, 1024, 512),
145
+ )
146
+ model = data_store._stream_model(
147
+ encoding=stream_def.encoder.info(), info=stream_def.info
148
+ )
149
+
150
+ event_stream = EventStream.create(data_store, stream_def.name, model)
151
+ stream = Lima2Stream(event_stream=event_stream)
152
+
153
+ return (stream_def, event_stream, stream)
154
+
155
+
156
+ def test_lima2_stream_attributes(lima2_stream):
157
+ stream_def, event_stream, stream = lima2_stream
158
+
159
+ assert stream.plugin == "lima2"
160
+ assert stream.shape == stream_def.info["shape"]
161
+ assert stream.kind == "array"
162
+ assert stream.dtype == stream._dtype
163
+ assert stream._need_last_only(last_only=True)
164
+ assert stream._need_last_only(last_only=False)
165
+
166
+
167
+ def test_lima2_stream_indexing(lima2_stream, monkeypatch):
168
+ stream_def, event_stream, stream = lima2_stream
169
+
170
+ # Feed the event stream
171
+ event_stream.send(np.uint32(42))
172
+ event_stream.join()
173
+ assert len(stream) == 42
174
+
175
+ event_stream.send(np.uint32(123))
176
+ event_stream.join()
177
+ assert len(stream) == 123
178
+
179
+ mock_get_frame = Mock()
180
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
181
+
182
+ # Indexing
183
+ _ = stream[0]
184
+ mock_get_frame.assert_called_with(
185
+ services=stream._services,
186
+ acq_uuid=stream._acq_uuid,
187
+ source="cafe",
188
+ frame_idx=0,
189
+ )
190
+
191
+ with pytest.raises(IndexNotYetThereError):
192
+ _ = stream[-1]
193
+
194
+ with pytest.raises(IndexNotYetThereError):
195
+ _ = stream[123123] # wayyy out of bounds, but we're not sealed yet
196
+
197
+ with monkeypatch.context() as mpc:
198
+ mpc.setattr(stream, "is_sealed", lambda: True)
199
+
200
+ with pytest.raises(IndexWontBeThereError):
201
+ _ = stream[123123] # wayyy out of bounds and we're sealed
202
+
203
+ event_stream.seal()
204
+ event_stream.join()
205
+
206
+ _ = stream[-3]
207
+ assert mock_get_frame.mock_calls[-1] == call(
208
+ services=stream._services,
209
+ acq_uuid=stream._acq_uuid,
210
+ source="cafe",
211
+ frame_idx=123 - 3,
212
+ )
213
+
214
+
215
+ def test_lima2_stream_slicing(lima2_stream, monkeypatch):
216
+ stream_def, event_stream, stream = lima2_stream
217
+
218
+ # Feed the event stream
219
+ event_stream.send(np.uint32(42))
220
+ event_stream.join()
221
+ assert len(stream) == 42
222
+
223
+ event_stream.send(np.uint32(123))
224
+ event_stream.join()
225
+ assert len(stream) == 123
226
+
227
+ mock_get_frame = Mock()
228
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
229
+
230
+ # Slicing
231
+ _ = stream[:3]
232
+ assert mock_get_frame.mock_calls[-3:] == [
233
+ call(
234
+ services=stream._services,
235
+ acq_uuid=stream._acq_uuid,
236
+ source="cafe",
237
+ frame_idx=0,
238
+ ),
239
+ call(
240
+ services=stream._services,
241
+ acq_uuid=stream._acq_uuid,
242
+ source="cafe",
243
+ frame_idx=1,
244
+ ),
245
+ call(
246
+ services=stream._services,
247
+ acq_uuid=stream._acq_uuid,
248
+ source="cafe",
249
+ frame_idx=2,
250
+ ),
251
+ ]
252
+
253
+ with pytest.raises(IndexNotYetThereError):
254
+ _ = stream[-2:]
255
+
256
+ with pytest.raises(IndexNotYetThereError):
257
+ _ = stream[:-2]
258
+
259
+ with pytest.raises(TypeError):
260
+ _ = stream["hi :)"]
261
+
262
+ event_stream.seal()
263
+ event_stream.join()
264
+
265
+ # Now slicing/indexing from end is ok
266
+ _ = stream[-2:]
267
+ assert mock_get_frame.mock_calls[-2:] == [
268
+ call(
269
+ services=stream._services,
270
+ acq_uuid=stream._acq_uuid,
271
+ source="cafe",
272
+ frame_idx=123 - 2,
273
+ ),
274
+ call(
275
+ services=stream._services,
276
+ acq_uuid=stream._acq_uuid,
277
+ source="cafe",
278
+ frame_idx=123 - 1,
279
+ ),
280
+ ]
281
+
282
+ _ = stream[:]
283
+ assert mock_get_frame.mock_calls[-123:] == [
284
+ call(
285
+ services=stream._services,
286
+ acq_uuid=stream._acq_uuid,
287
+ source="cafe",
288
+ frame_idx=i,
289
+ )
290
+ for i in range(123)
291
+ ]
292
+
293
+ _ = stream.get_last_live_image()
294
+ mock_get_frame.assert_called_with(
295
+ services=stream._services,
296
+ acq_uuid=stream._acq_uuid,
297
+ source="cafe",
298
+ frame_idx=-1,
299
+ )
300
+
301
+
302
+ def test_lima2_stream_build_view(lima2_stream, monkeypatch):
303
+ stream_def, event_stream, stream = lima2_stream
304
+
305
+ mock_get_frame = Mock()
306
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
307
+
308
+ view = stream._build_view_from_events(
309
+ index=0,
310
+ events=EventRange(
311
+ index=0,
312
+ nb_expired=0,
313
+ data=[np.uint32(5), np.uint32(24), np.uint32(42)],
314
+ end_of_stream=False,
315
+ ),
316
+ last_only=False,
317
+ )
318
+ assert view._idx_range == range(0, 42)
319
+
320
+ view = stream._build_view_from_events(
321
+ index=0,
322
+ events=EventRange(
323
+ index=0,
324
+ nb_expired=0,
325
+ data=[np.uint32(5), np.uint32(24), np.uint32(42)],
326
+ end_of_stream=False,
327
+ ),
328
+ last_only=True,
329
+ )
330
+ assert view._idx_range == range(41, 42)
331
+
332
+ with pytest.raises(EmptyViewException):
333
+ # index >= data[-1]
334
+ view = stream._build_view_from_events(
335
+ index=42,
336
+ events=EventRange(
337
+ index=0,
338
+ nb_expired=0,
339
+ data=[np.uint32(5), np.uint32(24), np.uint32(42)],
340
+ end_of_stream=False,
341
+ ),
342
+ last_only=False,
343
+ )
344
+
345
+
346
+ def test_lima2_stream_fetch_frames(lima2_stream, monkeypatch):
347
+ stream_def, event_stream, stream = lima2_stream
348
+
349
+ mock_get_frame = Mock()
350
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
351
+
352
+ with monkeypatch.context() as mpc:
353
+ # Stream sealed, has master file
354
+ mock_fetch = Mock()
355
+ mpc.setattr(stream, "_fetch_from_disk", mock_fetch)
356
+ mpc.setattr(stream, "is_sealed", lambda: True)
357
+ mpc.setattr(
358
+ stream,
359
+ "_master_file",
360
+ ("/path/to/master.h5", "entry/instrument/detector/dataset"),
361
+ )
362
+
363
+ stream._fetch_frames(start=0, stop=42, step=3)
364
+
365
+ mock_fetch.assert_called_once_with(
366
+ filepath="/path/to/master.h5",
367
+ datapath="entry/instrument/detector/dataset",
368
+ start=0,
369
+ stop=42,
370
+ step=3,
371
+ )
372
+
373
+ with monkeypatch.context() as mpc:
374
+ # Stream sealed, no master file
375
+ mock_fetch = Mock()
376
+ mpc.setattr(stream, "_fetch_online", mock_fetch)
377
+ mpc.setattr(stream, "is_sealed", lambda: True)
378
+ mpc.setattr(stream, "_master_file", None)
379
+
380
+ stream._fetch_frames(start=0, stop=42, step=3)
381
+
382
+ mock_fetch.assert_called_once_with(
383
+ start=0,
384
+ stop=42,
385
+ step=3,
386
+ )
387
+
388
+ with monkeypatch.context() as mpc:
389
+ # Stream not sealed
390
+ mock_fetch = Mock()
391
+ mpc.setattr(stream, "_fetch_online", mock_fetch)
392
+ mpc.setattr(stream, "is_sealed", lambda: False)
393
+
394
+ stream._fetch_frames(start=0, stop=42, step=3)
395
+
396
+ mock_fetch.assert_called_once_with(
397
+ start=0,
398
+ stop=42,
399
+ step=3,
400
+ )
401
+
402
+
403
+ def test_lima2_stream_fetch_online(lima2_stream, monkeypatch):
404
+ stream_def, event_stream, stream = lima2_stream
405
+
406
+ mock_get_frame = Mock()
407
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
408
+
409
+ stream._fetch_online(start=0, stop=42, step=3)
410
+
411
+ mock_get_frame.assert_has_calls(
412
+ [
413
+ call(
414
+ services=stream._services,
415
+ acq_uuid=stream._acq_uuid,
416
+ source=stream._source,
417
+ frame_idx=idx,
418
+ )
419
+ for idx in range(0, 42, 3)
420
+ ]
421
+ )
422
+
423
+ # RuntimeError
424
+ def mock_get_frame_runtime_error(services, acq_uuid, source, frame_idx):
425
+ raise RuntimeError("oh no :(")
426
+
427
+ monkeypatch.setattr(
428
+ "blissdata_lima2.stream.get_frame", mock_get_frame_runtime_error
429
+ )
430
+
431
+ with pytest.raises(IndexNoMoreThereError):
432
+ _ = stream._fetch_online(start=0, stop=1, step=1)
433
+
434
+ # ValueError
435
+ def mock_get_frame_value_error(services, acq_uuid, source, frame_idx):
436
+ raise ValueError("oh no :(")
437
+
438
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame_value_error)
439
+
440
+ with pytest.raises(IndexNoMoreThereError):
441
+ _ = stream._fetch_online(start=0, stop=1, step=1)
442
+
443
+
444
+ def test_lima2_stream_fetch_offline(lima2_stream, monkeypatch):
445
+ stream_def, event_stream, stream = lima2_stream
446
+
447
+ mock_dynamic_hdf5 = MagicMock()
448
+ monkeypatch.setattr("blissdata_lima2.stream.dynamic_hdf5", mock_dynamic_hdf5)
449
+
450
+ stream._fetch_from_disk(
451
+ filepath="/path/to/master.h5",
452
+ datapath="entry/instrument/detector/dataset",
453
+ start=0,
454
+ stop=42,
455
+ step=3,
456
+ )
457
+
458
+ mock_dynamic_hdf5.File.assert_called_once()
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blissdata-lima2
3
- Version: 2.0
3
+ Version: 2.0.2
4
4
  Summary: Lima2 plugin for blissdata
5
5
  Maintainer: BCU (ESRF)
6
6
  License-Expression: MIT
7
+ Project-URL: Repository, https://gitlab.esrf.fr/bliss/blissdata-lima2
7
8
  Keywords: blissdata,lima2
8
9
  Classifier: Intended Audience :: Science/Research
9
10
  Classifier: Programming Language :: Python :: 3
@@ -11,11 +12,13 @@ Requires-Python: >=3.10
11
12
  Description-Content-Type: text/markdown
12
13
  License-File: LICENSE.md
13
14
  Requires-Dist: blissdata~=2.3
14
- Requires-Dist: lima2-client~=4.0
15
+ Requires-Dist: lima2-client~=4.1
15
16
  Provides-Extra: dev
16
17
  Requires-Dist: black; extra == "dev"
17
18
  Requires-Dist: flake8; extra == "dev"
19
+ Requires-Dist: mypy; extra == "dev"
18
20
  Requires-Dist: pytest; extra == "dev"
21
+ Requires-Dist: pytest-cov; extra == "dev"
19
22
  Requires-Dist: pytest-redis; extra == "dev"
20
23
  Dynamic: license-file
21
24
 
@@ -1,8 +1,10 @@
1
1
  blissdata~=2.3
2
- lima2-client~=4.0
2
+ lima2-client~=4.1
3
3
 
4
4
  [dev]
5
5
  black
6
6
  flake8
7
+ mypy
7
8
  pytest
9
+ pytest-cov
8
10
  pytest-redis
@@ -1,178 +0,0 @@
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 unittest.mock import Mock, call
11
- from uuid import uuid1
12
-
13
- import numpy as np
14
- import pytest
15
- from blissdata.exceptions import IndexNotYetThereError
16
- from blissdata.lima.image_utils import ImageData
17
- from blissdata.streams import EventStream
18
-
19
- from blissdata_lima2 import Lima2Stream, Lima2View
20
- from blissdata_lima2.stream import get_frame
21
-
22
-
23
- def test_lima2_get_frame():
24
- services = Mock()
25
- uuid = str(uuid1())
26
- frm = get_frame(services=services, acq_uuid=uuid, source="cafe", frame_idx=123)
27
- services.pipeline.get_frame.assert_called_with(
28
- frame_idx=123, source="cafe", uuid=uuid
29
- )
30
- assert type(frm) is ImageData
31
-
32
-
33
- def test_lima2_view():
34
- services = Mock()
35
- uuid = str(uuid1())
36
- view = Lima2View(
37
- services=services,
38
- acq_uuid=uuid,
39
- source="cafe",
40
- start=0,
41
- stop=42,
42
- )
43
- assert len(view) == 42
44
- assert view.index == 0
45
-
46
- frames = view.get_data()
47
- assert len(frames) == len(view)
48
-
49
- services.pipeline.get_frame.assert_has_calls(
50
- [call(uuid=uuid, source="cafe", frame_idx=i) for i in range(42)]
51
- )
52
-
53
-
54
- def test_lima2_protocol(data_store):
55
- uuid = str(uuid1())
56
-
57
- stream_def = Lima2Stream.make_definition(
58
- name="device:cafe",
59
- source_name="cafe",
60
- conductor_hostname="www.lima2.org",
61
- conductor_port=12345,
62
- acq_uuid=uuid,
63
- master_file=None,
64
- dtype=np.float128, # fat pixels >:)
65
- shape=(4, 1024, 512),
66
- )
67
- model = data_store._stream_model(
68
- encoding=stream_def.encoder.info(), info=stream_def.info
69
- )
70
- model.info["protocol_version"] = 1 # hack the protocol number
71
- event_stream = EventStream.create(data_store, stream_def.name, model)
72
-
73
- with pytest.raises(RuntimeError):
74
- _ = Lima2Stream(event_stream=event_stream)
75
-
76
-
77
- def test_lima2_stream(data_store, monkeypatch):
78
- uuid = str(uuid1())
79
-
80
- stream_def = Lima2Stream.make_definition(
81
- name="device:cafe",
82
- source_name="cafe",
83
- conductor_hostname="www.lima2.org",
84
- conductor_port=12345,
85
- acq_uuid=uuid,
86
- master_file=None,
87
- dtype=np.float128, # fat pixels >:)
88
- shape=(4, 1024, 512),
89
- )
90
- model = data_store._stream_model(
91
- encoding=stream_def.encoder.info(), info=stream_def.info
92
- )
93
-
94
- event_stream = EventStream.create(data_store, stream_def.name, model)
95
- stream = Lima2Stream(event_stream=event_stream)
96
-
97
- assert stream.plugin == "lima2"
98
- assert stream.shape == stream_def.info["shape"]
99
-
100
- # Feed the event stream
101
- event_stream.send(np.uint32(42))
102
- event_stream.join()
103
- assert len(stream) == 42
104
-
105
- event_stream.send(np.uint32(123))
106
- event_stream.join()
107
- assert len(stream) == 123
108
-
109
- mock_get_frame = Mock()
110
- monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
111
-
112
- # Indexing
113
- _ = stream[0]
114
- mock_get_frame.assert_called_with(
115
- services=stream._services,
116
- acq_uuid=uuid,
117
- source="cafe",
118
- frame_idx=0,
119
- )
120
-
121
- with pytest.raises(IndexNotYetThereError):
122
- _ = stream[-1]
123
-
124
- # Slicing
125
- _ = stream[:3]
126
- assert mock_get_frame.mock_calls[-3:] == [
127
- call(services=stream._services, acq_uuid=uuid, source="cafe", frame_idx=0),
128
- call(services=stream._services, acq_uuid=uuid, source="cafe", frame_idx=1),
129
- call(services=stream._services, acq_uuid=uuid, source="cafe", frame_idx=2),
130
- ]
131
-
132
- with pytest.raises(IndexNotYetThereError):
133
- _ = stream[-2:]
134
-
135
- with pytest.raises(IndexNotYetThereError):
136
- _ = stream[:-2]
137
-
138
- event_stream.seal()
139
- event_stream.join()
140
-
141
- # Now slicing/indexing from end is ok
142
- _ = stream[-2:]
143
- assert mock_get_frame.mock_calls[-2:] == [
144
- call(
145
- services=stream._services,
146
- acq_uuid=uuid,
147
- source="cafe",
148
- frame_idx=123 - 2,
149
- ),
150
- call(
151
- services=stream._services,
152
- acq_uuid=uuid,
153
- source="cafe",
154
- frame_idx=123 - 1,
155
- ),
156
- ]
157
-
158
- _ = stream[-3]
159
- assert mock_get_frame.mock_calls[-1] == call(
160
- services=stream._services,
161
- acq_uuid=uuid,
162
- source="cafe",
163
- frame_idx=123 - 3,
164
- )
165
-
166
- _ = stream[:]
167
- assert mock_get_frame.mock_calls[-123:] == [
168
- call(services=stream._services, acq_uuid=uuid, source="cafe", frame_idx=i)
169
- for i in range(123)
170
- ]
171
-
172
- _ = stream.get_last_live_image()
173
- mock_get_frame.assert_called_with(
174
- services=stream._services,
175
- acq_uuid=uuid,
176
- source="cafe",
177
- frame_idx=-1,
178
- )
File without changes
File without changes