blissdata 0.3.2__tar.gz → 1.0.0__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 (103) hide show
  1. blissdata-1.0.0/PKG-INFO +42 -0
  2. blissdata-1.0.0/README.md +3 -0
  3. {blissdata-0.3.2/blissdata/data/nodes → blissdata-1.0.0/blissdata}/__init__.py +1 -5
  4. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/beacon/_base.py +3 -1
  5. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/beacon/data.py +14 -4
  6. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/dynamic_hdf5.py +4 -1
  7. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/hdf5.py +30 -5
  8. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/hdf5_retry.py +1 -1
  9. blissdata-1.0.0/blissdata/lima/client.py +315 -0
  10. blissdata-0.3.2/blissdata/data/lima_image.py → blissdata-1.0.0/blissdata/lima/image_utils.py +45 -32
  11. blissdata-1.0.0/blissdata/redis_engine/encoding/__init__.py +48 -0
  12. blissdata-1.0.0/blissdata/redis_engine/encoding/json.py +26 -0
  13. blissdata-1.0.0/blissdata/redis_engine/encoding/numeric.py +133 -0
  14. blissdata-1.0.0/blissdata/redis_engine/exceptions.py +51 -0
  15. blissdata-1.0.0/blissdata/redis_engine/identities.py +37 -0
  16. blissdata-1.0.0/blissdata/redis_engine/memory_tracker.py +340 -0
  17. blissdata-1.0.0/blissdata/redis_engine/scan.py +322 -0
  18. blissdata-1.0.0/blissdata/redis_engine/sink.py +511 -0
  19. blissdata-1.0.0/blissdata/redis_engine/store.py +404 -0
  20. blissdata-1.0.0/blissdata/redis_engine/stream.py +533 -0
  21. blissdata-1.0.0/blissdata/scan.py +41 -0
  22. blissdata-1.0.0/blissdata/schemas/scan_info.py +344 -0
  23. blissdata-1.0.0/blissdata/schemas/scan_info_display.py +126 -0
  24. blissdata-1.0.0/blissdata/stream.py +204 -0
  25. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/beacon/test_data.py +2 -4
  26. blissdata-1.0.0/blissdata/tests/conftest.py +58 -0
  27. blissdata-1.0.0/blissdata/tests/h5api/__init__.py +0 -0
  28. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/h5api/scanner.py +1 -1
  29. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/h5api/test_dynamic_files.py +25 -29
  30. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/h5api/test_static_files.py +21 -0
  31. blissdata-1.0.0/blissdata/tests/redis_engine/__init__.py +0 -0
  32. blissdata-1.0.0/blissdata/tests/redis_engine/conftest.py +14 -0
  33. blissdata-1.0.0/blissdata/tests/redis_engine/encoding/__init__.py +0 -0
  34. blissdata-1.0.0/blissdata/tests/redis_engine/encoding/test_numeric.py +192 -0
  35. blissdata-1.0.0/blissdata/tests/redis_engine/test_memory_tracker.py +499 -0
  36. blissdata-1.0.0/blissdata/tests/redis_engine/test_scan.py +78 -0
  37. blissdata-1.0.0/blissdata/tests/redis_engine/test_search.py +216 -0
  38. blissdata-1.0.0/blissdata/tests/redis_engine/test_streaming.py +682 -0
  39. blissdata-1.0.0/blissdata/tests/redis_engine/utils.py +23 -0
  40. blissdata-1.0.0/blissdata.egg-info/PKG-INFO +42 -0
  41. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata.egg-info/SOURCES.txt +29 -35
  42. blissdata-1.0.0/blissdata.egg-info/entry_points.txt +2 -0
  43. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata.egg-info/requires.txt +12 -3
  44. {blissdata-0.3.2 → blissdata-1.0.0}/pyproject.toml +1 -4
  45. {blissdata-0.3.2 → blissdata-1.0.0}/setup.cfg +13 -3
  46. blissdata-0.3.2/PKG-INFO +0 -67
  47. blissdata-0.3.2/README.md +0 -49
  48. blissdata-0.3.2/blissdata/__init__.py +0 -17
  49. blissdata-0.3.2/blissdata/client.py +0 -62
  50. blissdata-0.3.2/blissdata/common/__init__.py +0 -15
  51. blissdata-0.3.2/blissdata/common/utils.py +0 -143
  52. blissdata-0.3.2/blissdata/data/__init__.py +0 -15
  53. blissdata-0.3.2/blissdata/data/events/__init__.py +0 -22
  54. blissdata-0.3.2/blissdata/data/events/channel.py +0 -149
  55. blissdata-0.3.2/blissdata/data/events/lima.py +0 -458
  56. blissdata-0.3.2/blissdata/data/events/node.py +0 -28
  57. blissdata-0.3.2/blissdata/data/events/scan.py +0 -49
  58. blissdata-0.3.2/blissdata/data/events/walk.py +0 -42
  59. blissdata-0.3.2/blissdata/data/expiration.py +0 -42
  60. blissdata-0.3.2/blissdata/data/node.py +0 -1619
  61. blissdata-0.3.2/blissdata/data/nodes/channel.py +0 -390
  62. blissdata-0.3.2/blissdata/data/nodes/dataset.py +0 -82
  63. blissdata-0.3.2/blissdata/data/nodes/dataset_collection.py +0 -12
  64. blissdata-0.3.2/blissdata/data/nodes/lima.py +0 -422
  65. blissdata-0.3.2/blissdata/data/nodes/node_ref_channel.py +0 -34
  66. blissdata-0.3.2/blissdata/data/nodes/proposal.py +0 -12
  67. blissdata-0.3.2/blissdata/data/nodes/scan.py +0 -184
  68. blissdata-0.3.2/blissdata/data/nodes/scan_group.py +0 -12
  69. blissdata-0.3.2/blissdata/data/remote_node.py +0 -214
  70. blissdata-0.3.2/blissdata/data/scan.py +0 -659
  71. blissdata-0.3.2/blissdata/redis/__init__.py +0 -12
  72. blissdata-0.3.2/blissdata/redis/caching.py +0 -395
  73. blissdata-0.3.2/blissdata/redis/connection.py +0 -168
  74. blissdata-0.3.2/blissdata/redis/manager.py +0 -164
  75. blissdata-0.3.2/blissdata/redis/proxy.py +0 -969
  76. blissdata-0.3.2/blissdata/redis/scripting.py +0 -24
  77. blissdata-0.3.2/blissdata/settings.py +0 -1160
  78. blissdata-0.3.2/blissdata/streaming.py +0 -817
  79. blissdata-0.3.2/blissdata/streaming_events.py +0 -355
  80. blissdata-0.3.2/blissdata/tests/redis/manager.py +0 -23
  81. blissdata-0.3.2/blissdata.egg-info/PKG-INFO +0 -67
  82. {blissdata-0.3.2 → blissdata-1.0.0}/LICENSE +0 -0
  83. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/beacon/__init__.py +0 -0
  84. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/beacon/config.py +0 -0
  85. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/beacon/files.py +0 -0
  86. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/__init__.py +0 -0
  87. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/abstract.py +0 -0
  88. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/file_arguments.py +0 -0
  89. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/static_hdf5.py +0 -0
  90. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/__init__.py +0 -0
  91. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/bliss.py +0 -0
  92. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/lima.py +0 -0
  93. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/types.py +0 -0
  94. {blissdata-0.3.2/blissdata/tests → blissdata-1.0.0/blissdata/lima}/__init__.py +0 -0
  95. {blissdata-0.3.2/blissdata/tests/beacon → blissdata-1.0.0/blissdata/redis_engine}/__init__.py +0 -0
  96. {blissdata-0.3.2/blissdata/tests/h5api → blissdata-1.0.0/blissdata/schemas}/__init__.py +0 -0
  97. {blissdata-0.3.2/blissdata/tests/redis → blissdata-1.0.0/blissdata/tests}/__init__.py +0 -0
  98. /blissdata-0.3.2/blissdata/tests/conftest.py → /blissdata-1.0.0/blissdata/tests/beacon/__init__.py +0 -0
  99. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/beacon/test_config.py +0 -0
  100. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/beacon/test_files.py +0 -0
  101. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata.egg-info/dependency_links.txt +0 -0
  102. {blissdata-0.3.2 → blissdata-1.0.0}/blissdata.egg-info/top_level.txt +0 -0
  103. {blissdata-0.3.2 → blissdata-1.0.0}/setup.py +0 -0
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.1
2
+ Name: blissdata
3
+ Version: 1.0.0
4
+ Summary: Bliss data streaming client
5
+ Home-page: https://gitlab.esrf.fr/bliss/bliss/-/tree/master/blissdata
6
+ Author: BCU (ESRF)
7
+ License: LGPLv3
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: <3.10,>=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: beartype
15
+ Requires-Dist: h5py<3.10,>=2.9
16
+ Requires-Dist: msgpack
17
+ Requires-Dist: numpy>=1.13
18
+ Requires-Dist: redis>=4.6.0
19
+ Requires-Dist: redis-om
20
+ Requires-Dist: silx>=1.1.0
21
+ Provides-Extra: tango
22
+ Requires-Dist: pytango>=9.3.1; extra == "tango"
23
+ Provides-Extra: test
24
+ Requires-Dist: gevent>=20.9.0; extra == "test"
25
+ Requires-Dist: pytest>=7; extra == "test"
26
+ Requires-Dist: pytest-mock; extra == "test"
27
+ Requires-Dist: pytest-redis; extra == "test"
28
+ Requires-Dist: pytest-timeout; extra == "test"
29
+ Provides-Extra: dev
30
+ Requires-Dist: gevent>=20.9.0; extra == "dev"
31
+ Requires-Dist: pytest>=7; extra == "dev"
32
+ Requires-Dist: pytest-mock; extra == "dev"
33
+ Requires-Dist: pytest-redis; extra == "dev"
34
+ Requires-Dist: pytest-timeout; extra == "dev"
35
+ Requires-Dist: black==22.3; extra == "dev"
36
+ Requires-Dist: flake8>=4; extra == "dev"
37
+ Provides-Extra: beacon
38
+ Requires-Dist: ruamel.yaml; extra == "beacon"
39
+
40
+ # Blissdata
41
+
42
+ Documentation [here](https://bliss.gitlab-pages.esrf.fr/bliss/master/blissdata/getting_started.html).
@@ -0,0 +1,3 @@
1
+ # Blissdata
2
+
3
+ Documentation [here](https://bliss.gitlab-pages.esrf.fr/bliss/master/blissdata/getting_started.html).
@@ -4,8 +4,4 @@
4
4
  #
5
5
  # Copyright (c) 2015-2023 Beamline Control Unit, ESRF
6
6
  # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
-
8
- """Data management
9
-
10
- node data types that map to structure in redis
11
- """
7
+ __version__ = "1.0.0"
@@ -1,5 +1,6 @@
1
1
  """Base client to communicate with Beacon."""
2
2
 
3
+ import platform
3
4
  import socket
4
5
  import struct
5
6
  import threading
@@ -28,7 +29,8 @@ class BeaconClient:
28
29
  else:
29
30
  self._address = host, port
30
31
  connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
31
- connection.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10)
32
+ if platform.system() != "Windows":
33
+ connection.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10)
32
34
  connection.connect(self._address)
