blissdata 0.3.4__py3-none-any.whl

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 (67) hide show
  1. blissdata/__init__.py +17 -0
  2. blissdata/beacon/__init__.py +1 -0
  3. blissdata/beacon/_base.py +106 -0
  4. blissdata/beacon/config.py +24 -0
  5. blissdata/beacon/data.py +45 -0
  6. blissdata/beacon/files.py +141 -0
  7. blissdata/client.py +62 -0
  8. blissdata/common/__init__.py +15 -0
  9. blissdata/common/utils.py +159 -0
  10. blissdata/data/__init__.py +15 -0
  11. blissdata/data/events/__init__.py +22 -0
  12. blissdata/data/events/channel.py +149 -0
  13. blissdata/data/events/lima.py +478 -0
  14. blissdata/data/events/node.py +28 -0
  15. blissdata/data/events/scan.py +49 -0
  16. blissdata/data/events/walk.py +42 -0
  17. blissdata/data/expiration.py +42 -0
  18. blissdata/data/lima_image.py +465 -0
  19. blissdata/data/node.py +1619 -0
  20. blissdata/data/nodes/__init__.py +11 -0
  21. blissdata/data/nodes/channel.py +394 -0
  22. blissdata/data/nodes/dataset.py +82 -0
  23. blissdata/data/nodes/dataset_collection.py +12 -0
  24. blissdata/data/nodes/lima.py +422 -0
  25. blissdata/data/nodes/node_ref_channel.py +34 -0
  26. blissdata/data/nodes/proposal.py +12 -0
  27. blissdata/data/nodes/scan.py +192 -0
  28. blissdata/data/nodes/scan_group.py +12 -0
  29. blissdata/data/remote_node.py +204 -0
  30. blissdata/data/scan.py +666 -0
  31. blissdata/h5api/__init__.py +1 -0
  32. blissdata/h5api/abstract.py +97 -0
  33. blissdata/h5api/dynamic_hdf5.py +153 -0
  34. blissdata/h5api/file_arguments.py +28 -0
  35. blissdata/h5api/static_hdf5.py +139 -0
  36. blissdata/h5api/utils/__init__.py +0 -0
  37. blissdata/h5api/utils/bliss.py +138 -0
  38. blissdata/h5api/utils/hdf5.py +280 -0
  39. blissdata/h5api/utils/hdf5_retry.py +98 -0
  40. blissdata/h5api/utils/lima.py +286 -0
  41. blissdata/h5api/utils/types.py +13 -0
  42. blissdata/redis/__init__.py +12 -0
  43. blissdata/redis/caching.py +390 -0
  44. blissdata/redis/connection.py +169 -0
  45. blissdata/redis/manager.py +164 -0
  46. blissdata/redis/proxy.py +971 -0
  47. blissdata/redis/scripting.py +24 -0
  48. blissdata/settings.py +1174 -0
  49. blissdata/streaming.py +819 -0
  50. blissdata/streaming_events.py +355 -0
  51. blissdata/tests/__init__.py +0 -0
  52. blissdata/tests/beacon/__init__.py +0 -0
  53. blissdata/tests/beacon/test_config.py +26 -0
  54. blissdata/tests/beacon/test_data.py +50 -0
  55. blissdata/tests/beacon/test_files.py +80 -0
  56. blissdata/tests/conftest.py +0 -0
  57. blissdata/tests/h5api/__init__.py +0 -0
  58. blissdata/tests/h5api/scanner.py +390 -0
  59. blissdata/tests/h5api/test_dynamic_files.py +309 -0
  60. blissdata/tests/h5api/test_static_files.py +169 -0
  61. blissdata/tests/redis/__init__.py +0 -0
  62. blissdata/tests/redis/manager.py +23 -0
  63. blissdata-0.3.4.dist-info/LICENSE +165 -0
  64. blissdata-0.3.4.dist-info/METADATA +79 -0
  65. blissdata-0.3.4.dist-info/RECORD +67 -0
  66. blissdata-0.3.4.dist-info/WHEEL +5 -0
  67. blissdata-0.3.4.dist-info/top_level.txt +1 -0
