blissdata 2.4.0__tar.gz → 2.4.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.
Files changed (101) hide show
  1. {blissdata-2.4.0/src/blissdata.egg-info → blissdata-2.4.2}/PKG-INFO +1 -1
  2. {blissdata-2.4.0 → blissdata-2.4.2}/pyproject.toml +1 -1
  3. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/lima/client.py +46 -26
  4. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/lima/stream.py +40 -48
  5. blissdata-2.4.2/src/blissdata/tests/streams/plugins/test_lima_stream.py +173 -0
  6. {blissdata-2.4.0 → blissdata-2.4.2/src/blissdata.egg-info}/PKG-INFO +1 -1
  7. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata.egg-info/SOURCES.txt +1 -0
  8. {blissdata-2.4.0 → blissdata-2.4.2}/LICENSE +0 -0
  9. {blissdata-2.4.0 → blissdata-2.4.2}/README.md +0 -0
  10. {blissdata-2.4.0 → blissdata-2.4.2}/setup.cfg +0 -0
  11. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/__init__.py +0 -0
  12. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/beacon/__init__.py +0 -0
  13. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/beacon/_base.py +0 -0
  14. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/beacon/_utils.py +0 -0
  15. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/beacon/config.py +0 -0
  16. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/beacon/data.py +0 -0
  17. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/beacon/files.py +0 -0
  18. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/deprecation.py +0 -0
  19. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/exceptions.py +0 -0
  20. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/__init__.py +0 -0
  21. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/abstract.py +0 -0
  22. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/dynamic_hdf5.py +0 -0
  23. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/file_arguments.py +0 -0
  24. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/h5_scan_map.py +0 -0
  25. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/redis_hdf5.py +0 -0
  26. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/static_hdf5.py +0 -0
  27. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/utils/__init__.py +0 -0
  28. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/utils/bliss.py +0 -0
  29. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/utils/hdf5.py +0 -0
  30. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/utils/hdf5_retry.py +0 -0
  31. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/utils/lima.py +0 -0
  32. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5api/utils/types.py +0 -0
  33. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5map/__init__.py +0 -0
  34. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5map/data_items.py +0 -0
  35. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/h5map/hdf5_items.py +0 -0
  36. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/lima/__init__.py +0 -0
  37. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/lima/image_utils.py +0 -0
  38. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/py.typed +0 -0
  39. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/redis_engine/__init__.py +0 -0
  40. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/redis_engine/encoding/__init__.py +0 -0
  41. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/redis_engine/encoding/json.py +0 -0
  42. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/redis_engine/encoding/numeric.py +0 -0
  43. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/redis_engine/event_stream.py +0 -0
  44. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/redis_engine/exceptions.py +0 -0
  45. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/redis_engine/identities.py +0 -0
  46. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/redis_engine/memory_tracker.py +0 -0
  47. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/redis_engine/scan.py +0 -0
  48. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/redis_engine/store.py +0 -0
  49. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/scan.py +0 -0
  50. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/schemas/__init__.py +0 -0
  51. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/schemas/scan_info.py +0 -0
  52. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/schemas/scan_info_display.py +0 -0
  53. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/__init__.py +0 -0
  54. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/base.py +0 -0
  55. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/default.py +0 -0
  56. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/encoding/__init__.py +0 -0
  57. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/encoding/json.py +0 -0
  58. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/encoding/numeric.py +0 -0
  59. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/event_stream.py +0 -0
  60. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/hdf5_fallback/__init__.py +0 -0
  61. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/hdf5_fallback/stream.py +0 -0
  62. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/lima/__init__.py +0 -0
  63. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/scan_sequence/__init__.py +0 -0
  64. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/scan_sequence/stream.py +0 -0
  65. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/streams/sink.py +0 -0
  66. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/__init__.py +0 -0
  67. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/beacon/__init__.py +0 -0
  68. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/beacon/test_config.py +0 -0
  69. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/beacon/test_data.py +0 -0
  70. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/beacon/test_files.py +0 -0
  71. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/conftest.py +0 -0
  72. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/h5api/__init__.py +0 -0
  73. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/h5api/scanner.py +0 -0
  74. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/h5api/test_dynamic_files.py +0 -0
  75. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/h5api/test_h5api_lima_utils.py +0 -0
  76. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/h5api/test_hdf5_handlers.py +0 -0
  77. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/h5api/test_redis_files.py +0 -0
  78. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/h5api/test_static_files.py +0 -0
  79. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/h5map/__init__.py +0 -0
  80. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/h5map/test_data_items.py +0 -0
  81. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/h5map/test_hdf5_items.py +0 -0
  82. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/lima/__init__.py +0 -0
  83. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/lima/test_image_utils.py +0 -0
  84. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/redis_engine/__init__.py +0 -0
  85. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/redis_engine/conftest.py +0 -0
  86. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/redis_engine/test_cursor.py +0 -0
  87. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/redis_engine/test_memory_tracker.py +0 -0
  88. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/redis_engine/test_scan.py +0 -0
  89. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/redis_engine/test_search.py +0 -0
  90. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/redis_engine/utils.py +0 -0
  91. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/streams/encoding/__init__.py +0 -0
  92. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/streams/encoding/test_json.py +0 -0
  93. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/streams/encoding/test_numeric.py +0 -0
  94. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/streams/plugins/__init__.py +0 -0
  95. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/streams/plugins/test_file_backed_stream.py +0 -0
  96. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/streams/plugins/test_plugin_base.py +0 -0
  97. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata/tests/streams/test_event_stream.py +0 -0
  98. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata.egg-info/dependency_links.txt +0 -0
  99. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata.egg-info/entry_points.txt +0 -0
  100. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata.egg-info/requires.txt +0 -0
  101. {blissdata-2.4.0 → blissdata-2.4.2}/src/blissdata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blissdata