33
35
  connection.settimeout(timeout)
34
36
  self._connection = connection
@@ -16,7 +16,8 @@ class BeaconData(BeaconClient):
16
16
  REDIS_DATA_SERVER_OK = 34
17
17
 
18
18
  def get_redis_db(self) -> str:
19
- """Returns the URL of the Redis database that contains the Bliss settings. For example 'foobar:25001'."""
19
+ """Returns the URL of the Redis database that contains the Bliss settings.
20
+ For example 'redis://foobar:25001' or 'unix:///tmp/redis.sock'."""
20
21
  msg = b"%s%s" % (struct.pack("<ii", self.REDIS_QUERY, 0), b"")
21
22
  self._connection.sendall(msg)
22
23
  data = b""
@@ -32,14 +33,23 @@ class BeaconData(BeaconClient):
32
33
  break
33
34
  if message_type != self.REDIS_QUERY_ANSWER:
34
35
  raise RuntimeError(f"Unexpected message type '{message_type}'")
35
- return message.decode()
36
+ host, port = message.decode().split(":")
37
+ if host == "localhost":
38
+ return f"unix://{port}"
39
+ else:
40
+ return f"redis://{host}:{port}"
36
41
 
37
42
  def get_redis_data_db(self) -> str:
38
- """Returns the URL of the Redis database that contains the Bliss scan data. For example 'foobar:25002'."""
43
+ """Returns the URL of the Redis database that contains the Bliss scan data.
44
+ For example 'redis://foobar:25002' or 'unix:///tmp/redis_data.sock'."""
39
45
  response = self._request(self.REDIS_DATA_SERVER_QUERY, "")