blissdata/__init__.py ADDED
@@ -0,0 +1,17 @@
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
+ """Blissdata package
9
+
10
+ .. autosummary::
11
+ :toctree:
12
+
13
+ common
14
+ data
15
+ """
16
+
17
+ __version__ = "0.3.4"
@@ -0,0 +1 @@
1
+ """Beacon communication"""
@@ -0,0 +1,106 @@
1
+ """Base client to communicate with Beacon."""
2
+
3
+ import platform
4
+ import socket
5
+ import struct
6
+ import threading
7
+ from typing import Optional
8
+ from . import config
9
+
10
+
11
+ class IncompleteBeaconMessage(Exception):
12
+ """Raised when a received message is incomplete"""
13
+
14
+
15
+ class BeaconClient:
16
+ """Synchronous blocking Beacon client.
17
+
18
+ It takes a host and port to a beacon server to be instantiated or
19
+ uses the BEACON_HOST environment variable when when missing.
20
+ """
21
+
22
+ HEADER_SIZE = struct.calcsize("<ii")
23
+
24
+ def __init__(
25
+ self, host: Optional[str] = None, port: Optional[int] = None, timeout=3.0
26
+ ):
27
+ if host is None or port is None:
28
+ self._address = config.get_beacon_address()
29
+ else:
30
+ self._address = host, port
31
+ connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
32
+ if platform.system() != "Windows":
33
+ connection.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10)
34
+ connection.connect(self._address)
35
+ connection.settimeout(timeout)
36
+ self._connection = connection
37
+ self._cursor_id = 0
38
+ self._lock = threading.Lock()
39
+
40
+ def __repr__(self) -> str:
41
+ return (
42
+ f"{type(self).__name__}(host='{self._address[0]}', port={self._address[1]})"
43
+ )
44
+
45
+ def close(self):
46
+ """Close the connection to Beacon."""
47
+ self._connection.close()
48
+ self._connection = None
49
+
50
+ def _request(self, message_id, param1):
51
+ """Send a request and returns a response object"""
52
+ message_key = self._gen_message_key()
53
+ content = f"{message_key}|{param1}".encode()
54
+ header = struct.pack("<ii", message_id, len(content))
55
+ msg = b"%s%s" % (header, content)
56
+ self._connection.sendall(msg)
57
+ client = self
58
+
59
+ class Response:
60
+ def read(self):
61
+ return client._read(message_key)
62
+
63
+ return Response()
64
+
65
+ def _gen_message_key(self):
66
+ """Generate a unique message key.
67
+
68
+ This is not really needed for a synchronous service.
69
+ It could be a fixed value.
70
+ """
71
+ self._cursor_id = (self._cursor_id + 1) % 100000
72
+ return "%s" % self._cursor_id
73
+
74
+ def _unpack_message(self, s):
75
+ header_size = self.HEADER_SIZE
76
+ if len(s) < header_size:
77
+ raise IncompleteBeaconMessage
78
+ message_type, message_len = struct.unpack("<ii", s[:header_size])
79
+ if len(s) < header_size + message_len:
80
+ raise IncompleteBeaconMessage
81
+ message = s[header_size : header_size + message_len]
82
+ remaining = s[header_size + message_len :]
83
+ return message_type, message, remaining
84
+
85
+ def _read(self, expected_message_key):
86
+ data = b""
87
+ while True:
88
+ raw_data = self._connection.recv(16 * 1024)
89
+ if not raw_data:
90
+ break
91
+ data = b"%s%s" % (data, raw_data)
92
+ try:
93
+ message_type, message, data = self._unpack_message(data)
94
+ except IncompleteBeaconMessage:
95
+ continue
96
+ break
97
+ message_key, data = self._get_msg_key(message)
98
+ if message_key != expected_message_key:
99
+ raise RuntimeError(f"Unexpected message key '{message_key}'")
100
+ return message_type, data
101
+
102
+ def _get_msg_key(self, message):
103
+ pos = message.find(b"|")
104
+ if pos < 0:
105
+ return message.decode(), None
106
+ return message[:pos].decode(), message[pos + 1 :]
@@ -0,0 +1,24 @@
1
+ """Beacon configuration"""
2
+
3
+ import os
4
+ from typing import Tuple
5
+
6
+
7
+ def get_beacon_address() -> Tuple[str, int]:
8
+ """Beacon address from the environment var `BEACON_HOST`.
9
+
10
+ For example `('foobar', 25000)`.
11
+
12
+ Raises:
13
+ ValueError: If BEACON_HOST is missing or not properly set
14
+ """
15
+ beacon_host = os.environ.get("BEACON_HOST")
16
+ if beacon_host is None:
17
+ raise ValueError("BEACON_HOST is not specified")
18
+ try:
19
+ host, port = beacon_host.split(":")
20
+ return host, int(port)
21
+ except Exception:
22
+ raise ValueError(
23
+ f"BEACON_HOST variable not properly set. Expected: 'hostname:port'. Found: from '{beacon_host}'."
24
+ )
@@ -0,0 +1,45 @@
1
+ """Get Bliss data information from Beacon."""
2
+
3
+ import struct
4
+ from ._base import BeaconClient
5
+ from ._base import IncompleteBeaconMessage
6
+
7
+
8
+ class BeaconData(BeaconClient):
9
+ """Provides the API to read the redis databases urls."""
10
+
11
+ REDIS_QUERY = 30
12
+ REDIS_QUERY_ANSWER = 31
13
+
14
+ REDIS_DATA_SERVER_QUERY = 32
15
+ REDIS_DATA_SERVER_FAILED = 33
16
+ REDIS_DATA_SERVER_OK = 34
17
+
18
+ def get_redis_db(self) -> str:
19
+ """Returns the URL of the Redis database that contains the Bliss settings. For example 'foobar:25001'."""
20
+ msg = b"%s%s" % (struct.pack("<ii", self.REDIS_QUERY, 0), b"")
21
+ self._connection.sendall(msg)
22
+ data = b""
23
+ while True:
24
+ raw_data = self._connection.recv(16 * 1024)
25
+ if not raw_data:
26
+ break
27
+ data = b"%s%s" % (data, raw_data)
28
+ try:
29
+ message_type, message, data = self._unpack_message(data)
30
+ except IncompleteBeaconMessage:
31
+ continue
32
+ break
33
+ if message_type != self.REDIS_QUERY_ANSWER:
34
+ raise RuntimeError(f"Unexpected message type '{message_type}'")
35
+ return message.decode()
36
+
37
+ 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'."""
39
+ response = self._request(self.REDIS_DATA_SERVER_QUERY, "")
40
+ response_type, data = response.read()
41
+ if response_type == self.REDIS_DATA_SERVER_OK:
42
+ return data.decode().replace("|", ":", 1)
43
+ elif response_type == self.REDIS_DATA_SERVER_FAILED:
44
+ raise RuntimeError(data.decode())
45
+ raise RuntimeError(f"Unexpected Beacon response type {response_type}")
@@ -0,0 +1,141 @@
1
+ """Get files from Beacon."""
2
+
3
+ import sys
4
+ import json
5
+ from typing import Any
6
+ from urllib.parse import urlparse, ParseResult
7
+ from ._base import BeaconClient
8
+
9
+ try:
10
+ import ruamel.yaml
11
+
12
+ yaml_load = ruamel.yaml.YAML().load
13
+ except ImportError:
14
+ try:
15
+ from yaml import safe_load as yaml_load
16
+ except ImportError:
17
+ yaml_load = None
18
+
19
+
20
+ def read_config(url: str) -> Any:
21
+ """
22
+ Read configuration from a url.
23
+
24
+ In case of a Beacon url with missing host and port, the Beacon
25
+ server will be found from environment variable `BEACON_HOST`.
26
+
27
+ Arguments:
28
+ url: This can be a local yaml file (for example `/path/to/file.yaml`, `file:///path/to/file.yaml`)
29
+ or a Beacon URL (for example `beacon:///path/to/file.yml`, `beacon://id00:25000/path/to/file.yml`).
30
+ Returns:
31
+ A Python dict/list structure
32
+ """
33
+ url = _parse_config_url(url)
34
+ if url.scheme == "beacon":
35
+ return _read_config_beacon(url)
36
+ elif url.scheme in ("file", ""):
37
+ return _read_config_yaml(url)
38
+ else:
39
+ raise ValueError(
40
+ f"Configuration URL scheme '{url.scheme}' is not supported (Full URL: {url})"
41
+ )
42
+
43
+
44
+ def _parse_config_url(url: str) -> ParseResult:
45
+ presult = urlparse(url)
46
+ if presult.scheme == "beacon":
47
+ # beacon:///path/to/file.yml
48
+ # beacon://id00:25000/path/to/file.yml
49
+ return presult
50
+ elif presult.scheme in ("file", ""):
51
+ # /path/to/file.yaml
52
+ # file:///path/to/file.yaml
53
+ return presult
54
+ elif sys.platform == "win32" and len(presult.scheme) == 1:
55
+ # c:\\path\\to\\file.yaml
56
+ return urlparse(f"file://{url}")
57
+ else:
58
+ return presult
59
+
60
+
61
+ def _url_to_filename(url: ParseResult) -> str:
62
+ if url.netloc and url.path:
63
+ # urlparse("file://c:/a/b")
64
+ return url.netloc + url.path
65
+ elif url.netloc:
66
+ # urlparse("file://c:\\a\\b")
67
+ return url.netloc
68
+ else:
69
+ return url.path
70
+
71
+
72
+ def _read_config_beacon(url: ParseResult) -> Any:
73
+ if url.netloc:
74
+ host, port = url.netloc.split(":")
75
+ port = int(port)
76
+ else:
77
+ host = None
78
+ port = None
79
+
80
+ # Bliss < 1.11: Beacon cannot handle leading slashes
81
+ file_path = url.path
82
+ while file_path.startswith("/"):
83
+ file_path = file_path[1:]
84
+
85
+ beacon = BeaconFiles(host=host, port=port)
86
+ try:
87
+ config = beacon.get_file(file_path)
88
+ return yaml_load(config)
89
+ finally:
90
+ beacon.close()
91
+
92
+
93
+ def _read_config_yaml(url: ParseResult) -> Any:
94
+ if yaml_load is None:
95
+ raise ImportError(
96
+ "No yaml parser available. Try to install 'ruamel.yaml' or 'pyyaml'"
97
+ )
98
+ filename = _url_to_filename(url)
99
+ with open(filename, "r") as f:
100
+ return yaml_load(f)
101
+
102
+
103
+ class BeaconFiles(BeaconClient):
104
+ """Provides the API to read files managed by Beacon."""
105
+
106
+ CONFIG_GET_FILE = 50
107
+ CONFIG_GET_FILE_FAILED = 51
108
+ CONFIG_GET_FILE_OK = 52
109
+
110
+ CONFIG_GET_DB_TREE = 86
111
+ CONFIG_GET_DB_TREE_FAILED = 87
112
+ CONFIG_GET_DB_TREE_OK = 88
113
+
114
+ def get_file(self, file_path: str) -> bytes:
115
+ """Returns the binary content of a file from the Beacon configuration
116
+ file repository."""
117
+ with self._lock:
118
+ response = self._request(self.CONFIG_GET_FILE, file_path)
119
+ response_type, data = response.read()
120
+ if response_type == self.CONFIG_GET_FILE_OK:
121
+ return data
122
+ elif response_type == self.CONFIG_GET_FILE_FAILED:
123
+ raise RuntimeError(data.decode())
124
+ raise RuntimeError(f"Unexpected Beacon response type {response_type}")
125
+
126
+ def get_tree(self, base_path: str = "") -> dict:
127
+ """Returns the file tree from a base path of the Beacon configuration
128
+ file repository.
129
+
130
+ Return: A nested dictionary structure, where a file is a mapping
131
+ `filename: None`, an a directory is mapping of a dirname and a
132
+ nested dictionary.
133
+ """
134
+ with self._lock:
135
+ response = self._request(self.CONFIG_GET_DB_TREE, base_path)
136
+ response_type, data = response.read()
137
+ if response_type == self.CONFIG_GET_DB_TREE_OK:
138
+ return json.loads(data)
139
+ elif response_type == self.CONFIG_GET_DB_TREE_FAILED:
140
+ raise RuntimeError(data.decode())
141
+ raise RuntimeError(f"Unexpected Beacon response type {response_type}")
blissdata/client.py ADDED
@@ -0,0 +1,62 @@
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 typing import Optional
9
+ from blissdata.beacon.data import BeaconData
10
+ from blissdata.redis.manager import RedisConnectionManager, RedisAddress
11
+
12
+
13
+ _default_redis_connection_manager_callback = None
14
+
15
+
16
+ def set_default_redis_connection_manager_callback(cb):
17
+ global _default_redis_connection_manager_callback
18
+ _default_redis_connection_manager_callback = cb
19
+
20
+
21
+ def configure_with_beacon_address(
22
+ host: Optional[str] = None, port: Optional[int] = None
23
+ ):
24
+ beacon_client = BeaconData(host=host, port=port)
25
+ addresses = {
26
+ 0: RedisAddress.factory(beacon_client.get_redis_db()),
27
+ 1: RedisAddress.factory(beacon_client.get_redis_data_db()),
28
+ }
29
+ redis_connection_manager = RedisConnectionManager(addresses)
30
+
31
+ def redis_connection_manager_cb():
32
+ return redis_connection_manager
33
+
34
+ set_default_redis_connection_manager_callback(redis_connection_manager_cb)
35
+
36
+
37
+ def _get_default_redis_connection_manager():
38
+ global _default_redis_connection_manager_callback
39
+ if _default_redis_connection_manager_callback is None:
40
+ try:
41
+ configure_with_beacon_address()
42
+ except Exception as e:
43
+ raise RuntimeError("Blissdata configuration from BEACON_HOST failed") from e
44
+ return _default_redis_connection_manager_callback()
45
+
46
+
47
+ def get_redis_proxy(db: int = 0, caching: bool = False, shared: bool = True):
48
+ """Get a greenlet-safe proxy to a Redis database.
49
+
50
+ :param int db: Redis database too which we need a proxy
51
+ :param bool caching: client-side caching
52
+ :param bool shared: use a shared proxy held by the Beacon connection
53
+ """
54
+ return _get_default_redis_connection_manager().get_db_proxy(
55
+ db=db, caching=caching, shared=shared
56
+ )
57
+
58
+
59
+ def close_all_redis_connections():
60
+ default_redis_connection_manager = _get_default_redis_connection_manager()
61
+ if _get_default_redis_connection_manager() is not None:
62
+ default_redis_connection_manager.close_all_connections()
@@ -0,0 +1,15 @@
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
+
9
+ """Common function section
10
+
11
+ .. autosummary::
12
+ :toctree:
13
+
14
+ utils
15
+ """
@@ -0,0 +1,159 @@
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
+ import fnmatch
9
+ import re
10
+ from collections.abc import Mapping
11
+
12
+
13
+ class UndefinedType:
14
+ __slots__ = []
15
+
16
+
17
+ Undefined = UndefinedType()
18
+ """Can be used as default function argument when a `None` value is a valid
19
+ and optional input. For example for a default value from a `dict.get` method."""
20
+
21
+
22
+ def deep_update(d, u):
23
+ """Do a deep merge of one dict into another.
24
+
25
+ This will update d with values in u, but will not delete keys in d
26
+ not found in u at some arbitrary depth of d. That is, u is deeply
27
+ merged into d.
28
+
29
+ Args -
30
+ d, u: dicts
31
+
32
+ Note: this is destructive to d, but not u.
33
+
34
+ Returns: None
35
+ """
36
+ stack = [(d, u)]
37
+ while stack:
38
+ d, u = stack.pop(0)
39
+ for k, v in u.items():
40
+ if not isinstance(v, Mapping):
41
+ # u[k] is not a dict, nothing to merge, so just set it,
42
+ # regardless if d[k] *was* a dict
43
+ d[k] = v
44
+ else:
45
+ # note: u[k] is a dict
46
+
47
+ # get d[k], defaulting to a dict, if it doesn't previously
48
+ # exist
49
+ dv = d.setdefault(k, {})
50
+
51
+ if not isinstance(dv, Mapping):
52
+ # d[k] is not a dict, so just set it to u[k],
53
+ # overriding whatever it was
54
+ d[k] = v
55
+ else:
56
+ # both d[k] and u[k] are dicts, push them on the stack
57
+ # to merge
58
+ stack.append((dv, v))
59
+
60
+
61
+ def grouped(iterable, n):
62
+ """
63
+ Group elements of an iterable n by n.
64
+ Return a zip object.
65
+ s -> (s0,s1,s2,...sn-1), (sn,sn+1,sn+2,...s2n-1), (s2n,s2n+1,s2n+2,...s3n-1), ...
66
+ Excedentary elements are discarded.
67
+ Example:
68
+ DEMO [5]: list(grouped([1,2,3,4,5], 2))
69
+ Out [5]: [(1, 2), (3, 4)]
70
+ """
71
+ return zip(*[iter(iterable)] * n)
72
+
73
+
74
+ def update_node_info(node, d):
75
+ """Updates the BaseHashSetting of a DataNode and does a deep update if needed.
76
+ parameters: node: DataNode or DataNodeContainer; d: dict"""
77
+ assert isinstance(d, Mapping)
78
+ for key, value in d.items():
79
+ tmp = node.info.get(key)
80
+ if tmp and isinstance(value, Mapping) and isinstance(tmp, Mapping):
81
+ deep_update(tmp, value)
82
+ node.info[key] = tmp
83
+ else:
84
+ node.info[key] = value
85
+
86
+
87
+ def get_matching_names(patterns, names, strict_pattern_as_short_name=False):
88
+ """Search a pattern into a list of names (unix pattern style).
89
+
90
+ .. list-table::
91
+ :header-rows: 1
92
+
93
+ * - Pattern
94
+ - Meaning
95
+ * - `*`
96
+ - matches everything
97
+ * - `?`
98
+ - matches any single character
99
+ * - `[seq]`
100
+ - matches any character in seq
101
+ * - `[!seq]`
102
+ - matches any character not in seq
103
+
104
+ Arguments:
105
+ patterns: a list of patterns
106
+ names: a list of names
107
+ strict_pattern_as_short_name: if True patterns without special character,
108
+ are transformed like this: `'pattern' -> '*:pattern'`
109
+ (as the 'short name' part of a 'fullname')
110
+
111
+ Return: dict { pattern : matching names }
112
+ """
113
+
114
+ special_char = ["*", ":"]
115
+
116
+ if not isinstance(patterns, (list, tuple)):
117
+ patterns = [patterns]
118
+
119
+ matches = {}
120
+ for pat in patterns:
121
+
122
+ if not isinstance(pat, str):
123
+ pat = str(pat)
124
+
125
+ sub_pat = [pat]
126
+
127
+ if strict_pattern_as_short_name:
128
+ if all([sc not in pat for sc in special_char]):
129
+ sub_pat = [f"*:{pat}", f"*:{pat}:*", f"{pat}:*"]
130
+
131
+ # store the fullname of matching counters
132
+ matching_names = []
133
+ for _pat in sub_pat:
134
+
135
+ for name in names:
136
+ if fnmatch.fnmatch(name, _pat):
137
+ matching_names.append(name)
138
+
139
+ if matching_names:
140
+ break
141
+
142
+ matches[pat] = matching_names
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)
@@ -0,0 +1,15 @@
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
+ """Data management
9
+
10
+ .. autosummary::
11
+ :toctree:
12
+
13
+ nodes
14
+ scan
15
+ """
@@ -0,0 +1,22 @@
1
+ """
2
+ Raw Redis stream event decoding/encoding for all data nodes
3
+ Importing this module will "register" all stream events.
4
+ """
5
+
6
+ from . import node
7
+ from .node import * # noqa: F401,F403
8
+ from . import channel
9
+ from .channel import * # noqa: F401,F403
10
+ from . import lima
11
+ from .lima import * # noqa: F401,F403
12
+ from . import scan
13
+ from .scan import * # noqa: F401,F403
14
+ from . import walk
15
+ from .walk import * # noqa: F401,F403
16
+
17
+ __all__ = []
18
+ __all__.extend(node.__all__)
19
+ __all__.extend(channel.__all__)
20
+ __all__.extend(lima.__all__)
21
+ __all__.extend(scan.__all__)
22
+ __all__.extend(walk.__all__)