blissdata 2.3.0__tar.gz → 2.4.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.
- {blissdata-2.3.0/src/blissdata.egg-info → blissdata-2.4.1}/PKG-INFO +4 -4
- {blissdata-2.3.0 → blissdata-2.4.1}/pyproject.toml +4 -4
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/utils/hdf5.py +12 -1
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5map/__init__.py +1 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5map/data_items.py +74 -9
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/lima/client.py +46 -26
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/lima/image_utils.py +2 -2
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/redis_engine/memory_tracker.py +11 -4
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/redis_engine/store.py +2 -2
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/schemas/scan_info_display.py +0 -1
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/lima/stream.py +39 -47
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/beacon/test_files.py +2 -2
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/conftest.py +0 -1
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/h5api/test_dynamic_files.py +89 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/h5api/test_redis_files.py +6 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/h5map/test_data_items.py +91 -1
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/lima/test_image_utils.py +2 -2
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/redis_engine/test_memory_tracker.py +11 -5
- blissdata-2.4.1/src/blissdata/tests/streams/plugins/test_lima_stream.py +168 -0
- {blissdata-2.3.0 → blissdata-2.4.1/src/blissdata.egg-info}/PKG-INFO +4 -4
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata.egg-info/SOURCES.txt +1 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata.egg-info/requires.txt +3 -3
- {blissdata-2.3.0 → blissdata-2.4.1}/LICENSE +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/README.md +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/setup.cfg +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/beacon/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/beacon/_base.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/beacon/_utils.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/beacon/config.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/beacon/data.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/beacon/files.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/deprecation.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/exceptions.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/abstract.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/dynamic_hdf5.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/file_arguments.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/h5_scan_map.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/redis_hdf5.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/static_hdf5.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/utils/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/utils/bliss.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/utils/hdf5_retry.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/utils/lima.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5api/utils/types.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/h5map/hdf5_items.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/lima/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/py.typed +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/redis_engine/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/redis_engine/encoding/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/redis_engine/encoding/json.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/redis_engine/encoding/numeric.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/redis_engine/event_stream.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/redis_engine/exceptions.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/redis_engine/identities.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/redis_engine/scan.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/scan.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/schemas/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/schemas/scan_info.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/base.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/default.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/encoding/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/encoding/json.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/encoding/numeric.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/event_stream.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/hdf5_fallback/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/hdf5_fallback/stream.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/lima/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/scan_sequence/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/scan_sequence/stream.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/streams/sink.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/beacon/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/beacon/test_config.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/beacon/test_data.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/h5api/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/h5api/scanner.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/h5api/test_h5api_lima_utils.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/h5api/test_hdf5_handlers.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/h5api/test_static_files.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/h5map/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/h5map/test_hdf5_items.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/lima/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/redis_engine/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/redis_engine/conftest.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/redis_engine/test_cursor.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/redis_engine/test_scan.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/redis_engine/test_search.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/redis_engine/utils.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/streams/encoding/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/streams/encoding/test_json.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/streams/encoding/test_numeric.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/streams/plugins/__init__.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/streams/plugins/test_file_backed_stream.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/streams/plugins/test_plugin_base.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata/tests/streams/test_event_stream.py +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata.egg-info/dependency_links.txt +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/src/blissdata.egg-info/entry_points.txt +0 -0
- {blissdata-2.3.0 → blissdata-2.4.1}/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.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: Bliss data streaming client
|
|
5
5
|
Maintainer: BCU (ESRF)
|
|
6
6
|
License-Expression: LGPL-3.0-or-later
|
|
@@ -19,9 +19,9 @@ Requires-Dist: h5py>=2.9
|
|
|
19
19
|
Requires-Dist: msgpack
|
|
20
20
|
Requires-Dist: numpy>=1.20
|
|
21
21
|
Requires-Dist: numpydantic<1.6.10
|
|
22
|
-
Requires-Dist: pydantic>=2.
|
|
22
|
+
Requires-Dist: pydantic>=2.9
|
|
23
23
|
Requires-Dist: redis>=4.6.0
|
|
24
|
-
Requires-Dist: redis-om
|
|
24
|
+
Requires-Dist: redis-om~=1.0
|
|
25
25
|
Requires-Dist: silx>=1.1.0
|
|
26
26
|
Provides-Extra: tango
|
|
27
27
|
Requires-Dist: pytango>=9.3.1; extra == "tango"
|
|
@@ -36,7 +36,7 @@ Requires-Dist: pytest-timeout; extra == "test"
|
|
|
36
36
|
Requires-Dist: packaging; extra == "test"
|
|
37
37
|
Provides-Extra: dev
|
|
38
38
|
Requires-Dist: blissdata[test]; extra == "dev"
|
|
39
|
-
Requires-Dist: black
|
|
39
|
+
Requires-Dist: black; extra == "dev"
|
|
40
40
|
Requires-Dist: flake8>=4; extra == "dev"
|
|
41
41
|
Requires-Dist: flake8-pep585; extra == "dev"
|
|
42
42
|
Requires-Dist: mkdocs; extra == "dev"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "blissdata"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.1"
|
|
4
4
|
description = "Bliss data streaming client"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "LGPL-3.0-or-later"
|
|
@@ -19,9 +19,9 @@ dependencies = [
|
|
|
19
19
|
"msgpack",
|
|
20
20
|
"numpy>=1.20",
|
|
21
21
|
"numpydantic<1.6.10",
|
|
22
|
-
"pydantic>=2.
|
|
22
|
+
"pydantic>=2.9",
|
|
23
23
|
"redis>=4.6.0",
|
|
24
|
-
"redis-om
|
|
24
|
+
"redis-om~=1.0",
|
|
25
25
|
"silx>=1.1.0",
|
|
26
26
|
]
|
|
27
27
|
|
|
@@ -43,7 +43,7 @@ test = [
|
|
|
43
43
|
]
|
|
44
44
|
dev = [
|
|
45
45
|
"blissdata[test]",
|
|
46
|
-
"black
|
|
46
|
+
"black",
|
|
47
47
|
"flake8>=4",
|
|
48
48
|
"flake8-pep585",
|
|
49
49
|
"mkdocs",
|
|
@@ -251,7 +251,7 @@ class DynamicHDF5Handler:
|
|
|
251
251
|
try:
|
|
252
252
|
return item.attrs[key] # try one more time
|
|
253
253
|
except KeyError as e:
|
|
254
|
-
if not
|
|
254
|
+
if not _attribute_does_not_exist(e):
|
|
255
255
|
raise
|
|
256
256
|
# attribute will never be created
|
|
257
257
|
raise RetryTimeoutError(str(e)) from e
|
|
@@ -366,3 +366,14 @@ def _key_does_not_exist(exc: KeyError) -> bool:
|
|
|
366
366
|
|
|
367
367
|
"""
|
|
368
368
|
return "doesn't exist" in str(exc)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _attribute_does_not_exist(exc: KeyError) -> bool:
|
|
372
|
+
"""
|
|
373
|
+
A real KeyError (meaning that the attribute does not exist) gives the following error message:
|
|
374
|
+
|
|
375
|
+
.. code
|
|
376
|
+
|
|
377
|
+
KeyError: "Unable to synchronously open attribute (can't locate attribute: 'myattr')"
|
|
378
|
+
"""
|
|
379
|
+
return "can't locate attribute" in str(exc)
|
|
@@ -6,6 +6,7 @@ from pydantic import (
|
|
|
6
6
|
Base64Bytes,
|
|
7
7
|
Field,
|
|
8
8
|
field_validator,
|
|
9
|
+
model_validator,
|
|
9
10
|
model_serializer,
|
|
10
11
|
Discriminator,
|
|
11
12
|
Tag,
|
|
@@ -64,7 +65,13 @@ class InlineRaw(RootModel):
|
|
|
64
65
|
def decode(self):
|
|
65
66
|
value = self.model_dump()
|
|
66
67
|
if value.dtype.kind in "UT":
|
|
67
|
-
|
|
68
|
+
# Make strings similar to those read back from h5py:
|
|
69
|
+
# - Convert to "object" dtype
|
|
70
|
+
# - Tell h5py that "object" is variable length string using
|
|
71
|
+
# metadata, so writing doesn't fail even in a compound dataset.
|
|
72
|
+
return np.vectorize(
|
|
73
|
+
lambda x: x.encode(), otypes=[np.dtype("O", metadata={"vlen": str})]
|
|
74
|
+
)(value)
|
|
68
75
|
else:
|
|
69
76
|
return value
|
|
70
77
|
|
|
@@ -155,14 +162,6 @@ class InlineBase64(BaseModel):
|
|
|
155
162
|
return value
|
|
156
163
|
|
|
157
164
|
|
|
158
|
-
class Stream(BaseModel):
|
|
159
|
-
type: Literal["stream"] = "stream"
|
|
160
|
-
stream: str
|
|
161
|
-
|
|
162
|
-
def decode(self):
|
|
163
|
-
raise TypeError("Stream model is just a reference, can't use decode()")
|
|
164
|
-
|
|
165
|
-
|
|
166
165
|
def get_discriminator_value(v: Any) -> str:
|
|
167
166
|
if isinstance(v, dict):
|
|
168
167
|
return v.get("type")
|
|
@@ -171,12 +170,78 @@ def get_discriminator_value(v: Any) -> str:
|
|
|
171
170
|
return "raw"
|
|
172
171
|
|
|
173
172
|
|
|
173
|
+
class Compound(BaseModel):
|
|
174
|
+
"""Composite model to combine multiple DataItems as a structured array.
|
|
175
|
+
Unlike numpy structured arrays where data is a list of heterogeneous tuples,
|
|
176
|
+
here all elements of one dtype are stored together. This way existing
|
|
177
|
+
DataItem models can be reused as columns.
|
|
178
|
+
|
|
179
|
+
Validation ensures:
|
|
180
|
+
- Column names uniqueness
|
|
181
|
+
- Shape consistency between columns
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
type: Literal["compound"] = "compound"
|
|
185
|
+
columns: list[
|
|
186
|
+
tuple[
|
|
187
|
+
str,
|
|
188
|
+
Annotated[
|
|
189
|
+
(
|
|
190
|
+
Annotated[Inline, Tag("inline")]
|
|
191
|
+
| Annotated[InlineBase64, Tag("inline_b64")]
|
|
192
|
+
| Annotated[InlineRaw, Tag("raw")]
|
|
193
|
+
| Annotated["Compound", Tag("compound")]
|
|
194
|
+
# Note: Stream can't be part of a compound object
|
|
195
|
+
),
|
|
196
|
+
Discriminator(get_discriminator_value),
|
|
197
|
+
],
|
|
198
|
+
]
|
|
199
|
+
] = Field(min_length=1)
|
|
200
|
+
# Reusing DataItemType simplifies this model as it does not redefine
|
|
201
|
+
# handling of dtype, shape or base64 version of the values.
|
|
202
|
+
# Therefore the representation is the transposed of numpy one, which can be
|
|
203
|
+
# harder to read.
|
|
204
|
+
|
|
205
|
+
def decode(self):
|
|
206
|
+
decoded_columns = [(name, value.decode()) for name, value in self.columns]
|
|
207
|
+
rec = np.rec.fromarrays(
|
|
208
|
+
[value for _, value in decoded_columns],
|
|
209
|
+
names=[name for name, _ in decoded_columns],
|
|
210
|
+
)
|
|
211
|
+
return rec.view(np.ndarray)
|
|
212
|
+
|
|
213
|
+
@model_validator(mode="after")
|
|
214
|
+
@classmethod
|
|
215
|
+
def verify_names_and_shapes(cls, value):
|
|
216
|
+
"""Ensure uniqueness of names and shape equality between columns"""
|
|
217
|
+
if len(value.columns) != len({name for name, _ in value.columns}):
|
|
218
|
+
raise ValueError("found columns with same name")
|
|
219
|
+
|
|
220
|
+
shape_iterator = iter((col.decode().shape for _, col in value.columns))
|
|
221
|
+
try:
|
|
222
|
+
first_shape = next(shape_iterator)
|
|
223
|
+
except StopIteration:
|
|
224
|
+
return value
|
|
225
|
+
if not all(first_shape == shape for shape in shape_iterator):
|
|
226
|
+
raise ValueError("columns have different shapes")
|
|
227
|
+
return value
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class Stream(BaseModel):
|
|
231
|
+
type: Literal["stream"] = "stream"
|
|
232
|
+
stream: str
|
|
233
|
+
|
|
234
|
+
def decode(self):
|
|
235
|
+
raise TypeError("Stream model is just a reference, can't use decode()")
|
|
236
|
+
|
|
237
|
+
|
|
174
238
|
DataItemType = Annotated[
|
|
175
239
|
(
|
|
176
240
|
Annotated[Inline, Tag("inline")]
|
|
177
241
|
| Annotated[InlineBase64, Tag("inline_b64")]
|
|
178
242
|
| Annotated[Stream, Tag("stream")]
|
|
179
243
|
| Annotated[InlineRaw, Tag("raw")]
|
|
244
|
+
| Annotated[Compound, Tag("compound")]
|
|
180
245
|
),
|
|
181
246
|
Discriminator(get_discriminator_value),
|
|
182
247
|
]
|
|
@@ -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
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
298
|
+
raise IndexError
|
|
282
299
|
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
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
|
-
|
|
301
|
-
|
|
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(
|
|
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(
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
# Copyright (c) Beamline Control Unit, ESRF
|
|
6
6
|
# Distributed under the GNU LGPLv3. See LICENSE for more info.
|
|
7
7
|
|
|
8
|
-
"""Utility functions to read Lima images from file or device server.
|
|
9
|
-
|
|
8
|
+
"""Utility functions to read Lima images from file or device server."""
|
|
9
|
+
|
|
10
10
|
import struct
|
|
11
11
|
import numpy
|
|
12
12
|
import typing
|
|
@@ -16,7 +16,6 @@ from dataclasses import dataclass
|
|
|
16
16
|
from blissdata import DataStore, Scan, ScanState
|
|
17
17
|
from blissdata.exceptions import NoScanAvailable, ScanNotFoundError, ScanLoadError
|
|
18
18
|
|
|
19
|
-
|
|
20
19
|
memory_tracking_stream = "_MEMORY_TRACKING_"
|
|
21
20
|
SEAL_ID = ("-".join([str(2**64 - 1)] * 2)).encode()
|
|
22
21
|
|
|
@@ -35,6 +34,7 @@ class NoFreeableMemoryError(Exception):
|
|
|
35
34
|
class TrackedScan:
|
|
36
35
|
scan: Scan
|
|
37
36
|
last_activity: int
|
|
37
|
+
last_streams_ids: dict[str, int]
|
|
38
38
|
|
|
39
39
|
def __eq__(self, other):
|
|
40
40
|
return self.scan.key == other.scan.key
|
|
@@ -93,7 +93,7 @@ class MemoryTracker:
|
|
|
93
93
|
scan._model.state_stream.key(), count=1
|
|
94
94
|
)
|
|
95
95
|
last_activity = int(raw[0][0].split(b"-")[0]) / 1000
|
|
96
|
-
self._tracked_scans.add(TrackedScan(scan, last_activity))
|
|
96
|
+
self._tracked_scans.add(TrackedScan(scan, last_activity, {}))
|
|
97
97
|
except ScanNotFoundError:
|
|
98
98
|
# scan already deleted from Redis by user, skip it
|
|
99
99
|
pass
|
|
@@ -113,7 +113,7 @@ class MemoryTracker:
|
|
|
113
113
|
try:
|
|
114
114
|
scan = self._data_store.load_scan(scan_key)
|
|
115
115
|
last_activity = int(since.split("-")[0]) / 1000
|
|
116
|
-
self._tracked_scans.add(TrackedScan(scan, last_activity))
|
|
116
|
+
self._tracked_scans.add(TrackedScan(scan, last_activity, {}))
|
|
117
117
|
self._since = since
|
|
118
118
|
logging.info(f"Start tracking {scan.key}")
|
|
119
119
|
except ScanNotFoundError:
|
|
@@ -135,7 +135,14 @@ class MemoryTracker:
|
|
|
135
135
|
|
|
136
136
|
if track.scan.state >= ScanState.PREPARED:
|
|
137
137
|
streams_ids = self._data_streams_ids(track.scan)
|
|
138
|
-
|
|
138
|
+
stream_activity = {
|
|
139
|
+
key: idx
|
|
140
|
+
for key, idx in streams_ids.items()
|
|
141
|
+
if key not in track.last_streams_ids
|
|
142
|
+
or track.last_streams_ids[key] != idx
|
|
143
|
+
}
|
|
144
|
+
if stream_activity:
|
|
145
|
+
track.last_streams_ids = streams_ids
|
|
139
146
|
track.last_activity, _ = self._data_store._redis.time()
|
|
140
147
|
all_data_streams_ids.update(streams_ids)
|
|
141
148
|
|
|
@@ -43,7 +43,7 @@ class DataStore:
|
|
|
43
43
|
close_func = type(redis_pool).disconnect
|
|
44
44
|
weakref.finalize(self, close_func, redis_pool)
|
|
45
45
|
|
|
46
|
-
class StreamModel(HashModel):
|
|
46
|
+
class StreamModel(HashModel, index=True):
|
|
47
47
|
"""Actual stream model to be embedded into ScanModel, but not exposed to user directly.
|
|
48
48
|
They are wrapped into Stream objects instead."""
|
|
49
49
|
|
|
@@ -60,7 +60,7 @@ class DataStore:
|
|
|
60
60
|
encoding: dict | str | None = None
|
|
61
61
|
info: dict | str | None = None
|
|
62
62
|
|
|
63
|
-
class ScanModel(JsonModel):
|
|
63
|
+
class ScanModel(JsonModel, index=True):
|
|
64
64
|
class Meta:
|
|
65
65
|
global_key_prefix = identity_model_cls._meta.global_key_prefix
|
|
66
66
|
model_key_prefix = "scan"
|
|
@@ -28,8 +28,8 @@ from blissdata.lima.image_utils import ImageData
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class LimaView(BaseView):
|
|
31
|
-
def __init__(self,
|
|
32
|
-
self.
|
|
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.
|
|
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.
|
|
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,
|
|
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,
|
|
@@ -148,28 +154,41 @@ class LimaStream(BaseStream, LimaDirectAccess):
|
|
|
148
154
|
self._update_client()
|
|
149
155
|
return len(self._client)
|
|
150
156
|
|
|
151
|
-
def
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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()
|
|
@@ -23,7 +23,7 @@ def test_param_constructor(mocker):
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def test_get_file(mocker):
|
|
26
|
-
RESPONSE1 = b"\x34\x00\x00\x00\
|
|
26
|
+
RESPONSE1 = b"\x34\x00\x00\x00\x0e\x00\x00\x001|MY FILE DATA"
|
|
27
27
|
mock_socket = mocker.patch("socket.socket")
|
|
28
28
|
mock_socket.return_value.recv.side_effect = [RESPONSE1]
|
|
29
29
|
service = BeaconFiles("foo", 200)
|
|
@@ -35,7 +35,7 @@ def test_get_file(mocker):
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def test_get_tree(mocker):
|
|
38
|
-
RESPONSE1 = b"""\x58\x00\x00\x00\
|
|
38
|
+
RESPONSE1 = b"""\x58\x00\x00\x00\x2c\x00\x00\x001|{"afile":null,"adir":{"anotherfile":null}}"""
|
|
39
39
|
mock_socket = mocker.patch("socket.socket")
|
|
40
40
|
mock_socket.return_value.recv.side_effect = [RESPONSE1]
|
|
41
41
|
service = BeaconFiles("foo", 200)
|