40
46
  response_type, data = response.read()
41
47
  if response_type == self.REDIS_DATA_SERVER_OK:
42
- return data.decode().replace("|", ":", 1)
48
+ host, port = data.decode().split("|")[:2]
49
+ if host == "localhost":
50
+ return f"unix://{port}"
51
+ else:
52
+ return f"redis://{host}:{port}"
43
53
  elif response_type == self.REDIS_DATA_SERVER_FAILED:
44
54
  raise RuntimeError(data.decode())
45
55
  raise RuntimeError(f"Unexpected Beacon response type {response_type}")
@@ -74,7 +74,10 @@ class Group(Node, abstract.Group):
74
74
 
75
75
  def __getitem__(self, key: str) -> Node:
76
76
  if not key.startswith(hdf5.SEP):
77
- key = f"{self.name}{hdf5.SEP}{key}"
77
+ if self.name == hdf5.SEP:
78
+ key = f"{hdf5.SEP}{key}"
79
+ else:
80
+ key = f"{self.name}{hdf5.SEP}{key}"
78
81
  h5py_obj = self._retry_handler.get_item(key)
79
82
  if isinstance(h5py_obj, h5py.Group):
80
83
  return Group(self._retry_handler, h5py_obj.name)
@@ -10,7 +10,7 @@ from . import types
10
10
  from . import lima
