blissdata 2.1.0__tar.gz → 2.2.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.
- {blissdata-2.1.0/src/blissdata.egg-info → blissdata-2.2.0}/PKG-INFO +1 -1
- {blissdata-2.1.0 → blissdata-2.2.0}/pyproject.toml +1 -1
- blissdata-2.2.0/src/blissdata/h5api/h5_scan_map.py +133 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/redis_hdf5.py +238 -99
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5map/data_items.py +1 -1
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/redis_engine/scan.py +44 -26
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/streams/base.py +51 -7
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/streams/lima2/stream.py +18 -5
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/conftest.py +9 -0
- blissdata-2.2.0/src/blissdata/tests/h5api/test_redis_files.py +485 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/h5map/test_data_items.py +1 -0
- blissdata-2.2.0/src/blissdata/tests/plugin_streams/test_plugin_base.py +112 -0
- {blissdata-2.1.0 → blissdata-2.2.0/src/blissdata.egg-info}/PKG-INFO +1 -1
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata.egg-info/SOURCES.txt +2 -1
- blissdata-2.1.0/src/blissdata/h5api/scan_mapping.py +0 -295
- blissdata-2.1.0/src/blissdata/tests/h5api/test_redis_files.py +0 -287
- {blissdata-2.1.0 → blissdata-2.2.0}/LICENSE +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/README.md +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/setup.cfg +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/beacon/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/beacon/_base.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/beacon/_utils.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/beacon/config.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/beacon/data.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/beacon/files.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/abstract.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/dynamic_hdf5.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/file_arguments.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/static_hdf5.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/utils/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/utils/bliss.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/utils/hdf5.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/utils/hdf5_retry.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/utils/lima.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5api/utils/types.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5map/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/h5map/hdf5_items.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/lima/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/lima/client.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/lima/image_utils.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/py.typed +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/redis_engine/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/redis_engine/encoding/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/redis_engine/encoding/json.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/redis_engine/encoding/numeric.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/redis_engine/event_stream.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/redis_engine/exceptions.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/redis_engine/identities.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/redis_engine/memory_tracker.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/redis_engine/sink.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/redis_engine/store.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/scan.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/schemas/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/schemas/scan_info.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/schemas/scan_info_display.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/streams/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/streams/hdf5_fallback/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/streams/hdf5_fallback/stream.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/streams/lima/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/streams/lima/stream.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/streams/lima2/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/streams/scan_sequence/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/streams/scan_sequence/stream.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/beacon/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/beacon/test_config.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/beacon/test_data.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/beacon/test_files.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/h5api/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/h5api/scanner.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/h5api/test_dynamic_files.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/h5api/test_h5api_lima_utils.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/h5api/test_hdf5_handlers.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/h5api/test_static_files.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/h5map/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/h5map/test_hdf5_items.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/lima/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/lima/test_image_utils.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/plugin_streams/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/plugin_streams/test_file_backed_stream.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/plugin_streams/test_lima2_stream.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/redis_engine/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/redis_engine/conftest.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/redis_engine/encoding/__init__.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/redis_engine/encoding/test_json.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/redis_engine/encoding/test_numeric.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/redis_engine/test_cursor.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/redis_engine/test_event_stream.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/redis_engine/test_memory_tracker.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/redis_engine/test_scan.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/redis_engine/test_search.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata/tests/redis_engine/utils.py +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata.egg-info/dependency_links.txt +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata.egg-info/entry_points.txt +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata.egg-info/requires.txt +0 -0
- {blissdata-2.1.0 → blissdata-2.2.0}/src/blissdata.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import logging
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from pathlib import PurePosixPath as Path
|
|
5
|
+
from blissdata.redis_engine.scan import ScanState
|
|
6
|
+
from blissdata.h5map import HDF5ItemType, Group
|
|
7
|
+
|
|
8
|
+
_logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class H5ScanMap(Mapping):
|
|
12
|
+
"""H5ScanMap is a wrapper around a blissdata's Scan. It exposes its HDF5
|
|
13
|
+
mapping with conventional dict API, but also handles automatic refresh of
|
|
14
|
+
the underlying model from Redis (see _updated_root method).
|
|
15
|
+
|
|
16
|
+
IMPORTANT: This is not an H5-like object, it helps manipulating the mapping
|
|
17
|
+
within a scan. Then H5-like groups and datasets can be implemented on top of
|
|
18
|
+
it."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, scan):
|
|
21
|
+
self._scan = scan
|
|
22
|
+
self._root = None
|
|
23
|
+
self._last_update = time.perf_counter()
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def scan(self):
|
|
27
|
+
return self._scan
|
|
28
|
+
|
|
29
|
+
def __len__(self):
|
|
30
|
+
return len(self._updated_root().children)
|
|
31
|
+
|
|
32
|
+
def __iter__(self):
|
|
33
|
+
yield from self._updated_root().children.keys()
|
|
34
|
+
|
|
35
|
+
def __getitem__(self, path: str) -> HDF5ItemType:
|
|
36
|
+
"""Return the HDF5Item associated to that path.
|
|
37
|
+
Links are not resolved here, instead a SoftLink or ExternalLink is
|
|
38
|
+
returned and it is caller choice to resolve it or not.
|
|
39
|
+
"""
|
|
40
|
+
assert path.startswith("/")
|
|
41
|
+
h5_item = self._updated_root()
|
|
42
|
+
current_path = Path("/")
|
|
43
|
+
for part in Path(path).parts:
|
|
44
|
+
if part == "/":
|
|
45
|
+
continue
|
|
46
|
+
current_path /= part
|
|
47
|
+
if not isinstance(h5_item, Group):
|
|
48
|
+
raise KeyError(str(current_path))
|
|
49
|
+
try:
|
|
50
|
+
h5_item = h5_item.children[part]
|
|
51
|
+
except KeyError:
|
|
52
|
+
raise KeyError(str(current_path))
|
|
53
|
+
return h5_item
|
|
54
|
+
|
|
55
|
+
def path_partition(self, path: str) -> tuple[str, HDF5ItemType | None, str]:
|
|
56
|
+
"""Similar to __getitem__, but may return partially resolved path.
|
|
57
|
+
Goal is to allow link items to be returned along with the remaining
|
|
58
|
+
part of the path. Links can be resolved before going further.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
tuple:
|
|
62
|
+
- path of the returned item
|
|
63
|
+
- h5map item or None if path doesn't exist
|
|
64
|
+
- remaining path (empty if item is the actual target)
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
/
|
|
68
|
+
├─a
|
|
69
|
+
│ ├─b (link to d)
|
|
70
|
+
│ └─c
|
|
71
|
+
└─d
|
|
72
|
+
└─e
|
|
73
|
+
|
|
74
|
+
path_partition("/a/b/c") -> "/a/b/c", Dataset(...), ""
|
|
75
|
+
path_partition("/a/b/e") -> "/a/b", SoftLink("/d"), "e"
|
|
76
|
+
path_partition("/a/x/y") -> "/a/x", None, "y"
|
|
77
|
+
"""
|
|
78
|
+
assert path.startswith("/")
|
|
79
|
+
h5_item = self._updated_root()
|
|
80
|
+
item_parts = ["/"]
|
|
81
|
+
remaining_parts = list(Path(path).parts[1:])
|
|
82
|
+
while remaining_parts:
|
|
83
|
+
if isinstance(h5_item, Group):
|
|
84
|
+
try:
|
|
85
|
+
item_parts.append(remaining_parts.pop(0))
|
|
86
|
+
h5_item = h5_item.children[item_parts[-1]]
|
|
87
|
+
except KeyError:
|
|
88
|
+
h5_item = None
|
|
89
|
+
break
|
|
90
|
+
else:
|
|
91
|
+
break
|
|
92
|
+
remaining_path = str(Path(*remaining_parts)) if remaining_parts else ""
|
|
93
|
+
return str(Path(*item_parts)), h5_item, remaining_path
|
|
94
|
+
|
|
95
|
+
def _updated_root(self) -> Group:
|
|
96
|
+
"""Return an h5map.Group loaded from scan info. Reuse previous answer if
|
|
97
|
+
not older than 0.2 seconds (polling Redis to 5Hz at most if fine).
|
|
98
|
+
Moreover, scan state doesn't change that much and the scan's json is
|
|
99
|
+
only downloaded on changes.
|
|
100
|
+
|
|
101
|
+
In case the scan provides no mapping, an empty Group is returned.
|
|
102
|
+
"""
|
|
103
|
+
scan_state_changed = False
|
|
104
|
+
|
|
105
|
+
# Mapping is not expected to be published before scan is prepared, wait
|
|
106
|
+
while self._scan.state < ScanState.PREPARED:
|
|
107
|
+
self._scan.update()
|
|
108
|
+
self._last_update = time.perf_counter()
|
|
109
|
+
scan_state_changed = True
|
|
110
|
+
|
|
111
|
+
# Update whenever the scan is not CLOSED and the last_update is not too
|
|
112
|
+
# recent.
|
|
113
|
+
min_update_interval = 0.2
|
|
114
|
+
now = time.perf_counter()
|
|
115
|
+
if (
|
|
116
|
+
self._last_update + min_update_interval < now
|
|
117
|
+
and self._scan.state < ScanState.CLOSED
|
|
118
|
+
):
|
|
119
|
+
scan_state_changed = self._scan.update(block=False)
|
|
120
|
+
self._last_update = time.perf_counter()
|
|
121
|
+
|
|
122
|
+
# (re)generate mapping tree if necessary
|
|
123
|
+
if scan_state_changed or self._root is None:
|
|
124
|
+
try:
|
|
125
|
+
mappings = self._scan.info.get("h5maps", {})
|
|
126
|
+
self._root = Group.model_validate(mappings[self._scan.path])
|
|
127
|
+
except KeyError:
|
|
128
|
+
_logger.warning(
|
|
129
|
+
f"Scan {self._scan.number} has no hdf5 mapping ({self._scan.key})"
|
|
130
|
+
)
|
|
131
|
+
return Group()
|
|
132
|
+
|
|
133
|
+
return self._root
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import time
|
|
3
|
+
import math
|
|
3
4
|
import functools
|
|
4
5
|
import numpy as np
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
5
7
|
from pathlib import PurePosixPath as Path
|
|
6
8
|
|
|
9
|
+
import blissdata.h5map as h5m
|
|
7
10
|
from blissdata.beacon.data import BeaconData
|
|
8
11
|
from blissdata.redis_engine.store import DataStore
|
|
9
|
-
from blissdata.h5api import abstract
|
|
10
|
-
from blissdata.h5api.scan_mapping import (
|
|
11
|
-
MappingNode,
|
|
12
|
-
GroupNode,
|
|
13
|
-
DatasetNode,
|
|
14
|
-
ScanMapper,
|
|
15
|
-
SoftLinkNode,
|
|
16
|
-
)
|
|
17
12
|
from blissdata.redis_engine.exceptions import EndOfStream, IndexNotYetThereError
|
|
13
|
+
from blissdata.h5api import abstract
|
|
14
|
+
from blissdata.h5api.h5_scan_map import H5ScanMap
|
|
18
15
|
|
|
19
16
|
|
|
20
17
|
class Attributes(abstract.Attributes):
|
|
@@ -41,9 +38,13 @@ class File(abstract.File):
|
|
|
41
38
|
file can iterate on live data.
|
|
42
39
|
"""
|
|
43
40
|
|
|
44
|
-
def __init__(
|
|
41
|
+
def __init__(
|
|
42
|
+
self, filepath: str, mode: str = "r", data_store: DataStore | None = None
|
|
43
|
+
):
|
|
45
44
|
"""If no data_store is provided, but $BEACON_HOST variable is defined,
|
|
46
45
|
then try to query Redis address from that beacon server."""
|
|
46
|
+
if mode != "r":
|
|
47
|
+
raise ValueError("Invalid mode; only r is supported with live mode")
|
|
47
48
|
self._filepath = Path(filepath)
|
|
48
49
|
self._closed = False
|
|
49
50
|
if data_store is None:
|
|
@@ -81,58 +82,67 @@ class File(abstract.File):
|
|
|
81
82
|
self._closed = True
|
|
82
83
|
|
|
83
84
|
def __repr__(self):
|
|
84
|
-
return f'<
|
|
85
|
+
return f'<Live-HDF5 file "{self._filepath.name}" (mode r)>'
|
|
85
86
|
|
|
86
87
|
def __len__(self):
|
|
87
88
|
"""Return the instant length from the scans that are already there."""
|
|
88
89
|
return len(self.keys())
|
|
89
90
|
|
|
90
91
|
def __getitem__(self, path: str):
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if scan_path == "/":
|
|
94
|
-
return Group(self, path, self.attrs)
|
|
95
|
-
|
|
96
|
-
try:
|
|
97
|
-
scan_mapper = self._get_scan_mapper_by_path(scan_path)
|
|
98
|
-
node = scan_mapper[str(path)]
|
|
99
|
-
except KeyError:
|
|
100
|
-
raise KeyError(f"No such path: {path}")
|
|
92
|
+
# save originally requested path
|
|
93
|
+
original_path = path
|
|
101
94
|
|
|
95
|
+
# track visited paths to detect link cycles
|
|
102
96
|
visited_paths = []
|
|
103
|
-
while
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if path in visited_paths:
|
|
97
|
+
while path:
|
|
98
|
+
path = Path("/") / path
|
|
99
|
+
if str(path) in visited_paths:
|
|
107
100
|
raise KeyError(
|
|
108
|
-
f"
|
|
101
|
+
f"'{original_path}': path can't be resolved, found link cycle: {[str(p) for p in visited_paths]}"
|
|
109
102
|
)
|
|
103
|
+
else:
|
|
104
|
+
visited_paths.append(str(path))
|
|
105
|
+
|
|
106
|
+
scan_path = self._scan_path(path)
|
|
107
|
+
if scan_path == "/":
|
|
108
|
+
return Group(self, "/", self.attrs)
|
|
109
|
+
|
|
110
110
|
try:
|
|
111
|
-
|
|
111
|
+
scan_map = self._get_scan_map_by_path(scan_path)
|
|
112
112
|
except KeyError:
|
|
113
|
-
raise KeyError(f"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
elif
|
|
127
|
-
|
|
128
|
-
|
|
113
|
+
raise KeyError(f"No such path: '{scan_path}'")
|
|
114
|
+
|
|
115
|
+
item_path, item, remain_path = scan_map.path_partition(str(path))
|
|
116
|
+
if item is None:
|
|
117
|
+
raise KeyError(f"No such path: '{item_path}'")
|
|
118
|
+
|
|
119
|
+
if isinstance(item, h5m.SoftLink):
|
|
120
|
+
path = Path(item_path).parent / item.target_path / remain_path
|
|
121
|
+
elif isinstance(item, h5m.ExternalLink):
|
|
122
|
+
# NOTE no protection against link cycles involving multiple files
|
|
123
|
+
return File(item.target_file, data_store=self._data_store)[
|
|
124
|
+
str(Path(item.target_path) / remain_path)
|
|
125
|
+
]
|
|
126
|
+
elif remain_path:
|
|
127
|
+
raise KeyError(
|
|
128
|
+
f"'{item_path}' is not a Group, can't reach '{str(path)}'"
|
|
129
129
|
)
|
|
130
130
|
else:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
return self._map_item_to_h5(str(path), item, scan_map.scan)
|
|
132
|
+
|
|
133
|
+
def _map_item_to_h5(self, path: str, item: h5m.HDF5ItemType, scan):
|
|
134
|
+
if isinstance(item, h5m.Group):
|
|
135
|
+
return Group(self, path, item.attributes)
|
|
136
|
+
elif isinstance(
|
|
137
|
+
item, (h5m.Dataset, h5m.ExternalBinaryDataset, h5m.VirtualDataset)
|
|
138
|
+
):
|
|
139
|
+
return dataset_factory(self, path, item, scan)
|
|
140
|
+
elif isinstance(item, h5m.SoftLink):
|
|
141
|
+
raise TypeError("Oops, unresolved h5m.SoftLink should not end up there")
|
|
142
|
+
elif isinstance(item, h5m.ExternalLink):
|
|
143
|
+
raise TypeError("Oops, unresolved h5m.ExternalLink should not end up there")
|
|
134
144
|
else:
|
|
135
|
-
raise TypeError(f"Unknown
|
|
145
|
+
raise TypeError(f"Unknown item type {type(item).__name__}")
|
|
136
146
|
|
|
137
147
|
def __iter__(self) -> str:
|
|
138
148
|
"""Iterate forever: keep waiting for new scans once existing ones are
|
|
@@ -140,7 +150,7 @@ class File(abstract.File):
|
|
|
140
150
|
# iterate through existing scans in that file_path
|
|
141
151
|
ts, keys = self._data_store.search_existing_scans(path=self.filename)
|
|
142
152
|
for key in keys:
|
|
143
|
-
yield from self.
|
|
153
|
+
yield from self._get_scan_map_by_key(key)
|
|
144
154
|
|
|
145
155
|
# iterate forever over the next scans
|
|
146
156
|
while True:
|
|
@@ -150,7 +160,7 @@ class File(abstract.File):
|
|
|
150
160
|
# outside of blissdata (a proper API should be discussed if needed)
|
|
151
161
|
path = self._data_store._redis.json().get(key, "id.path")
|
|
152
162
|
if path == self.filename:
|
|
153
|
-
yield from self.
|
|
163
|
+
yield from self._get_scan_map_by_key(key)
|
|
154
164
|
|
|
155
165
|
def keys(self):
|
|
156
166
|
"""Instant list of keys in the file. Can be used to only iterate over
|
|
@@ -158,12 +168,12 @@ class File(abstract.File):
|
|
|
158
168
|
_, keys = self._data_store.search_existing_scans(path=self.filename)
|
|
159
169
|
ret = set()
|
|
160
170
|
for key in keys:
|
|
161
|
-
ret |= set(self.
|
|
171
|
+
ret |= set(self._get_scan_map_by_key(key).keys())
|
|
162
172
|
return ret
|
|
163
173
|
|
|
164
|
-
def
|
|
174
|
+
def _get_scan_map_by_path(self, scan_path: str) -> H5ScanMap:
|
|
165
175
|
key = self._get_scan_key(scan_path)
|
|
166
|
-
return self.
|
|
176
|
+
return self._get_scan_map_by_key(key)
|
|
167
177
|
|
|
168
178
|
def _get_scan_key(self, scan_path: str) -> str:
|
|
169
179
|
return File._get_scan_key_cached(self._data_store, self.filename, scan_path)
|
|
@@ -187,60 +197,60 @@ class File(abstract.File):
|
|
|
187
197
|
)
|
|
188
198
|
return keys.pop()
|
|
189
199
|
|
|
190
|
-
def
|
|
191
|
-
"""Load a scan from Redis, wrap it into a
|
|
200
|
+
def _get_scan_map_by_key(self, key: str) -> H5ScanMap:
|
|
201
|
+
"""Load a scan from Redis, wrap it into a H5ScanMap and update
|
|
192
202
|
cache"""
|
|
193
|
-
return File.
|
|
203
|
+
return File._get_scan_map_by_key_cached(self._data_store, key)
|
|
194
204
|
|
|
195
205
|
@staticmethod
|
|
196
206
|
@functools.lru_cache(maxsize=100)
|
|
197
|
-
def
|
|
207
|
+
def _get_scan_map_by_key_cached(data_store: DataStore, key: str) -> H5ScanMap:
|
|
198
208
|
scan = data_store.load_scan(key)
|
|
199
|
-
return
|
|
209
|
+
return H5ScanMap(scan)
|
|
200
210
|
|
|
201
211
|
def _len_group(self, path: str) -> int:
|
|
202
212
|
"""Used by Group.__len__ to query length of a particular path (not only
|
|
203
213
|
the root)"""
|
|
204
|
-
scan_path
|
|
214
|
+
scan_path = self._scan_path(path)
|
|
205
215
|
if scan_path == "/":
|
|
206
216
|
return len(self)
|
|
207
217
|
else:
|
|
208
218
|
# number of items inside a scan
|
|
209
219
|
try:
|
|
210
|
-
|
|
211
|
-
|
|
220
|
+
scan_map = self._get_scan_map_by_path(scan_path)
|
|
221
|
+
group_item = scan_map[path]
|
|
212
222
|
except KeyError as e:
|
|
213
223
|
raise KeyError(f"No such path: {str(scan_path / e.args[0])}")
|
|
214
|
-
return len(
|
|
224
|
+
return len(group_item.children)
|
|
215
225
|
|
|
216
226
|
def _iter_group(self, path: str) -> str:
|
|
217
227
|
"""Used by Group.__iter__ to iterate through a particular path (not
|
|
218
228
|
only the root)."""
|
|
219
|
-
scan_path
|
|
229
|
+
scan_path = self._scan_path(path)
|
|
220
230
|
if scan_path == "/":
|
|
221
231
|
# iterate over scans (never stops)
|
|
222
232
|
yield from self
|
|
223
233
|
else:
|
|
224
234
|
# iterate inside a scan
|
|
225
235
|
try:
|
|
226
|
-
|
|
227
|
-
|
|
236
|
+
scan_map = self._get_scan_map_by_path(scan_path)
|
|
237
|
+
group_item = scan_map[path]
|
|
228
238
|
except KeyError as e:
|
|
229
239
|
raise KeyError(f"No such path: {str(scan_path / e.args[0])}")
|
|
230
|
-
yield from
|
|
231
|
-
|
|
232
|
-
def
|
|
233
|
-
"""Make path canonical and
|
|
234
|
-
'' -> '/',
|
|
235
|
-
'123.1' -> '/123.1',
|
|
236
|
-
'//123.1///' -> '/123.1',
|
|
237
|
-
'/123.1/a/b/c' -> '/123.1',
|
|
240
|
+
yield from group_item.children
|
|
241
|
+
|
|
242
|
+
def _scan_path(self, path: str) -> str:
|
|
243
|
+
"""Make path canonical and keep scan part only
|
|
244
|
+
'' -> '/',
|
|
245
|
+
'123.1' -> '/123.1',
|
|
246
|
+
'//123.1///' -> '/123.1',
|
|
247
|
+
'/123.1/a/b/c' -> '/123.1',
|
|
238
248
|
"""
|
|
239
249
|
path = Path("/") / path
|
|
240
250
|
if len(path.parts) <= 2:
|
|
241
|
-
return str(path)
|
|
251
|
+
return str(path)
|
|
242
252
|
else:
|
|
243
|
-
return str(Path(*path.parts[:2]))
|
|
253
|
+
return str(Path(*path.parts[:2]))
|
|
244
254
|
|
|
245
255
|
|
|
246
256
|
class Node(abstract.Node):
|
|
@@ -287,7 +297,11 @@ class Group(Node, abstract.Group):
|
|
|
287
297
|
return self._path
|
|
288
298
|
|
|
289
299
|
def __repr__(self):
|
|
290
|
-
|
|
300
|
+
try:
|
|
301
|
+
length = len(self)
|
|
302
|
+
except Exception:
|
|
303
|
+
length = "?"
|
|
304
|
+
return f'<Live-HDF5 group "{self._path}" ({length} members)>'
|
|
291
305
|
|
|
292
306
|
def __getitem__(self, path: str):
|
|
293
307
|
try:
|
|
@@ -304,48 +318,105 @@ class Group(Node, abstract.Group):
|
|
|
304
318
|
return self._file._len_group(self._path)
|
|
305
319
|
|
|
306
320
|
|
|
307
|
-
class
|
|
308
|
-
def __init__(
|
|
321
|
+
class Dataset(Node, abstract.Dataset):
|
|
322
|
+
def __init__(
|
|
323
|
+
self,
|
|
324
|
+
file: File,
|
|
325
|
+
path: str,
|
|
326
|
+
attrs: dict,
|
|
327
|
+
array: ...,
|
|
328
|
+
virtual: bool = False,
|
|
329
|
+
external=None,
|
|
330
|
+
):
|
|
309
331
|
Node.__init__(self, file, path, attrs)
|
|
310
|
-
self.
|
|
332
|
+
self._array = array
|
|
333
|
+
self._virtual = virtual
|
|
334
|
+
self._external = external
|
|
311
335
|
|
|
312
336
|
def __repr__(self):
|
|
313
|
-
|
|
337
|
+
if isinstance(self._array, StreamAsArray):
|
|
338
|
+
shape = f"(_,{','.join([str(i) for i in self._array._stream.shape])})"
|
|
339
|
+
else:
|
|
340
|
+
shape = self.shape
|
|
341
|
+
return f'<Live-HDF5 dataset "{Path(self.name).name}": shape {shape}, type "{self.dtype.str}">'
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def is_virtual(self):
|
|
345
|
+
return self._virtual
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def external(self):
|
|
349
|
+
return self._external
|
|
314
350
|
|
|
315
351
|
def __getitem__(self, idx):
|
|
316
|
-
if
|
|
317
|
-
|
|
318
|
-
return self.
|
|
352
|
+
if self.ndim == 0 and idx != ():
|
|
353
|
+
raise ValueError("Illegal slicing argument for scalar dataspace")
|
|
354
|
+
return self._array[idx]
|
|
319
355
|
|
|
320
356
|
def __len__(self):
|
|
321
|
-
|
|
357
|
+
if self.ndim == 0:
|
|
358
|
+
raise TypeError("Attempt to take len() of scalar dataset")
|
|
359
|
+
return len(self._array)
|
|
322
360
|
|
|
323
361
|
@property
|
|
324
362
|
def dtype(self):
|
|
325
|
-
|
|
363
|
+
return self._array.dtype
|
|
326
364
|
|
|
327
365
|
@property
|
|
328
366
|
def shape(self):
|
|
329
|
-
|
|
367
|
+
return self._array.shape
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def ndim(self):
|
|
371
|
+
return self._array.ndim
|
|
330
372
|
|
|
331
373
|
@property
|
|
332
374
|
def size(self):
|
|
333
|
-
|
|
375
|
+
return self._array.size
|
|
334
376
|
|
|
335
377
|
def __iter__(self):
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
yield from self.
|
|
378
|
+
if self.ndim == 0:
|
|
379
|
+
raise TypeError("Can't iterate over a scalar dataset")
|
|
380
|
+
yield from self._array
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class ArrayInterface(ABC):
|
|
384
|
+
@abstractmethod
|
|
385
|
+
def __getitem__(self, idx):
|
|
386
|
+
pass
|
|
387
|
+
|
|
388
|
+
@abstractmethod
|
|
389
|
+
def __len__(self):
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
@abstractmethod
|
|
394
|
+
def dtype(self):
|
|
395
|
+
pass
|
|
339
396
|
|
|
340
397
|
@property
|
|
398
|
+
@abstractmethod
|
|
399
|
+
def shape(self):
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
@abstractmethod
|
|
341
404
|
def ndim(self):
|
|
342
|
-
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
@abstractmethod
|
|
409
|
+
def size(self):
|
|
410
|
+
pass
|
|
343
411
|
|
|
412
|
+
@abstractmethod
|
|
413
|
+
def __iter__(self):
|
|
414
|
+
pass
|
|
344
415
|
|
|
345
|
-
|
|
346
|
-
|
|
416
|
+
|
|
417
|
+
class StreamAsArray(ArrayInterface):
|
|
418
|
+
def __init__(self, stream):
|
|
347
419
|
assert stream.kind == "array"
|
|
348
|
-
Node.__init__(self, file, path, attrs)
|
|
349
420
|
self._stream = stream
|
|
350
421
|
|
|
351
422
|
def __getitem__(self, idx):
|
|
@@ -384,22 +455,90 @@ class StreamDataset(Node, abstract.Dataset):
|
|
|
384
455
|
def shape(self):
|
|
385
456
|
return (len(self),) + self._stream.shape
|
|
386
457
|
|
|
458
|
+
@property
|
|
459
|
+
def ndim(self):
|
|
460
|
+
return len(self._stream.shape) + 1
|
|
461
|
+
|
|
387
462
|
@property
|
|
388
463
|
def size(self):
|
|
389
|
-
return
|
|
464
|
+
return math.prod(self.shape)
|
|
390
465
|
|
|
391
466
|
def __iter__(self):
|
|
392
467
|
cursor = self._stream.cursor()
|
|
393
|
-
timeout = 1.0
|
|
394
468
|
while True:
|
|
395
469
|
try:
|
|
396
|
-
view = cursor.read(
|
|
397
|
-
yield from view
|
|
398
|
-
# TODO views should be iterable to choose on their own what's
|
|
399
|
-
# the best batch size to download (e.g. LimaView)
|
|
470
|
+
view = cursor.read()
|
|
471
|
+
yield from view
|
|
400
472
|
except EndOfStream:
|
|
401
473
|
break
|
|
402
474
|
|
|
475
|
+
|
|
476
|
+
class ExternalBinaryAsArray:
|
|
477
|
+
def __init__(self, item):
|
|
478
|
+
self.memmaps = []
|
|
479
|
+
self._shape = tuple(item.shape)
|
|
480
|
+
self._dtype = np.dtype(item.dtype)
|
|
481
|
+
for file in item.files:
|
|
482
|
+
if file.size is None:
|
|
483
|
+
shape = None
|
|
484
|
+
else:
|
|
485
|
+
shape = (file.size // self._dtype.itemsize,)
|
|
486
|
+
self.memmaps.append(
|
|
487
|
+
np.memmap(
|
|
488
|
+
file.name,
|
|
489
|
+
dtype=self._dtype,
|
|
490
|
+
mode="r",
|
|
491
|
+
offset=file.offset,
|
|
492
|
+
shape=shape,
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# NOTE naive approach loading files in memory, but memmaps could be
|
|
497
|
+
# accessed on demand
|
|
498
|
+
self._data = np.concatenate([m for m in self.memmaps])[: self.size].reshape(
|
|
499
|
+
self.shape
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def __getitem__(self, key):
|
|
503
|
+
return self._data[key]
|
|
504
|
+
|
|
505
|
+
def __len__(self):
|
|
506
|
+
return self._shape[0]
|
|
507
|
+
|
|
508
|
+
@property
|
|
509
|
+
def dtype(self):
|
|
510
|
+
return self._dtype
|
|
511
|
+
|
|
512
|
+
@property
|
|
513
|
+
def shape(self):
|
|
514
|
+
return self._shape
|
|
515
|
+
|
|
403
516
|
@property
|
|
404
517
|
def ndim(self):
|
|
405
|
-
return len(self.
|
|
518
|
+
return len(self._shape)
|
|
519
|
+
|
|
520
|
+
@property
|
|
521
|
+
def size(self):
|
|
522
|
+
return math.prod(self._shape)
|
|
523
|
+
|
|
524
|
+
def __iter__(self):
|
|
525
|
+
yield from self._data
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def dataset_factory(parent: File, path: str, item: h5m.HDF5ItemType, scan) -> Dataset:
|
|
529
|
+
virtual = False
|
|
530
|
+
external = None
|
|
531
|
+
if isinstance(item, h5m.Dataset):
|
|
532
|
+
if isinstance(item.value, h5m.Stream):
|
|
533
|
+
array = StreamAsArray(scan.streams[item.value.stream])
|
|
534
|
+
else:
|
|
535
|
+
array = item.value.decode()
|
|
536
|
+
elif isinstance(item, h5m.ExternalBinaryDataset):
|
|
537
|
+
external = [(file.name, file.offset, file.size) for file in item.files]
|
|
538
|
+
array = ExternalBinaryAsArray(item)
|
|
539
|
+
elif isinstance(item, h5m.VirtualDataset):
|
|
540
|
+
virtual = True
|
|
541
|
+
# TODO
|
|
542
|
+
raise NotImplementedError
|
|
543
|
+
|
|
544
|
+
return Dataset(parent, path, item, array, virtual, external)
|