3
- Version: 2.4.0
3
+ Version: 2.4.2
4
4
  Summary: Bliss data streaming client
5
5
  Maintainer: BCU (ESRF)
6
6
  License-Expression: LGPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "blissdata"
3
- version = "2.4.0"
3
+ version = "2.4.2"
4
4
  description = "Bliss data streaming client"
5
5
  readme = "README.md"
6
6
  license = "LGPL-3.0-or-later"
@@ -5,13 +5,13 @@
5
5
  # Copyright (c) Beamline Control Unit, ESRF
6
6
  # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
7
  import math
8
+ import time
8
9
  import logging
9
10
  import numpy as np
10
11
  from packaging.version import Version
11
12
  from functools import cached_property
12
13
  from dataclasses import dataclass
13
14
 
14
- from blissdata.exceptions import IndexNoMoreThereError, IndexNotYetThereError
15
15
  from . import image_utils
16
16
 
17
17
  try:
@@ -58,7 +58,7 @@ class ImageReference:
58
58
  class LimaClient:
59
59
  PROTOCOL_VERSION = 1
60
60
 
61
- def __init__(self, data_store, **lima_info):
61
+ def __init__(self, data_store, request_update_cb, **lima_info):
62
62
  protocol = lima_info["protocol_version"]
63
63
  if self.PROTOCOL_VERSION != protocol:
64
64
  raise Exception(
@@ -66,6 +66,8 @@ class LimaClient:
66
66
  )
67
67
 
68
68
  self._data_store = data_store
69
+ self._request_update = request_update_cb
70
+ self._stream_sealed = False
69
71
  self._proxy = None
70
72
  self._server_url = lima_info["server_url"]
71
73
  self._buffer_max_number = lima_info["buffer_max_number"]
@@ -123,9 +125,10 @@ class LimaClient:
123
125
  def __len__(self):
124
126
  return self._last_index + 1
125
127
 
126
- def update(self, last_index, last_index_saved):
128
+ def update(self, last_index=-1, last_index_saved=-1, sealed=False):
127
129
  self._last_index = max(self._last_index, last_index)
128
130
  self._last_index_saved = max(self._last_index_saved, last_index_saved)
131
+ self._stream_sealed = self._stream_sealed or sealed
129
132
  if self._saved:
130
133
  self._last_index_saved_and_closed = self._last_readable_index()
131
134
 
@@ -155,10 +158,14 @@ class LimaClient:
155
158
 
156
159
  def _last_readable_index(self):
157
160
  """Return the last frame index that can be read from a closed file, assuming a
158
- file is closed when at least one frame is saved into the next one.
161
+ file is closed either when stream is sealed or when at least one frame is saved
162
+ into the next file.
159
163
  NOTE: This is trickier than a modulo as an acquisition may not be an exact
160
164
  multiple of the file length (see self._decompose_frame_id).
161
165
  """
166
+ if self._stream_sealed:
167
+ return self._last_index_saved
168
+
162
169
  acq_in_scan, file_in_acq, _ = self._decompose_frame_id(self._last_index_saved)
163
170
  return (
164
171
  acq_in_scan * self._frames_per_acquisition
@@ -215,7 +222,7 @@ class LimaClient:
215
222
  frame_id = normalize_frame_id(data.frame_id)
216
223
  return image_utils.ImageData(data.array, frame_id, None)
217
224
 
218
- def __getitem__(self, key):
225
+ def __getitem__(self, key: int | slice):
219
226
  if isinstance(key, int):
220
227
  return self._get_index(key)
221
228
  elif isinstance(key, slice):
@@ -232,21 +239,28 @@ class LimaClient:
232
239
  raise NotImplementedError("Negative index not supported")
233
240
 
234
241
  if self._saved and frame_id <= self._last_index_saved_and_closed:
235
- # immediately accessible from file
242
+ # 1. priority to file if available
236
243
  return self._get_from_file(frame_id)
237
244
  elif frame_id <= self._last_index:
238
- # try to access on lima server
239
245
  try:
246
+ # 2. try from server then
240
247
  return self._get_from_server(frame_id)
241
- except IndexNoMoreThereError:
242
- # wait for image to become available from file
243
- # TODO wait_for_file(timeout)
248
+ except IndexError:
244
249
  if self._saved:
250
+ # 3. finally wait for file
251
+ while (
252
+ frame_id > self._last_index_saved_and_closed
253
+ and not self._stream_sealed
254
+ ):
255
+ self._request_update()
256
+ time.sleep(0.1)
257
+ if frame_id > self._last_index_saved_and_closed:
258
+ raise
245
259
  return self._get_from_file(frame_id)
246
260
  else:
247
261
  raise
248
262
  else:
249
- raise IndexNotYetThereError
263
+ raise IndexError
250
264
 
251
265
  def _get_slice(self, key):
252
266
  key_range = range(*key.indices(len(self)))
@@ -267,7 +281,10 @@ class LimaClient:
267
281
 
268
282
  def _get_from_server(self, frame_id):
269
283
  if self._buffer_max_number <= (self._last_index - frame_id):
270
- raise IndexNoMoreThereError() # TODO IndexError here and more details in LimaStream ???
284
+ # if frame_id is in the current acquisition, then it's not in the
285
+ # buffer any longer. If another acquisition started, it's not here
286
+ # neither.
287
+ raise IndexError
271
288
 
272
289
  acq_in_scan, file_in_acq, frame_in_file = self._decompose_frame_id(frame_id)
273
290
  frame_in_acq = file_in_acq * self._frames_per_file + frame_in_file
@@ -278,15 +295,24 @@ class LimaClient:
278
295
  if not self._is_acquisition_still_on_server(
279
296
  acq_in_scan + self._acquisition_offset
280
297
  ):
281
- raise IndexNoMoreThereError() # TODO IndexError here and more details in LimaStream ???
298
+ raise IndexError
282
299
 
283
- frame = image_utils.image_from_server(self.proxy, frame_in_acq)
284
- assert frame_in_acq == frame.frame_id
300
+ try:
301
+ frame = image_utils.image_from_server(self.proxy, frame_in_acq)
302
+ except (RuntimeError, image_utils.NoImageAvailable) as e:
303
+ raise IndexError from e
304
+
305
+ if frame_in_acq != frame.frame_id:
306
+ _logger.warning(
307
+ f"Requested frame {frame_in_acq} from lima device, "
308
+ f"got frame {frame.frame_id}. Discarding it."
309
+ )
310
+ raise IndexError
285
311
 
286
312
  if not self._is_acquisition_still_on_server(
287
313
  acq_in_scan + self._acquisition_offset
288
314
  ):
289
- raise IndexNoMoreThereError() # TODO IndexError here and more details in LimaStream ???
315
+ raise IndexError
290
316
 
291
317
  return frame.array
292
318
 
@@ -296,19 +322,13 @@ class LimaClient:
296
322
  ref.file_path, ref.data_path, ref.index, ref.format
297
323
  )
