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.
- blissdata/__init__.py +17 -0
- blissdata/beacon/__init__.py +1 -0
- blissdata/beacon/_base.py +106 -0
- blissdata/beacon/config.py +24 -0
- blissdata/beacon/data.py +45 -0
- blissdata/beacon/files.py +141 -0
- blissdata/client.py +62 -0
- blissdata/common/__init__.py +15 -0
- blissdata/common/utils.py +159 -0
- blissdata/data/__init__.py +15 -0
- blissdata/data/events/__init__.py +22 -0
- blissdata/data/events/channel.py +149 -0
- blissdata/data/events/lima.py +478 -0
- blissdata/data/events/node.py +28 -0
- blissdata/data/events/scan.py +49 -0
- blissdata/data/events/walk.py +42 -0
- blissdata/data/expiration.py +42 -0
- blissdata/data/lima_image.py +465 -0
- blissdata/data/node.py +1619 -0
- blissdata/data/nodes/__init__.py +11 -0
- blissdata/data/nodes/channel.py +394 -0
- blissdata/data/nodes/dataset.py +82 -0
- blissdata/data/nodes/dataset_collection.py +12 -0
- blissdata/data/nodes/lima.py +422 -0
- blissdata/data/nodes/node_ref_channel.py +34 -0
- blissdata/data/nodes/proposal.py +12 -0
- blissdata/data/nodes/scan.py +192 -0
- blissdata/data/nodes/scan_group.py +12 -0
- blissdata/data/remote_node.py +204 -0
- blissdata/data/scan.py +666 -0
- blissdata/h5api/__init__.py +1 -0
- blissdata/h5api/abstract.py +97 -0
- blissdata/h5api/dynamic_hdf5.py +153 -0
- blissdata/h5api/file_arguments.py +28 -0
- blissdata/h5api/static_hdf5.py +139 -0
- blissdata/h5api/utils/__init__.py +0 -0
- blissdata/h5api/utils/bliss.py +138 -0
- blissdata/h5api/utils/hdf5.py +280 -0
- blissdata/h5api/utils/hdf5_retry.py +98 -0
- blissdata/h5api/utils/lima.py +286 -0
- blissdata/h5api/utils/types.py +13 -0
- blissdata/redis/__init__.py +12 -0
- blissdata/redis/caching.py +390 -0
- blissdata/redis/connection.py +169 -0
- blissdata/redis/manager.py +164 -0
- blissdata/redis/proxy.py +971 -0
- blissdata/redis/scripting.py +24 -0
- blissdata/settings.py +1174 -0
- blissdata/streaming.py +819 -0
- blissdata/streaming_events.py +355 -0
- blissdata/tests/__init__.py +0 -0
- blissdata/tests/beacon/__init__.py +0 -0
- blissdata/tests/beacon/test_config.py +26 -0
- blissdata/tests/beacon/test_data.py +50 -0
- blissdata/tests/beacon/test_files.py +80 -0
- blissdata/tests/conftest.py +0 -0
- blissdata/tests/h5api/__init__.py +0 -0
- blissdata/tests/h5api/scanner.py +390 -0
- blissdata/tests/h5api/test_dynamic_files.py +309 -0
- blissdata/tests/h5api/test_static_files.py +169 -0
- blissdata/tests/redis/__init__.py +0 -0
- blissdata/tests/redis/manager.py +23 -0
- blissdata-0.3.4.dist-info/LICENSE +165 -0
- blissdata-0.3.4.dist-info/METADATA +79 -0
- blissdata-0.3.4.dist-info/RECORD +67 -0
- blissdata-0.3.4.dist-info/WHEEL +5 -0
- 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
|
+
)
|
blissdata/beacon/data.py
ADDED
|
@@ -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__)
|