11
11
  from .hdf5_retry import retry_file_access
12
12
  from .hdf5_retry import ignore_retry_timeout
13
- from .hdf5_retry import RetryError, SoftRetryError
13
+ from .hdf5_retry import RetryTimeoutError, RetryError, SoftRetryError
14
14
 
15
15
 
16
16
  HDF5Item = Union[h5py.Dataset, h5py.Group, h5py.File, lima.LimaDataset, lima.LimaGroup]
@@ -71,13 +71,19 @@ class DynamicHDF5Handler:
71
71
  return self._file_obj
72
72
 
73
73
  def get_item(self, name: str) -> HDF5Item:
74
- return self._retry_get_item(name)
74
+ try:
75
+ return self._retry_get_item(name)
76
+ except RetryTimeoutError as e:
77
+ raise KeyError(name) from e
75
78
 
76
79
  def slice_dataset(self, name: str, idx: types.DataIndexType) -> types.DataType:
77
80
  return self._retry_slice_dataset(name, idx)
78
81
 
79
82
  def get_attr(self, name: str, key: str) -> Any:
80
- return self._retry_get_attr(name, key)
83
+ try:
84
+ return self._retry_get_attr(name, key)
85
+ except RetryTimeoutError as e:
86
+ raise KeyError(name) from e
81
87
 
82
88
  @ignore_retry_timeout
83
89
  def iter_attrs(self, name: str) -> Iterator[str]:
@@ -138,7 +144,21 @@ class DynamicHDF5Handler:
138
144
  item = self._native_items.get(name)
139
145
  if item is not None:
140
146
  return item
141
- item = self.file_obj[name]
147
+ try:
148
+ item = self.file_obj[name]
149
+ except KeyError as e:
150
+ parent_name = name.rpartition(SEP)[0]
151
+ if not parent_name:
152
+ parent_name = SEP
153
+ if parent_name != name:
154
+ try:
155
+ parent_item = self._get_item(parent_name)
156
+ except KeyError:
157
+ pass
158
+ else:
159
+ if self._is_finished(parent_item):
160
+ raise RetryTimeoutError(str(e)) from e
161
+ raise
142
162
  self._native_items[name] = item
143
163
  return item
144
164
 
@@ -152,7 +172,12 @@ class DynamicHDF5Handler:
152
172
 
153
173
  def _get_attr(self, name: str, key: str) -> Any:
154
174
  item = self._get_item(name)
155
- return item.attrs[key]
175
+ try:
176
+ return item.attrs[key]
177
+ except KeyError as e:
178
+ if self._is_finished(item):
179
+ raise RetryTimeoutError(str(e)) from e
180
+ raise
156
181
 
157
182
  def _iter_attrs(self, name: str, start_index: int = 0) -> Iterator[str]:
158
183
  item = self._get_item(name)
@@ -22,7 +22,7 @@ def ignore_retry_timeout(method: Callable) -> Callable:
22
22
  @wraps(method)
