blissdata 0.3.2__tar.gz → 0.3.4__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 (73) hide show
  1. {blissdata-0.3.2 → blissdata-0.3.4}/PKG-INFO +14 -2
  2. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/__init__.py +1 -1
  3. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/beacon/_base.py +3 -1
  4. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/common/utils.py +16 -0
  5. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/events/lima.py +25 -5
  6. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/expiration.py +1 -1
  7. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/nodes/channel.py +4 -0
  8. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/nodes/scan.py +9 -1
  9. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/remote_node.py +1 -11
  10. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/scan.py +7 -0
  11. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/h5api/utils/bliss.py +2 -5
  12. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/h5api/utils/hdf5.py +68 -15
  13. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/h5api/utils/hdf5_retry.py +1 -1
  14. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/redis/caching.py +23 -28
  15. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/redis/connection.py +1 -0
  16. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/redis/proxy.py +2 -0
  17. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/settings.py +16 -2
  18. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/streaming.py +2 -0
  19. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/h5api/scanner.py +2 -2
  20. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata.egg-info/PKG-INFO +14 -2
  21. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata.egg-info/requires.txt +1 -1
  22. {blissdata-0.3.2 → blissdata-0.3.4}/pyproject.toml +1 -4
  23. {blissdata-0.3.2 → blissdata-0.3.4}/setup.cfg +1 -1
  24. {blissdata-0.3.2 → blissdata-0.3.4}/LICENSE +0 -0
  25. {blissdata-0.3.2 → blissdata-0.3.4}/README.md +0 -0
  26. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/beacon/__init__.py +0 -0
  27. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/beacon/config.py +0 -0
  28. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/beacon/data.py +0 -0
  29. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/beacon/files.py +0 -0
  30. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/client.py +0 -0
  31. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/common/__init__.py +0 -0
  32. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/__init__.py +0 -0
  33. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/events/__init__.py +0 -0
  34. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/events/channel.py +0 -0
  35. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/events/node.py +0 -0
  36. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/events/scan.py +0 -0
  37. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/events/walk.py +0 -0
  38. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/lima_image.py +0 -0
  39. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/node.py +0 -0
  40. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/nodes/__init__.py +0 -0
  41. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/nodes/dataset.py +0 -0
  42. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/nodes/dataset_collection.py +0 -0
  43. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/nodes/lima.py +0 -0
  44. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/nodes/node_ref_channel.py +0 -0
  45. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/nodes/proposal.py +0 -0
  46. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/data/nodes/scan_group.py +0 -0
  47. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/h5api/__init__.py +0 -0
  48. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/h5api/abstract.py +0 -0
  49. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/h5api/dynamic_hdf5.py +0 -0
  50. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/h5api/file_arguments.py +0 -0
  51. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/h5api/static_hdf5.py +0 -0
  52. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/h5api/utils/__init__.py +0 -0
  53. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/h5api/utils/lima.py +0 -0
  54. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/h5api/utils/types.py +0 -0
  55. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/redis/__init__.py +0 -0
  56. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/redis/manager.py +0 -0
  57. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/redis/scripting.py +0 -0
  58. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/streaming_events.py +0 -0
  59. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/__init__.py +0 -0
  60. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/beacon/__init__.py +0 -0
  61. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/beacon/test_config.py +0 -0
  62. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/beacon/test_data.py +0 -0
  63. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/beacon/test_files.py +0 -0
  64. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/conftest.py +0 -0
  65. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/h5api/__init__.py +0 -0
  66. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/h5api/test_dynamic_files.py +0 -0
  67. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/h5api/test_static_files.py +0 -0
  68. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/redis/__init__.py +0 -0
  69. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata/tests/redis/manager.py +0 -0
  70. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata.egg-info/SOURCES.txt +0 -0
  71. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata.egg-info/dependency_links.txt +0 -0
  72. {blissdata-0.3.2 → blissdata-0.3.4}/blissdata.egg-info/top_level.txt +0 -0
  73. {blissdata-0.3.2 → blissdata-0.3.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: blissdata
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Bliss data streaming client
5
5
  Home-page: https://gitlab.esrf.fr/bliss/bliss/-/tree/master/blissdata
6
6
  Author: BCU (ESRF)
@@ -10,11 +10,23 @@ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGP
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Requires-Python: <3.10,>=3.7
12
12
  Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: gevent>=20.9.0
15
+ Requires-Dist: h5py>=2.9
16
+ Requires-Dist: numpy>=1.13
17
+ Requires-Dist: silx>=1.1.0
13
18
  Provides-Extra: tango
19
+ Requires-Dist: pytango>=9.3.1; extra == "tango"
14
20
  Provides-Extra: test
21
+ Requires-Dist: pytest>=7; extra == "test"
22
+ Requires-Dist: pytest-mock; extra == "test"
15
23
  Provides-Extra: dev
24
+ Requires-Dist: pytest>=7; extra == "dev"
25
+ Requires-Dist: pytest-mock; extra == "dev"
26
+ Requires-Dist: black==22.3; extra == "dev"
27
+ Requires-Dist: flake8>=4; extra == "dev"
16
28
  Provides-Extra: beacon
17
- License-File: LICENSE
29
+ Requires-Dist: ruamel.yaml; extra == "beacon"
18
30
 
19
31
  # blissdata
20
32
 
@@ -14,4 +14,4 @@
14
14
  data
15
15
  """
16
16
 
17
- __version__ = "0.3.2"
17
+ __version__ = "0.3.4"
@@ -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
@@ -6,6 +6,7 @@
6
6
  # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
7
 
8
8
  import fnmatch
9
+ import re
9
10
  from collections.abc import Mapping
10
11
 
11
12
 
@@ -141,3 +142,18 @@ def get_matching_names(patterns, names, strict_pattern_as_short_name=False):
141
142
  matches[pat] = matching_names
142
143
 
143
144
  return matches
145
+
146
+
147
+ def natural_sort(words):
148
+ def convert(text):
149
+ return int(text) if text.isdigit() else text.lower()
150
+
151
+ def alphanum_key(key):
152
+ return [
153
+ convert(c)
154
+ for c in re.split(
155
+ "([0-9]+)", key.decode() if isinstance(key, bytes) else key
156
+ )
157
+ ]
158
+
159
+ return sorted(words, key=alphanum_key)
@@ -244,9 +244,24 @@ class LimaImageStatusEvent(streaming_events.StreamEvent):
244
244
  images_per_acquisition = info["saving_frame_per_acquisition"]
245
245
 
246
246
  first_file_index_in_scan = info["saving_next_number"]
247
- subdir_format = (
248
- info["saving_prefix"] + info["saving_index_format"] + info["saving_suffix"]
249
- )
247
+ if "saving_managed_mode" not in info:
248
+ raise RuntimeError(
249
+ "Lima info does not have 'saving_managed_mode'. Upgrade Bliss to solve it."
250
+ )
251
+ if info["saving_managed_mode"] == "HARDWARE":
252
+ # Assume Dectris hardware saving
253
+ subdir_format = (
254
+ info["saving_prefix"]
255
+ + "_data_"
256
+ + info["saving_index_format"]
257
+ + info["saving_suffix"]
258
+ )
259
+ else:
260
+ subdir_format = (
261
+ info["saving_prefix"]
262
+ + info["saving_index_format"]
263
+ + info["saving_suffix"]
264
+ )
250
265
  path_format = os.path.join(info["saving_directory"], subdir_format)
251
266
  file_format = info["saving_format"]
252
267
 
@@ -260,9 +275,13 @@ class LimaImageStatusEvent(streaming_events.StreamEvent):
260
275
  )
261
276
  if file_index_in_scan is None:
262
277
  break
263
- file_path = path_format % file_index_in_scan
264
278
  if file_format.lower().startswith("hdf5"):
265
- if info["lima_version"] == "<1.9.1":
279
+ if info["saving_managed_mode"] == "HARDWARE":
280
+ # Assume Dectris hardware saving
281
+ path_in_file = "/entry/data/data"
282
+ # Dectris starts counting from 1 instead of 0 like lima does
283
+ file_index_in_scan += 1
284
+ elif info["lima_version"] == "<1.9.1":
266
285
  # 'old' lima
267
286
  path_in_file = (
268
287
  "/entry_0000/instrument/"
@@ -277,6 +296,7 @@ class LimaImageStatusEvent(streaming_events.StreamEvent):
277
296
  )
278
297
  else:
279
298
  path_in_file = ""
299
+ file_path = path_format % file_index_in_scan
280
300
  yield file_path, path_in_file, image_index_in_file, file_format
281
301
 
282
302
  @staticmethod
@@ -1,7 +1,7 @@
1
1
  from typing import Optional
2
2
  from blissdata.client import get_redis_proxy
3
3
 
4
- _DATA_EXPIRATION_TIME = 600 # 5 minutes
4
+ _DATA_EXPIRATION_TIME = 600 # 10 minutes
5
5
  _PARENT_EXPIRATION_TIME = 24 * 3600 # 1 day
6
6
 
7
7
 
@@ -15,6 +15,10 @@ from blissdata.data.events import EventData, ChannelDataEvent
15
15
  CHANNEL_MAX_LEN = 2048
16
16
 
17
17
 
18
+ class RedisDataExpiredError(Exception):
19
+ pass
20
+
21
+
18
22
  class _ChannelDataNodeBase(DataNode):
19
23
  _NODE_TYPE = NotImplemented
20
24
 
@@ -6,7 +6,7 @@
6
6
  # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
7
 
8
8
  from blissdata.data.node import DataNodeContainer
9
- from blissdata.data.nodes.channel import ChannelDataNode
9
+ from blissdata.data.nodes.channel import ChannelDataNode, RedisDataExpiredError
10
10
  from blissdata.data.nodes.lima import LimaImageChannelDataNode
11
11
  from blissdata.streaming_events import StreamEvent
12
12
  from blissdata.data.events import (
@@ -179,6 +179,14 @@ def get_data_from_nodes(pipeline, *nodes):
179
179
  for i, (channel_name, get_data_func) in enumerate(
180
180
  scan_channel_get_data_func.items()
181
181
  ):
182
+ if not result[i]:
183
+ raise RedisDataExpiredError("Redis data channel expired.")
184
+
185
+ if result[i][0][0] != b"1-0": # first event is 1-0, not 0-0
186
+ raise RedisDataExpiredError(
187
+ f"Redis data channel {channel_name} have been trimmed out."
188
+ )
189
+
182
190
  yield channel_name, get_data_func(result[i])
183
191
  for channel_name, view in scan_image_get_view.items():
184
192
  yield channel_name, view
@@ -199,16 +199,6 @@ class RemoteNodeWalker:
199
199
  node.node_type,
200
200
  node.name,
201
201
  node.db_name,
202
- RemoteNodeWalker.pack_info(node.info),
202
+ node.info.get_all(),
203
203
  redis_url,
204
204
  )
205
-
206
- @staticmethod
207
- def pack_info(info):
208
- info_dict = info.get_all()
209
- try:
210
- # ScanState objects brings Bliss dependencies with them, replace with name
211
- info_dict["state"] = info["state"].name
212
- except KeyError:
213
- pass
214
- return info_dict
@@ -9,6 +9,7 @@
9
9
  import sys
10
10
  import numpy
11
11
  import gevent
12
+ import gevent.event
12
13
  import typing
13
14
  import warnings
14
15
  import contextlib
@@ -22,6 +23,12 @@ from blissdata.data.events import EventType
22
23
  from blissdata.streaming import DataStreamReaderStopHandler
23
24
 
24
25
 
26
+ def get_data(scan):
27
+ # keep get_data() wrapper for scripts backward compatibility until the
28
+ # new data API provide the same functionality (expected for Bliss 1.12)
29
+ return scan.get_data()
30
+
31
+
25
32
  def get_counter_names(scan):
26
33
  """
27
34
  Return a list of counter names
@@ -102,7 +102,7 @@ class BlissDynamicHDF5Handler(hdf5.DynamicHDF5Handler):
102
102
  if h5item.name == hdf5.SEP:
103
103
  return False
104
104
  scan = [s for s in h5item.name.split(hdf5.SEP) if s][0]
105
- nxentry = self.file_obj.file[scan]
105
+ nxentry = self.file_obj[scan]
106
106
  except AttributeError:
107
107
  raise RetryError("file is closed")
108
108
  if "end_time" in nxentry:
@@ -119,10 +119,7 @@ class BlissDynamicHDF5Handler(hdf5.DynamicHDF5Handler):
119
119
  nxentry = self.file_obj[scan]
120
120
  except AttributeError:
121
121
  raise RetryError("file is closed")
122
- if "end_time" in nxentry:
123
- # Last dataset written to the file
124
- return True
125
- return self._get_writer_status(nxentry) in ("SUCCEEDED", "FAILED")
122
+ return "end_time" in nxentry # Last dataset written to the file
126
123
 
127
124
  def _get_writer_status(self, nxentry: h5py.Dataset) -> Optional[str]:
128
125
  nxnote = nxentry.get("writer", None)
@@ -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:
77
+ raise KeyError(name)
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:
86
+ raise AttributeError(name)
81
87
 
82
88
  @ignore_retry_timeout
83
89
  def iter_attrs(self, name: str) -> Iterator[str]:
@@ -138,7 +144,24 @@ 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:
150
+ parent_name, _, item_name = name.rpartition(SEP)
151
+ if not parent_name:
152
+ parent_name = SEP
153
+ if parent_name == name:
154
+ raise
155
+ parent_item = self._get_item(parent_name)
156
+ if not self._is_finished(parent_item):
157
+ raise # item could still be created
158
+ try:
159
+ item = parent_item[item_name] # try one more time
160
+ except KeyError as e:
161
+ if not _key_does_not_exist(e):
162
+ raise
163
+ # item will never be created
164
+ raise RetryTimeoutError(str(e)) from e
142
165
  self._native_items[name] = item
143
166
  return item
144
167
 
@@ -152,16 +175,28 @@ class DynamicHDF5Handler:
152
175
 
153
176
  def _get_attr(self, name: str, key: str) -> Any:
154
177
  item = self._get_item(name)
155
- return item.attrs[key]
178
+ try:
179
+ return item.attrs[key]
180
+ except KeyError:
181
+ if not self._is_finished(item):
182
+ raise # attribute could still be created
183
+ try:
184
+ return item.attrs[key] # try one more time
185
+ except KeyError as e:
186
+ if not _key_does_not_exist(e):
187
+ raise
188
+ # attribute will never be created
189
+ raise RetryTimeoutError(str(e)) from e
156
190
 
157
191
  def _iter_attrs(self, name: str, start_index: int = 0) -> Iterator[str]:
158
192
  item = self._get_item(name)
193
+ is_complete = self._is_initialized(item)
159
194
  if start_index == 0:
160
195
  yield from item.attrs
161
196
  else:
162
197
  yield from list(item.attrs)[start_index:]
163
- if not self._is_initialized(item):
164
- raise RetryError("not fully initialized")
198
+ if not is_complete:
199
+ raise RetryError("attributes could still be added")
165
200
 
166
201
  def _len_attrs(self, name: str) -> int:
167
202
  item = self._get_item(name)
@@ -173,19 +208,19 @@ class DynamicHDF5Handler:
173
208
  item = self._get_item(name)
174
209
  self._check_dataset_before_read(item)
175
210
  if self.is_group(item):
176
- is_initialized = self._is_initialized(item)
211
+ is_complete = self._is_initialized(item)
177
212
  for key in self._iter_group(item, start_index):
178
- is_initialized = False
213
+ is_complete = False
179
214
  yield key
180
- if not is_initialized:
181
- raise RetryError("not initialized")
215
+ if not is_complete:
216
+ raise RetryError("children could still be added to the group")
182
217
  else:
183
- is_finished = self._is_finished(item)
218
+ is_complete = self._is_finished(item)
184
219
  for data in self._iter_dataset(item, start_index):
185
- is_finished = False
220
+ is_complete = False
186
221
  yield data
187
- if not is_finished:
188
- raise RetryError("not finished")
222
+ if not is_complete:
223
+ raise RetryError("dataset could still grow")
189
224
 
190
225
  @staticmethod
191
226
  def is_group(h5item: HDF5Item):
@@ -225,3 +260,21 @@ class DynamicHDF5Handler:
225
260
 
226
261
  def _is_finished(self, h5item: HDF5Item) -> bool:
227
262
  return True
263
+
264
+
265
+ def _key_does_not_exist(exc: KeyError) -> bool:
266
+ """
267
+ A real KeyError (meaning that the key does not exist) gives the following error message:
268
+
269
+ .. code
270
+
271
+ KeyError: "Unable to synchronously open object (object 'end_time' doesn't exist)"
272
+
273
+ An example of a KeyError caused by another reason
274
+
275
+ .. code
276
+
277
+ KeyError: 'Unable to synchronously open object (addr overflow, addr = 64392369, size = 96, eoa = 64365353)'
278
+
279
+ """
280
+ return "doesn't exist" in str(exc)
@@ -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
 
@@ -1,9 +1,10 @@
1
1
  """Client-side Redis cache with an invalidation connection.
2
2
  """
3
3
 
4
- import os
4
+ import socket
5
5
  import gevent
6
6
  import gevent.event
7
+ import gevent.select
7
8
  from functools import wraps
8
9
  from contextlib import ExitStack, contextmanager
9
10
  from collections.abc import MutableMapping
@@ -38,7 +39,7 @@ class CachingConnectionGreenlet(gevent.Greenlet):
38
39
  self._started_event = started_event
39
40
  self._tracking_redirect_id = None
40
41
  self.connection = None
41
- self._close_pipe = None
42
+ self._rp, self._wp = None, None
42
43
  super().__init__()
43
44
 
44
45
  def stop(self):
@@ -47,11 +48,11 @@ class CachingConnectionGreenlet(gevent.Greenlet):
47
48
 
48
49
  Calling `kill` might not return the pubsub connection to the pool.
49
50
  """
50
- self._send_fd(self._close_pipe, b"X")
51
+ self._send_fd(self._wp, b"X")
51
52
 
52
53
  def _run(self):
53
54
  with ExitStack() as stack:
54
- ctx = self._setup_close_pipe()
55
+ ctx = self._setup_wp()
55
56
  rp, stop_callback = stack.enter_context(ctx)
56
57
 
57
58
  ctx = self._setup_invalidation_connection(stop_callback)
@@ -64,44 +65,38 @@ class CachingConnectionGreenlet(gevent.Greenlet):
64
65
  self._started_event.set()
65
66
  self._invalidation_loop(pubsub, rp)
66
67
 
67
- @contextmanager
68
- def _setup_close_pipe(self):
69
- rp, wp = os.pipe()
70
-
71
- def stop_callback(*args, **kw):
68
+ @staticmethod
69
+ def _close_fd(fd):
70
+ if fd is not None:
72
71
  try:
73
- os.close(wp)
72
+ fd.close()
74
73
  except OSError:
75
74
  # already closed
76
75
  pass
77
76
 
78
- try:
79
- self._close_pipe = wp
80
- yield rp, stop_callback
81
- finally:
82
- self._close_pipe = None
83
- self._close_fd(wp)
84
- self._close_fd(rp)
85
-
86
77
  @staticmethod
87
- def _close_fd(fd):
78
+ def _send_fd(fd, msg):
88
79
  if fd is None:
89
80
  return
90
81
  try:
91
- os.close(fd)
82
+ fd.send(msg)
92
83
  except OSError:
93
84
  # already closed
94
85
  pass
95
86
 
96
- @staticmethod
97
- def _send_fd(fd, msg):
98
- if fd is None:
99
- return
87
+ def _close_wp(self):
88
+ return self._close_fd(self._wp)
89
+
90
+ @contextmanager
91
+ def _setup_wp(self):
92
+ self._rp, self._wp = socket.socketpair()
93
+
100
94
  try:
101
- os.write(fd, msg)
102
- except OSError:
103
- # already closed
104
- pass
95
+ yield self._rp, self._close_wp
96
+ finally:
97
+ self._close_fd(self._rp)
98
+ self._close_fd(self._wp)
99
+ self._rp, self._wp = None, None
105
100
 
106
101
  @contextmanager
107
102
  def _setup_invalidation_connection(self, stop_callback):
@@ -27,6 +27,7 @@ These proxies are also greenlet-safe. You will have one cache per pool.
27
27
 
28
28
  import os
29
29
  import gevent
30
+ import gevent.lock
30
31
  import socket
31
32
  import redis
32
33
 
@@ -11,6 +11,8 @@ from collections.abc import MutableMapping
11
11
  from collections import Counter
12
12
  from contextlib import contextmanager
13
13
  import gevent
14
+ import gevent.event
15
+ import gevent.lock
14
16
  import redis
15
17
  import redis.client
16
18
 
@@ -13,7 +13,7 @@ import logging
13
13
  import numpy
14
14
  from blissdata.client import get_redis_proxy
15
15
  from blissdata.redis.scripting import register_script, evaluate_script
16
- from blissdata.common.utils import Undefined
16
+ from blissdata.common.utils import Undefined, natural_sort
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
@@ -712,7 +712,7 @@ local set_size = redis.call("ZCARD", setkey)
712
712
  if set_max_score > set_size then
713
713
  -- we need to reassign scores as some was deleted
714
714
  local new_order = 1
715
- table = redis.call("ZRANGE", setkey, 0, -1)
715
+ local table = redis.call("ZRANGE", setkey, 0, -1)
716
716
  for k, attr in pairs(table)
717
717
  do
718
718
  --redis.call("ZADD", setkey, new_order, attr)
@@ -760,6 +760,20 @@ class OrderedHashSetting(BaseHashSetting):
760
760
  orderedhashsetting_helper_script,
761
761
  )
762
762
 
763
+ # check if this ordered hash is built from an existing hash setting
764
+ if self.connection.exists(self.name) and not self.connection.exists(
765
+ self._name_order
766
+ ):
767
+ if self.connection.type(self.name) == b"hash": # noqa E721
768
+ # we can provide a default order (alphabetical), to ensure an ordered hash
769
+ # can be built on top of an existing hash ; this is to preserve
770
+ # settings coherency in case of type "upgrade" (like when Lima ROIs
771
+ # have been ordered from "normal" hashes)
772
+ sorted_keys = natural_sort(self.connection.hkeys(self.name))
773
+ self.connection.zadd(
774
+ self._name_order, {v: k for k, v in enumerate(sorted_keys)}
775
+ )
776
+
763
777
  @property
764
778
  def _name_order(self):
765
779
  return self._name + ":creation_order"
@@ -7,6 +7,8 @@
7
7
 
8
8
  import sys
9
9
  import gevent
10
+ import gevent.event
11
+ import gevent.queue
10
12
  import uuid
11
13
  import enum
12
14
  import fnmatch
@@ -268,7 +268,7 @@ def writer_context(filename: str, scan_number: int) -> Iterator[h5py.Group]:
268
268
 
269
269
  try:
270
270
  entry.attrs["NX_class"] = "NXentry"
271
- entry.attrs["start_time"] = timestamp()
271
+ entry["start_time"] = timestamp()
272
272
  writer = entry.create_group("writer")
273
273
  writer.attrs["NX_class"] = "NXnote"
274
274
  writer["status"] = "STARTING"
@@ -279,7 +279,7 @@ def writer_context(filename: str, scan_number: int) -> Iterator[h5py.Group]:
279
279
  else:
280
280
  writer["status"][()] = "SUCCEEDED"
281
281
  finally:
282
- entry.attrs["end_time"] = timestamp()
282
+ entry["end_time"] = timestamp()
283
283
 
284
284
 
285
285
  @contextmanager
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: blissdata
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Bliss data streaming client
5
5
  Home-page: https://gitlab.esrf.fr/bliss/bliss/-/tree/master/blissdata
6
6
  Author: BCU (ESRF)
@@ -10,11 +10,23 @@ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGP
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Requires-Python: <3.10,>=3.7
12
12
  Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: gevent>=20.9.0
15
+ Requires-Dist: h5py>=2.9
16
+ Requires-Dist: numpy>=1.13
17
+ Requires-Dist: silx>=1.1.0
13
18
  Provides-Extra: tango
19
+ Requires-Dist: pytango>=9.3.1; extra == "tango"
14
20
  Provides-Extra: test
21
+ Requires-Dist: pytest>=7; extra == "test"
22
+ Requires-Dist: pytest-mock; extra == "test"
15
23
  Provides-Extra: dev
24
+ Requires-Dist: pytest>=7; extra == "dev"
25
+ Requires-Dist: pytest-mock; extra == "dev"
26
+ Requires-Dist: black==22.3; extra == "dev"
27
+ Requires-Dist: flake8>=4; extra == "dev"
16
28
  Provides-Extra: beacon
17
- License-File: LICENSE
29
+ Requires-Dist: ruamel.yaml; extra == "beacon"
18
30
 
19
31
  # blissdata
20
32
 
@@ -9,7 +9,7 @@ ruamel.yaml
9
9
  [dev]
10
10
  pytest>=7
11
11
  pytest-mock
12
- black>=22
12
+ black==22.3
13
13
  flake8>=4
14
14
 
15
15
  [tango]
@@ -1,6 +1,3 @@
1
1
  [build-system]
2
2
  build-backend = "setuptools.build_meta"
3
- requires = [
4
- "setuptools>=46.4<63",
5
- "wheel",
6
- ]
3
+ requires = ["setuptools>=46.4", "wheel"]
@@ -29,7 +29,7 @@ test =
29
29
  pytest-mock
30
30
  dev =
31
31
  %(test)s
32
- black >=22
32
+ black ==22.3
33
33
  flake8 >=4
34
34
  beacon =
35
35
  ruamel.yaml
File without changes
File without changes
File without changes
File without changes