298
324
 
299
- def get_references(self, key) -> ImageReference | list[ImageReference]:
300
- # NOTE: _last_index is used to define when references are available.
301
- # It should normally be _last_index_saved instead, but then, a LimaView
302
- # would not return the same amount of images and references.
303
- saved_len = self._last_index + 1
325
+ def get_references(self, key: int | slice) -> ImageReference | list[ImageReference]:
326
+ """Create references no matter the images are available or not. It is
327
+ caller responsibility to query indices that make sense."""
304
328
  if isinstance(key, int):
305
- if key < 0:
306
- key += saved_len
307
- elif key >= saved_len:
308
- raise IndexNotYetThereError
309
329
  return self._file_ref(key)
310
330
  elif isinstance(key, slice):
311
- key_range = range(*key.indices(saved_len))
331
+ key_range = range(*key.indices(len(self)))
312
332
  return [self._file_ref(i) for i in key_range]
313
333
  else:
314
334
  raise TypeError(
@@ -28,8 +28,8 @@ from blissdata.lima.image_utils import ImageData
28
28
 
29
29
 
30
30
  class LimaView(BaseView):
31
- def __init__(self, client, start, stop):
32
- self._client = client
31
+ def __init__(self, stream: "LimaStream", start: int, stop: int) -> None:
32
+ self._stream = stream
33
33
  self._view_range = range(start, stop)
34
34
 
35
35
  @property
@@ -41,13 +41,13 @@ class LimaView(BaseView):
41
41
 
42
42
  def get_data(self, start=None, stop=None):
43
43
  trimmed_range = self._view_range[start:stop]
44
- return self._client[trimmed_range.start : trimmed_range.stop]
44
+ return self._stream[trimmed_range.start : trimmed_range.stop]
45
45
 
46
46
  def get_references(
47
47
  self, start=None, stop=None
48
48
  ) -> ImageReference | list[ImageReference]:
49
49
  trimmed_range = self._view_range[start:stop]
50
- return self._client.get_references(
50
+ return self._stream.get_references(
51
51
  slice(trimmed_range.start, trimmed_range.stop)
52
52
  )
53
53
 
@@ -67,10 +67,16 @@ class LimaStream(BaseStream, LimaDirectAccess):
67
67
  def __init__(self, event_stream):
68
68
  super().__init__(event_stream)
69
69
  self._client = LimaClient(
70
- event_stream._data_store, **event_stream.info["lima_info"]
70
+ data_store=event_stream._data_store,
71
+ request_update_cb=self._force_update_client,
72
+ **event_stream.info["lima_info"],
71
73
  )
72
74
  self._cursor = Stream(event_stream).cursor()
73
75
 
76
+ def _force_update_client(self):
77
+ _, status_list, sealed = self._event_stream._read_last(0)
78
+ self._client.update(**status_list[0], sealed=sealed)
79
+
74
80
  @staticmethod
75
81
  def make_definition(
76
82
  name: str,
@@ -145,31 +151,44 @@ class LimaStream(BaseStream, LimaDirectAccess):
145
151
  return tuple(self.info["shape"])
146
152
 
147
153
  def __len__(self):
148
- self._update_client()
154
+ self._update_client_if_needed(-1)
149
155
  return len(self._client)
150
156
 
151
- def _update_client(self):
157
+ def _update_client_if_needed(self, key: int | slice):
158
+ if isinstance(key, slice):
159
+ need_update = key.stop is None or not (0 <= key.stop <= len(self._client))
160
+ else:
161
+ need_update = not (0 <= key < len(self._client))
162
+ if not need_update:
163
+ return
164
+
152
165
  try:
153
166
  view = self._cursor.read(block=False, last_only=True)
154
167
  except EndOfStream:
155
- return
156
- if view is not None:
168
+ view = None
169
+ # use ._seal instead of .is_sealed() to skip redis query and because
170
+ # we just updated it by reading cursor.
171
+ sealed = self._event_stream._seal is not None
172
+ if view is None:
173
+ self._client.update(sealed=sealed)
174
+ else:
157
175
  last_status = view.get_data(-1, None)[0]
158
- self._client.update(**last_status)
176
+ self._client.update(**last_status, sealed=sealed)
159
177
 
160
178
  def __getitem__(self, key):
161
- if isinstance(key, slice):
162
- need_update = key.stop is None or not (0 <= key.stop < len(self._client))
163
- else:
164
- need_update = not (0 <= key < len(self._client))
179
+ return self._get_from_client(key, reference_mode=False)
165
180
 
166
- if need_update:
167
- self._update_client()
181
+ def get_references(self, key) -> ImageReference | list[ImageReference]:
182
+ return self._get_from_client(key, reference_mode=True)
168
183
 
184
+ def _get_from_client(self, key, reference_mode):
185
+ self._update_client_if_needed(key)
169
186
  try:
170
- return self._client[key]
187
+ if reference_mode:
188
+ return self._client.get_references(key)
189
+ else:
190
+ return self._client[key]
171
191
  except IndexError:
172
- # TODO could be verified before asking the client
173
192
  index = key.start if isinstance(key, slice) else key
174
193
  if index is None:
175
194
  index = 0
@@ -177,7 +196,7 @@ class LimaStream(BaseStream, LimaDirectAccess):
177
196
  index += len(self._client)
178
197
 
179
198
  if index >= len(self._client):
180
- if self._event_stream.is_sealed():
199
+ if self._client._stream_sealed:
181
200
  raise IndexWontBeThereError
182
201
  else:
183
202
  raise IndexNotYetThereError
@@ -189,7 +208,7 @@ class LimaStream(BaseStream, LimaDirectAccess):
189
208
  return True
190
209
 
191
210
  def _build_view_from_events(self, index: int, events: EventRange, last_only: bool):
192
- self._client.update(**events.data[-1])
211
+ self._client.update(**events.data[-1], sealed=events.end_of_stream)
193
212
 
194
213
  if len(self._client) <= index:
195
214
  # no new images despite client update
@@ -199,34 +218,7 @@ class LimaStream(BaseStream, LimaDirectAccess):
199
218
  start = len(self._client) - 1
200
219
  else:
201
220
  start = index
202
- return LimaView(self._client, start, len(self._client))
203
-
204
- def get_references(self, key) -> ImageReference | list[ImageReference]:
205
- if isinstance(key, slice):
206
- need_update = key.stop is None or not (0 <= key.stop < len(self._client))
207
- else:
208
- need_update = not (0 <= key < len(self._client))
209
-
210
- if need_update:
211
- self._update_client()
212
-
213
- try:
214
- return self._client.get_references(key)
215
- except IndexError:
216
- # TODO could be verified before asking the client
217
- index = key.start if isinstance(key, slice) else key
218
- if index is None:
219
- index = 0
220
- elif index < 0:
221
- index += len(self._client)
222
-
223
- if index >= len(self._client):
224
- if self._event_stream.is_sealed():
225
- raise IndexWontBeThereError
226
- else:
227
- raise IndexNotYetThereError
228
- else:
229
- raise IndexNoMoreThereError
221
+ return LimaView(self, start, len(self._client))
230
222
 
231
223
  def get_last_live_image(self) -> ImageData:
232
224
  return self._client.get_last_live_image()
@@ -0,0 +1,173 @@
1
+ import pytest
2
+ import time
3
+ from threading import Thread
4
+ from contextlib import contextmanager
5
+
6
+ from blissdata.lima.client import LimaClient
7
+ from blissdata.streams.encoding.json import JsonStreamEncoder
8
+ from blissdata.streams.event_stream import EventStream
9
+ from blissdata.streams.lima import LimaStream
10
+ from blissdata.exceptions import (
11
+ IndexNoMoreThereError,
12
+ IndexNotYetThereError,
13
+ IndexWontBeThereError,
14
+ )
15
+
16
+
17
+ class LimaDataSimulator:
18
+ def __init__(self, rw_stream, buffer_size, acq_size, file_size=None):
19
+ self.rw_stream = rw_stream
20
+ self.last_index = -1
21
+ self.last_index_saved = -1
22
+
23
+ self.buffer_size = buffer_size
24
+ self.acq_size = acq_size
25
+ self.file_size = file_size
26
+
27
+ def publish_images(self, last_index=-1, last_index_saved=-1):
28
+ self.last_index = max(self.last_index, last_index)
29
+ self.last_index_saved = max(self.last_index_saved, last_index_saved)
30
+ self.rw_stream.send(
31
+ {"last_index": self.last_index, "last_index_saved": self.last_index_saved}
32
+ )
33
+ self.rw_stream.join()
34
+
35
+ def get_from_server_mock(self, frame_id: int):
36
+ if frame_id > self.last_index:
37
+ raise IndexError("Not acquired yet")
38
+ if frame_id <= self.last_index - self.buffer_size:
39
+ raise IndexError("Buffer expired")
40
+ print(f"read {frame_id} from memory")
41
+ return f"memo-{frame_id}"
42
+
43
+ def get_from_file_mock(self, frame_id):
44
+ if self.file_size is None:
45
+ raise Exception("Stream is not being saved")
46
+ if frame_id > (self.last_index_saved // self.file_size * self.file_size):
47
+ raise Exception("File not complete")
48
+ print(f"read {frame_id} from file")
49
+ return f"file-{frame_id}"
50
+
51
+
52
+ @contextmanager
53
+ def lima_stream_pair(data_store, buffer_size, acq_size, file_size=None):
54
+ info = {
55
+ "protocol_version": LimaClient.PROTOCOL_VERSION,
56
+ "server_url": "cafe",
57
+ "buffer_max_number": buffer_size,
58
+ "frame_per_acquisition": acq_size,
59
+ "acquisition_offset": 0,
60
+ }
61
+ if file_size is not None:
62
+ info.update(
63
+ {
64
+ "file_offset": 0,
65
+ "frame_per_file": file_size,
66
+ "file_path": "foo",
67
+ "data_path": "bar",
68
+ "file_format": "hdf5",
69
+ }
70
+ )
71
+
72
+ encoder = JsonStreamEncoder()
73
+ model = data_store._stream_model(encoding=encoder.info(), info={"lima_info": info})
74
+ event_rw_stream = EventStream.create(data_store, "test_stream", model)
75
+ event_ro_stream = EventStream.open(data_store, "test_stream", model)
76
+
77
+ try:
78
+ yield LimaStream(event_rw_stream), LimaStream(event_ro_stream)
79
+ finally:
80
+ event_rw_stream._sink.stop()
81
+
82
+
83
+ def test_get_image_no_file(monkeypatch, data_store):
84
+ params = dict(buffer_size=10, acq_size=1000, file_size=None)
85
+
86
+ with lima_stream_pair(data_store, **params) as (rw_stream, ro_stream):
87
+ data_simu = LimaDataSimulator(rw_stream, **params)
88
+ monkeypatch.setattr(
89
+ LimaClient, "_get_from_server", data_simu.get_from_server_mock
90
+ )
91
+ monkeypatch.setattr(LimaClient, "_get_from_file", data_simu.get_from_file_mock)
92
+
93
+ # not image at all
94
+ assert len(ro_stream) == 0
95
+ with pytest.raises(IndexNotYetThereError):
96
+ ro_stream[0]
97
+
98
+ # 0 to 4
99
+ data_simu.publish_images(last_index=4)
100
+ assert len(ro_stream) == 5
101
+ assert ro_stream[0] == "memo-0"
102
+ assert ro_stream[4] == "memo-4"
103
+ with pytest.raises(IndexNotYetThereError):
104
+ ro_stream[5]
105
+
106
+ # 5 to 14 (buffer wrapped around)
107
+ data_simu.publish_images(last_index=14)
108
+ assert len(ro_stream) == 15
109
+ assert ro_stream[5] == "memo-5"
110
+ assert ro_stream[14] == "memo-14"
111
+ with pytest.raises(IndexNoMoreThereError):
112
+ ro_stream[4]
113
+ with pytest.raises(IndexNotYetThereError):
114
+ ro_stream[15]
115
+
116
+ rw_stream.seal()
117
+ with pytest.raises(IndexWontBeThereError):
118
+ ro_stream[15]
119
+
120
+
121
+ @pytest.mark.timeout(60)
122
+ def test_get_image_file(monkeypatch, data_store):
123
+ params = dict(buffer_size=10, acq_size=1000, file_size=20)
124
+
125
+ with lima_stream_pair(data_store, **params) as (rw_stream, ro_stream):
126
+ data_simu = LimaDataSimulator(rw_stream, **params)
127
+ monkeypatch.setattr(
128
+ LimaClient, "_get_from_server", data_simu.get_from_server_mock
129
+ )
130
+ monkeypatch.setattr(LimaClient, "_get_from_file", data_simu.get_from_file_mock)
131
+
132
+ # buffer [0, 9], file [None]
133
+ data_simu.publish_images(last_index=9, last_index_saved=9)
134
+ assert len(ro_stream) == 10
135
+ assert ro_stream[0] == "memo-0"
136
+ assert ro_stream[9] == "memo-9"
137
+ with pytest.raises(IndexNotYetThereError):
138
+ ro_stream[10]
139
+
140
+ # buffer [15, 24], file [0-19]
141
+ data_simu.publish_images(last_index=24, last_index_saved=20)
142
+ assert len(ro_stream) == 25
143
+ assert ro_stream[0] == "file-0"
144
+ assert ro_stream[9] == "file-9"
145
+ assert ro_stream[19] == "file-19"
146
+ assert ro_stream[20] == "memo-20"
147
+ assert ro_stream[24] == "memo-24"
148
+ with pytest.raises(IndexNotYetThereError):
149
+ ro_stream[25]
150
+
151
+ # buffer [25, 34], file [0-19] ----> dead zone [20, 24]
152
+ data_simu.publish_images(last_index=34, last_index_saved=20)
153
+
154
+ def complete_file_soon():
155
+ time.sleep(0.5)
156
+ # buffer [31, 40], file [0-39] ----> file caught up the dead zone
157
+ data_simu.publish_images(last_index=40, last_index_saved=40)
158
+
159
+ t = Thread(target=complete_file_soon)
160
+ t.start()
161
+ # reading in the dead zone will wait for file index to catch up
162
+ assert ro_stream[24] == "file-24"
163
+ t.join()
164
+
165
+ # read from memory if file is not complete
166
+ assert ro_stream[40] == "memo-40"
167
+
168
+ rw_stream.seal()
169
+ with pytest.raises(IndexWontBeThereError):
170
+ ro_stream[41]
171
+
172
+ # read from file after stream sealing
173
+ assert ro_stream[40] == "file-40"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blissdata
3
- Version: 2.4.0
3
+ Version: 2.4.2
4
4
  Summary: Bliss data streaming client
5
5
  Maintainer: BCU (ESRF)
6
6
  License-Expression: LGPL-3.0-or-later
@@ -95,4 +95,5 @@ src/blissdata/tests/streams/encoding/test_json.py
95
95
  src/blissdata/tests/streams/encoding/test_numeric.py
96
96
  src/blissdata/tests/streams/plugins/__init__.py
97
97
  src/blissdata/tests/streams/plugins/test_file_backed_stream.py
98
+ src/blissdata/tests/streams/plugins/test_lima_stream.py
98
99
  src/blissdata/tests/streams/plugins/test_plugin_base.py
File without changes
File without changes
File without changes