23
23
  def wrapper(self, *args, **kw):
24
24
  try:
25
- yield from method(self, *args, *kw)
25
+ yield from method(self, *args, **kw)
26
26
  except RetryTimeoutError:
27
27
  pass
28
28
 
@@ -0,0 +1,315 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of the bliss project
4
+ #
5
+ # Copyright (c) 2015-2023 Beamline Control Unit, ESRF
6
+ # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
+
8
+ from __future__ import annotations
9
+ import math
10
+ import numpy as np
11
+ from abc import ABC, abstractmethod
12
+ from dataclasses import dataclass
13
+ from typing import Optional
14
+
15
+ from blissdata.redis_engine.exceptions import (
16
+ IndexNoMoreThereError,
17
+ IndexNotYetThereError,
18
+ )
19
+ from . import image_utils
20
+
21
+ try:
22
+ from gevent.monkey import is_anything_patched
23
+ except ImportError:
24
+ use_gevent = False
25
+ else:
26
+ use_gevent = is_anything_patched()
27
+
28
+ try:
29
+ if use_gevent:
30
+ from tango.gevent import DeviceProxy
31
+ else:
32
+ from tango import DeviceProxy
33
+ from tango import DevSource
34
+ except ImportError:
35
+ DeviceProxy = None
36
+ DevSource = None
37
+
38
+
39
+ def acquisition_on_server(data_store, server_url):
40
+ return int(data_store._redis.get(server_url).decode())
41
+
42
+
43
+ def prepare_next_lima_acquisition(data_store, server_url):
44
+ pipe = data_store._redis.pipeline(transaction=True)
45
+ # when the key doesn't exist, set it to -1 before .incr()
46
+ pipe.setnx(server_url, -1)
47
+ pipe.incr(server_url)
48
+ return pipe.execute()[1]
49
+
50
+
51
+ @dataclass
52
+ class ImageReference:
53
+ format: str
54
+ file_path: str
55
+ data_path: Optional[str]
56
+ index: int
57
+
58
+
59
+ class LimaClientInterface(ABC):
60
+ """
61
+ Interface which have to be used to provide a service to LimaStream
62
+ """
63
+
64
+ @abstractmethod
65
+ def update(self, *args, **kwargs):
66
+ raise NotImplementedError
67
+
68
+ @abstractmethod
69
+ def __len__(self):
70
+ raise NotImplementedError
71
+
72
+ @abstractmethod
73
+ def __getitem__(self, frame_id):
74
+ raise NotImplementedError
75
+
76
+
77
+ def lima_client_factory(
78
+ data_store, channel_info: dict, ref_mode: bool = False
79
+ ) -> LimaClientInterface:
80
+ """Factory to instanciate the appropriate lima client (Lima1, Lima2) in
81
+ either normal mode (client returns images) or ref mode (client returns
82
+ references to files)."""
83
+ format = channel_info.get("format")
84
+ if format == "lima_v1":
85
+ if ref_mode:
86
+ return LimaRefClient(data_store, **channel_info["lima_info"])
87
+ else:
88
+ return LimaClient(data_store, **channel_info["lima_info"])
89
+ elif format == "lima_v2":
90
+ raise NotImplementedError("No client for Lima2 yet")
91
+ else:
92
+ raise ValueError(f"Not a lima stream, format: {format}")
93
+
94
+
95
+ class LimaClient(LimaClientInterface):
96
+ PROTOCOL_VERSION = 1
97
+
98
+ def __init__(self, data_store, **lima_info):
99
+ protocol = lima_info["protocol_version"]
100
+ if self.PROTOCOL_VERSION != protocol:
101
+ raise Exception(
102
+ f"{type(self).__name__} supports lima json protocol {self.PROTOCOL_VERSION}, found version {protocol}"
103
+ )
104
+
105
+ self._data_store = data_store
106
+ self._proxy = None
107
+ self._server_url = lima_info["server_url"]
108
+ self._buffer_max_number = lima_info["buffer_max_number"]
109
+ self._frame_per_acquisition = lima_info["frame_per_acquisition"]
110
+ self._acquisition_offset = lima_info["acquisition_offset"]
111
+ self._acq_trigger_mode: str | None = None
112
+
113
+ self._saved = "file_path" in lima_info
114
+ if self._saved:
115
+ self._file_offset = lima_info["file_offset"]
116
+ self._frame_per_file = lima_info["frame_per_file"]
117
+ self._file_path = lima_info["file_path"]
118
+ self._data_path = lima_info["data_path"]
119
+ self._file_format = lima_info["file_format"]
120
+ self._file_per_acquisition = math.ceil(
121
+ self._frame_per_acquisition / self._frame_per_file
122
+ )
123
+ else:
124
+ self._file_format = None
125
+ self._frame_per_file = 0
126
+
127
+ self._last_index = -1
128
+ self._last_index_saved = -1
129
+ self._last_index_saved_and_closed = -1
130
+ self._last_acq_seen_on_server = -1
131
+
132
+ @property
133
+ def acq_trigger_mode(self) -> str:
134
+ """Returns the cached acq trigger mode of this detector"""
135
+ if self._acq_trigger_mode is not None:
136
+ return self._acq_trigger_mode
137
+ acq_trigger_mode = self.proxy.acq_trigger_mode
138
+ self._acq_trigger_mode = acq_trigger_mode
139
+ return acq_trigger_mode
140
+
141
+ @property
142
+ def file_format(self):
143
+ return self._file_format
144
+
145
+ def __len__(self):
146
+ return self._last_index + 1
147
+
148
+ def update(self, last_index, last_index_saved):
149
+ self._last_index = last_index
150
+ self._last_index_saved = last_index_saved
151
+ if self._saved:
152
+ self._last_index_saved_and_closed = self._last_readable_index(
153
+ last_index_saved
154
+ )
155
+
156
+ def _decompose_frame_id(self, frame_id):
157
+ try:
158
+ acq_in_scan, frame_in_acq = divmod(frame_id, self._frame_per_acquisition)
159
+ except ZeroDivisionError:
160
+ acq_in_scan, frame_in_acq = 0, frame_id
161
+ try:
162
+ file_in_acq, frame_in_file = divmod(frame_in_acq, self._frame_per_file)
163
+ except ZeroDivisionError:
164
+ file_in_acq, frame_in_file = 0, frame_in_acq
165
+ return acq_in_scan, file_in_acq, frame_in_file
166
+
167
+ def _last_readable_index(self, frame_id):
168
+ """Return the last index that can be read from a file (i.e. file is complete)"""
169
+ acq_in_scan, file_in_acq, _ = self._decompose_frame_id(frame_id)
170
+ return (
171
+ acq_in_scan * self._file_per_acquisition
172
+ + file_in_acq * self._frame_per_file
173
+ - 1
174
+ )
175
+
176
+ def _is_acquisition_still_on_server(self, acq_id):
177
+ if acq_id < self._last_acq_seen_on_server:
178
+ # We already know this acquisition is outdated
179
+ return False
180
+ self._last_acq_seen_on_server = acquisition_on_server(
181
+ self._data_store, self._server_url
182
+ )
183
+ return acq_id == self._last_acq_seen_on_server
184
+
185
+ @property
186
+ def proxy(self):
187
+ """Lazy connection to the tango device, because a lima client may never use it."""
188
+ if self._proxy is None:
189
+ if DeviceProxy is None:
190
+ raise RuntimeError("requires 'pytango' to be installed")
191
+ self._proxy = DeviceProxy(self._server_url)
192
+ self._proxy.set_source(DevSource.DEV)
193
+ return self._proxy
194
+
195
+ def get_last_live_image(self) -> image_utils.ImageData:
196
+ """Returns the last frame from the memory buffer of Lima.
197
+
198
+ This code does not check if the frame is part of the actual scan.
199
+
200
+ Raises:
201
+ NoImageAvailable: when the lima server buffer does not yet contain any frame
202
+ ImageFormatNotSupported: when the retrieved data is not supported
203
+ """
204
+ data = image_utils.image_from_server(self.proxy, -1)
205
+
206
+ def normalize_frame_id(frame_id: int | None) -> int | None:
207
+ if frame_id == 0:
208
+ trigger_mode = self.acq_trigger_mode
209
+ if trigger_mode == "INTERNAL_TRIGGER":
210
+ # Because of the bliss/lima architecture we can't really know the index
211
+ if self._last_index == 0:
212
+ # For a ct the index is valid
213
+ return 0
214
+ return None
215
+ return frame_id
216
+
217
+ frame_id = normalize_frame_id(data.frame_id)
218
+ return image_utils.ImageData(data.array, frame_id)
219
+
220
+ def __getitem__(self, key):
221
+ if isinstance(key, int):
222
+ return self._get_index(key)
223
+ elif isinstance(key, slice):
224
+ return self._get_slice(key)
225
+ else:
226
+ raise TypeError(
227
+ f"{type(self).__name__} indices must be integers or slices, not {type(key).__name__}"
228
+ )
229
+
230
+ def _get_index(self, frame_id):
231
+ if frame_id == -1:
232
+ return self.get_last_live_image().array
233
+ if frame_id < -1:
234
+ raise NotImplementedError("Negative index not supported")
235
+
236
+ if self._saved and frame_id <= self._last_index_saved_and_closed:
237
+ # immediately accessible from file
238
+ return self._get_from_file(frame_id)
239
+ elif frame_id <= self._last_index:
240
+ # try to access on lima server
241
+ try:
242
+ return self._get_from_server(frame_id)
243
+ except IndexNoMoreThereError:
244
+ # wait for image to become available from file
245
+ # TODO wait_for_file(timeout)
246
+ if self._saved:
247
+ return self._get_from_file(frame_id)
248
+ else:
249
+ raise
250
+ else:
251
+ raise IndexNotYetThereError
252
+
253
+ def _get_slice(self, key):
254
+ key_range = range(*key.indices(len(self)))
255
+ return np.array([self._get_index(i) for i in key_range])
256
+
257
+ def _file_ref(self, frame_id) -> ImageReference:
258
+ """Convert a frame_id to an image reference to access from file.
259
+ IMPORTANT: It doesn't mean such frame_id is actually available."""
260
+ acq_in_scan, file_in_acq, frame_in_file = self._decompose_frame_id(frame_id)
261
+ file_in_scan = acq_in_scan * self._file_per_acquisition + file_in_acq
262
+ file_number = file_in_scan + self._file_offset
263
+ file_path = self._file_path % (file_number)
264
+ return ImageReference(
265
+ self._file_format, file_path, self._data_path, frame_in_file
266
+ )
267
+
268
+ def _get_from_server(self, frame_id):
269
+ if self._buffer_max_number <= (self._last_index - frame_id):
270
+ raise IndexNoMoreThereError() # TODO IndexError here and more details in LimaStream ???
271
+
272
+ acq_in_scan, file_in_acq, frame_in_file = self._decompose_frame_id(frame_id)
273
+ frame_in_acq = file_in_acq * self._frame_per_file + frame_in_file
274
+
275
+ # We need to check the server acquisition is the same before AND after reading the image.
276
+ # This is the only way to ensure the image belong to an acquisition. This is due to the
277
+ # lack of atomic operation to get an image and the current acquisition on server.
278
+ if not self._is_acquisition_still_on_server(
279
+ acq_in_scan + self._acquisition_offset
280
+ ):
281
+ raise IndexNoMoreThereError() # TODO IndexError here and more details in LimaStream ???
282
+
283
+ image, index = image_utils.image_from_server(self.proxy, frame_in_acq)
284
+ assert frame_in_acq == index
285
+
286
+ if not self._is_acquisition_still_on_server(
287
+ acq_in_scan + self._acquisition_offset
288
+ ):
289
+ raise IndexNoMoreThereError() # TODO IndexError here and more details in LimaStream ???
290
+
291
+ return image
292
+
293
+ def _get_from_file(self, frame_id):
294
+ ref = self._file_ref(frame_id)
295
+ return image_utils.image_from_file(
296
+ ref.file_path, ref.data_path, ref.index, ref.format
297
+ )
298
+
299
+
300
+ class LimaRefClient(LimaClient):
301
+ def __getitem__(self, key):
302
+ saved_len = self._last_index_saved + 1
303
+ if isinstance(key, int):
304
+ if key < 0:
305
+ key += saved_len
306
+ elif key >= saved_len:
307
+ raise IndexNotYetThereError
308
+ return self._file_ref(key)
309
+ elif isinstance(key, slice):
310
+ key_range = range(*key.indices(saved_len))
311
+ return [self._file_ref(i) for i in key_range]
312
+ else:
313
+ raise TypeError(
314
+ f"{type(self).__name__} indices must be integers or slices, not {type(key).__name__}"
315
+ )