ewoksdata 2.3.0rc2__tar.gz → 2.3.0rc3__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.
- {ewoksdata-2.3.0rc2/src/ewoksdata.egg-info → ewoksdata-2.3.0rc3}/PKG-INFO +1 -1
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/pyproject.toml +1 -1
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/mirror.py +204 -63
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_mirror_hdf5.py +280 -107
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3/src/ewoksdata.egg-info}/PKG-INFO +1 -1
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/LICENSE.md +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/README.md +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/setup.cfg +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/__init__.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/__init__.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/bliss.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/blissdata/__init__.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/blissdata/_lima.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/blissdata/blissdatav0.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/blissdata/blissdatav1.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/blissdata/blissdatav2.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/contextiterator.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/__init__.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/config.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/__init__.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/_base.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/_sequential.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/types.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/nexus.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/url.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/utils.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/__init__.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/integration/__init__.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/integration/conftest.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/integration/test_blissdata.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/__init__.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/conftest.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/data/__init__.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/data/bliss_scans.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_contextiterator.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_data_bliss.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_data_hdf5.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_data_nexus.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_dataset_writer.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_find_lima_scans.py +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata.egg-info/SOURCES.txt +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata.egg-info/dependency_links.txt +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata.egg-info/requires.txt +0 -0
- {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ewoksdata"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.0rc3"
|
|
8
8
|
authors = [{ name = "ESRF", email = "dau-pydev@esrf.fr" }]
|
|
9
9
|
description = "Ewoks tasks for data access"
|
|
10
10
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
from typing import Any
|
|
3
4
|
from typing import NamedTuple
|
|
5
|
+
from typing import Optional
|
|
4
6
|
from typing import Sequence
|
|
5
7
|
from typing import Set
|
|
6
8
|
from typing import Union
|
|
7
9
|
|
|
8
10
|
import h5py
|
|
11
|
+
import numpy
|
|
9
12
|
from silx.io.url import DataUrl
|
|
10
13
|
|
|
11
14
|
# For type annotations only. Code works for any h5py-like objects.
|
|
@@ -44,7 +47,8 @@ def mirror_hdf5_content(
|
|
|
44
47
|
|
|
45
48
|
HDF5 links are soft links, external links and virtual datasets.
|
|
46
49
|
|
|
47
|
-
An HDF5 link with target(s) within :code:`original` is always kept as a link
|
|
50
|
+
An HDF5 link with target(s) within :code:`original` is always kept as a link,
|
|
51
|
+
re-targeted to the mirror of those target(s).
|
|
48
52
|
|
|
49
53
|
An HDF5 link with target(s) outside :code:`original` is called out-of-scope.
|
|
50
54
|
An out-of-scope link is kept as a link to wherever the data already is
|
|
@@ -78,10 +82,11 @@ def mirror_hdf5_content(
|
|
|
78
82
|
expand_paths = _normalize_relative_paths(expand)
|
|
79
83
|
recursive_paths = _normalize_relative_paths(recursive)
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
home_original = DataUrl(
|
|
82
86
|
file_path=os.path.abspath(_get_hdf5_filename(original.file)),
|
|
83
87
|
data_path=original.name,
|
|
84
88
|
)
|
|
89
|
+
home_destination = _normalize_hdf5_item_name(parent.name, name)
|
|
85
90
|
|
|
86
91
|
_mirror(
|
|
87
92
|
parent,
|
|
@@ -94,7 +99,8 @@ def mirror_hdf5_content(
|
|
|
94
99
|
expand=expand_paths,
|
|
95
100
|
recursive=recursive_paths,
|
|
96
101
|
copy=copy,
|
|
97
|
-
|
|
102
|
+
home_original=home_original,
|
|
103
|
+
home_destination=home_destination,
|
|
98
104
|
overwrite=overwrite,
|
|
99
105
|
allow_cross_file=allow_cross_file,
|
|
100
106
|
)
|
|
@@ -103,6 +109,7 @@ def mirror_hdf5_content(
|
|
|
103
109
|
class _ReachedBy(NamedTuple):
|
|
104
110
|
link: Union[None, h5py.HardLink, h5py.SoftLink, h5py.ExternalLink]
|
|
105
111
|
in_scope: bool
|
|
112
|
+
soft_link_target: Optional[str] = None
|
|
106
113
|
|
|
107
114
|
@property
|
|
108
115
|
def is_soft_link_in_scope(self) -> bool:
|
|
@@ -115,17 +122,21 @@ class _ReachedBy(NamedTuple):
|
|
|
115
122
|
)
|
|
116
123
|
|
|
117
124
|
@classmethod
|
|
118
|
-
def from_h5py(
|
|
125
|
+
def from_h5py(
|
|
126
|
+
cls, parent: GroupType, name: str, home_original: DataUrl
|
|
127
|
+
) -> "_ReachedBy":
|
|
119
128
|
link = _get_hdf5_link(parent, name)
|
|
129
|
+
soft_link_target = None
|
|
120
130
|
if isinstance(link, h5py.SoftLink):
|
|
121
|
-
|
|
131
|
+
soft_link_target = _absolute_soft_link_target(parent, link)
|
|
132
|
+
in_scope = _soft_link_in_scope(parent, soft_link_target, home_original)
|
|
122
133
|
elif isinstance(link, h5py.ExternalLink):
|
|
123
134
|
in_scope = False
|
|
124
135
|
elif isinstance(link, h5py.HardLink):
|
|
125
136
|
in_scope = True
|
|
126
137
|
else:
|
|
127
138
|
raise NotImplementedError(f"Unknown HDF5 link type: {type(link)}")
|
|
128
|
-
return _ReachedBy(link, in_scope)
|
|
139
|
+
return _ReachedBy(link, in_scope, soft_link_target)
|
|
129
140
|
|
|
130
141
|
|
|
131
142
|
def _mirror(
|
|
@@ -139,7 +150,8 @@ def _mirror(
|
|
|
139
150
|
expand: Set[str],
|
|
140
151
|
recursive: Set[str],
|
|
141
152
|
copy: bool,
|
|
142
|
-
|
|
153
|
+
home_original: DataUrl,
|
|
154
|
+
home_destination: str,
|
|
143
155
|
overwrite: bool,
|
|
144
156
|
allow_cross_file: bool,
|
|
145
157
|
) -> None:
|
|
@@ -164,12 +176,16 @@ def _mirror(
|
|
|
164
176
|
:param copy: copy instead of link by default at this level (already
|
|
165
177
|
accounts for any :code:`invert` entry applied by an
|
|
166
178
|
ancestor).
|
|
167
|
-
:param
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
:param home_original: file and HDF5 path of the root original; used to
|
|
180
|
+
never physically copy data reached through a link
|
|
181
|
+
into another file (unless :code:`allow_cross_file`),
|
|
182
|
+
and to tell whether a soft link or virtual dataset
|
|
183
|
+
source refers to a path guaranteed to exist in
|
|
184
|
+
:code:`parent` (within :code:`home_original`'s tree)
|
|
185
|
+
or not.
|
|
186
|
+
:param home_destination: absolute HDF5 path of the mirror of
|
|
187
|
+
:code:`home_original`; used to re-target a link
|
|
188
|
+
to a path within :code:`home_original`'s tree.
|
|
173
189
|
:param overwrite: delete a conflicting existing name instead of warning
|
|
174
190
|
and leaving it untouched.
|
|
175
191
|
:param allow_cross_file: allow crossing into a file other than the
|
|
@@ -219,7 +235,7 @@ def _mirror(
|
|
|
219
235
|
# than the one being mirrored: never allowed unless `allow_cross_file`,
|
|
220
236
|
# regardless of what `expand`/`recursive` ask for (see `traverse` below).
|
|
221
237
|
crosses_file = (
|
|
222
|
-
os.path.abspath(_get_hdf5_filename(original.file)) !=
|
|
238
|
+
os.path.abspath(_get_hdf5_filename(original.file)) != home_original.file_path()
|
|
223
239
|
)
|
|
224
240
|
|
|
225
241
|
# Open `original` as a group instead of mirroring it as a single link or
|
|
@@ -243,7 +259,7 @@ def _mirror(
|
|
|
243
259
|
destination = parent.create_group(name)
|
|
244
260
|
_update_attrs(destination, original, overwrite)
|
|
245
261
|
for child_name in original:
|
|
246
|
-
child_reached_by = _ReachedBy.from_h5py(original, child_name,
|
|
262
|
+
child_reached_by = _ReachedBy.from_h5py(original, child_name, home_original)
|
|
247
263
|
try:
|
|
248
264
|
original_child = _resolve_hdf5_group_child(original, child_name)
|
|
249
265
|
except KeyError:
|
|
@@ -262,7 +278,8 @@ def _mirror(
|
|
|
262
278
|
expand=expand,
|
|
263
279
|
recursive=recursive,
|
|
264
280
|
copy=do_copy,
|
|
265
|
-
|
|
281
|
+
home_original=home_original,
|
|
282
|
+
home_destination=home_destination,
|
|
266
283
|
overwrite=overwrite,
|
|
267
284
|
allow_cross_file=allow_cross_file,
|
|
268
285
|
)
|
|
@@ -274,23 +291,35 @@ def _mirror(
|
|
|
274
291
|
# `name` is ensured to not exist in `parent`
|
|
275
292
|
|
|
276
293
|
if not do_copy:
|
|
294
|
+
if reached_by.is_soft_link_in_scope and _create_mirrored_soft_link(
|
|
295
|
+
parent, name, reached_by, home_original, home_destination, exclude
|
|
296
|
+
):
|
|
297
|
+
# Preserve the link faithfully instead of linking to what it
|
|
298
|
+
# points to in the file being mirrored.
|
|
299
|
+
return
|
|
277
300
|
_create_hdf5_link(parent, name, original, overwrite=overwrite)
|
|
278
301
|
return
|
|
279
302
|
|
|
280
303
|
# The content is being copied. First try preserving links faithfully.
|
|
281
304
|
|
|
282
|
-
if reached_by.is_soft_link_in_scope
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
305
|
+
if reached_by.is_soft_link_in_scope and _create_mirrored_soft_link(
|
|
306
|
+
parent, name, reached_by, home_original, home_destination, exclude
|
|
307
|
+
):
|
|
308
|
+
# Preserve the link faithfully instead of copying the data it points to.
|
|
286
309
|
return
|
|
287
310
|
|
|
288
|
-
|
|
289
|
-
|
|
311
|
+
# Virtual datasets and copies are handled with the h5py dataset
|
|
312
|
+
# underneath, when there is one.
|
|
313
|
+
h5py_original = None if is_group else _as_h5py_item(original)
|
|
314
|
+
|
|
315
|
+
is_vds = h5py_original is not None and h5py_original.is_virtual
|
|
316
|
+
if is_vds and _virtual_dataset_in_scope(h5py_original, home_original):
|
|
290
317
|
# Every source of this VDS resolves within the tree being mirrored,
|
|
291
318
|
# which is guaranteed to also exist in `parent`'s file: preserve it
|
|
292
319
|
# faithfully as a VDS.
|
|
293
|
-
if _copy_virtual_dataset(
|
|
320
|
+
if _copy_virtual_dataset(
|
|
321
|
+
parent, name, h5py_original, home_original, home_destination
|
|
322
|
+
):
|
|
294
323
|
return
|
|
295
324
|
|
|
296
325
|
if allow_cross_file:
|
|
@@ -298,8 +327,8 @@ def _mirror(
|
|
|
298
327
|
# Materialize instead of preserving the VDS: the whole point of
|
|
299
328
|
# allow_cross_file is to no longer depend on the file being
|
|
300
329
|
# mirrored, which is the only place this source resolves in.
|
|
301
|
-
dataset = parent.create_dataset(name, data=
|
|
302
|
-
dataset.attrs.update(
|
|
330
|
+
dataset = parent.create_dataset(name, data=h5py_original[()])
|
|
331
|
+
dataset.attrs.update(h5py_original.attrs)
|
|
303
332
|
return
|
|
304
333
|
else:
|
|
305
334
|
# When possible, preserve links faithfully.
|
|
@@ -314,13 +343,15 @@ def _mirror(
|
|
|
314
343
|
return
|
|
315
344
|
if is_vds:
|
|
316
345
|
# Re-create the VDS (no always possible) to recomputed relative file path.
|
|
317
|
-
if _copy_virtual_dataset(
|
|
346
|
+
if _copy_virtual_dataset(
|
|
347
|
+
parent, name, h5py_original, home_original, home_destination
|
|
348
|
+
):
|
|
318
349
|
return
|
|
319
350
|
|
|
320
351
|
# Copy the actual content
|
|
321
352
|
|
|
322
353
|
if not is_group:
|
|
323
|
-
parent
|
|
354
|
+
_copy_hdf5_dataset(parent, name, original, h5py_original)
|
|
324
355
|
return
|
|
325
356
|
|
|
326
357
|
# Recreate the group instead of `h5py.Group.copy` to handle links.
|
|
@@ -337,14 +368,19 @@ def _mirror(
|
|
|
337
368
|
# {""} forces the whole subtree to be traversed (see `in_recursive`).
|
|
338
369
|
recursive={""},
|
|
339
370
|
copy=True,
|
|
340
|
-
|
|
371
|
+
home_original=home_original,
|
|
372
|
+
home_destination=home_destination,
|
|
341
373
|
overwrite=overwrite,
|
|
342
374
|
allow_cross_file=allow_cross_file,
|
|
343
375
|
)
|
|
344
376
|
|
|
345
377
|
|
|
346
378
|
def _copy_virtual_dataset(
|
|
347
|
-
parent: GroupType,
|
|
379
|
+
parent: GroupType,
|
|
380
|
+
name: str,
|
|
381
|
+
original: h5py.Dataset,
|
|
382
|
+
home_original: DataUrl,
|
|
383
|
+
home_destination: str,
|
|
348
384
|
) -> bool:
|
|
349
385
|
"""Try to faithfully copy virtual dataset :code:`original` under :code:`name`
|
|
350
386
|
in :code:`parent`, by reconstructing its virtual sources with paths
|
|
@@ -352,8 +388,10 @@ def _copy_virtual_dataset(
|
|
|
352
388
|
mapping verbatim (which does not adjust source paths when :code:`parent`
|
|
353
389
|
is not in the same directory as :code:`original`).
|
|
354
390
|
|
|
355
|
-
:param
|
|
356
|
-
|
|
391
|
+
:param home_original: file and HDF5 path of the root original being
|
|
392
|
+
mirrored; see :code:`_mirror`.
|
|
393
|
+
:param home_destination: absolute HDF5 path of the mirror of :code:`home_original`;
|
|
394
|
+
see :code:`_mirror`.
|
|
357
395
|
:returns: :code:`False` (nothing created) when a source mapping is not a
|
|
358
396
|
single, whole-source hyperslab, the only shape this can
|
|
359
397
|
faithfully reconstruct; the caller should fall back to a plain
|
|
@@ -369,6 +407,7 @@ def _copy_virtual_dataset(
|
|
|
369
407
|
return False
|
|
370
408
|
mappings.append(mapping)
|
|
371
409
|
|
|
410
|
+
home_original_path = home_original.data_path()
|
|
372
411
|
layout = h5py.VirtualLayout(shape=original.shape, dtype=original.dtype)
|
|
373
412
|
original_filename = _get_hdf5_filename(original.file)
|
|
374
413
|
parent_filename = _get_hdf5_filename(parent.file)
|
|
@@ -376,11 +415,16 @@ def _copy_virtual_dataset(
|
|
|
376
415
|
start, stop = mapping.vspace.get_select_bounds()
|
|
377
416
|
region = tuple(slice(a, b + 1) for a, b in zip(start, stop))
|
|
378
417
|
region_shape = tuple(b - a + 1 for a, b in zip(start, stop))
|
|
418
|
+
vsource_dset_name = mapping.dset_name
|
|
379
419
|
if mapping.file_name == ".":
|
|
380
|
-
if _in_scope(mapping.dset_name,
|
|
381
|
-
#
|
|
382
|
-
#
|
|
420
|
+
if _in_scope(mapping.dset_name, home_original_path):
|
|
421
|
+
# Point to the mirror of the source, which is guaranteed to
|
|
422
|
+
# exist in `parent`'s file.
|
|
383
423
|
vsource_filename = "."
|
|
424
|
+
vsource_dset_name = _normalize_hdf5_item_name(
|
|
425
|
+
home_destination,
|
|
426
|
+
_relative_to_home_original(mapping.dset_name, home_original),
|
|
427
|
+
)
|
|
384
428
|
else:
|
|
385
429
|
# "." pointing outside the tree being mirrored: that path is
|
|
386
430
|
# only guaranteed to exist in `original`'s own file, so point
|
|
@@ -395,7 +439,7 @@ def _copy_virtual_dataset(
|
|
|
395
439
|
parent_filename, original_filename, mapping.file_name
|
|
396
440
|
)
|
|
397
441
|
source = h5py.VirtualSource(
|
|
398
|
-
vsource_filename,
|
|
442
|
+
vsource_filename, vsource_dset_name, shape=region_shape
|
|
399
443
|
)
|
|
400
444
|
layout[region] = source
|
|
401
445
|
parent.create_virtual_dataset(name, layout, fillvalue=original.fillvalue)
|
|
@@ -418,6 +462,21 @@ def _relative_virtual_source_filename(
|
|
|
418
462
|
return os.path.relpath(abs_vsource_filename, abs_parent_dir)
|
|
419
463
|
|
|
420
464
|
|
|
465
|
+
def _copy_hdf5_dataset(
|
|
466
|
+
parent: GroupType,
|
|
467
|
+
name: str,
|
|
468
|
+
original: DatasetType,
|
|
469
|
+
h5py_original: Optional[h5py.Dataset],
|
|
470
|
+
) -> None:
|
|
471
|
+
"""Copy dataset :code:`original` under :code:`name` in :code:`parent`."""
|
|
472
|
+
if h5py_original is not None:
|
|
473
|
+
parent.copy(h5py_original, name)
|
|
474
|
+
else:
|
|
475
|
+
# `h5py.Group.copy` only accepts h5py objects.
|
|
476
|
+
dataset = parent.create_dataset(name, data=original[()])
|
|
477
|
+
dataset.attrs.update(original.attrs)
|
|
478
|
+
|
|
479
|
+
|
|
421
480
|
def _resolve_hdf5_group_child(
|
|
422
481
|
parent: GroupType, name: str
|
|
423
482
|
) -> Union[DatasetType, GroupType]:
|
|
@@ -427,51 +486,115 @@ def _resolve_hdf5_group_child(
|
|
|
427
486
|
after resolving has the correct `:code:.file` but not the correct `:code:.name`.
|
|
428
487
|
"""
|
|
429
488
|
link = _get_hdf5_link(parent, name)
|
|
430
|
-
if not isinstance(link, h5py.SoftLink):
|
|
431
|
-
return parent[name]
|
|
432
489
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
target_path =
|
|
490
|
+
if isinstance(link, h5py.SoftLink):
|
|
491
|
+
# Resolve the target of the soft link instead of resolving the soft
|
|
492
|
+
# link, as the target may be an external link itself.
|
|
493
|
+
target_path = _absolute_soft_link_target(parent, link)
|
|
494
|
+
return _resolve_hdf5_item(parent.file, target_path)
|
|
437
495
|
|
|
438
|
-
|
|
439
|
-
|
|
496
|
+
if isinstance(link, h5py.ExternalLink):
|
|
497
|
+
# An h5py-like object behind an external link has no way of telling
|
|
498
|
+
# which file it belongs to: use the h5py object underneath, which does.
|
|
499
|
+
h5py_parent = _as_h5py_item(parent)
|
|
500
|
+
if h5py_parent is not None:
|
|
501
|
+
return h5py_parent[name]
|
|
440
502
|
|
|
503
|
+
return parent[name]
|
|
441
504
|
|
|
442
|
-
|
|
505
|
+
|
|
506
|
+
def _resolve_hdf5_item(root: GroupType, path: str) -> Union[DatasetType, GroupType]:
|
|
507
|
+
"""Resolve the absolute HDF5 path :code:`path` in :code:`root`, one link at
|
|
508
|
+
a time.
|
|
509
|
+
"""
|
|
510
|
+
item = root
|
|
511
|
+
for name in path.split("/"):
|
|
512
|
+
if name:
|
|
513
|
+
item = _resolve_hdf5_group_child(item, name)
|
|
514
|
+
return item
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _create_mirrored_soft_link(
|
|
443
518
|
parent: GroupType,
|
|
444
|
-
|
|
445
|
-
|
|
519
|
+
name: str,
|
|
520
|
+
reached_by: "_ReachedBy",
|
|
521
|
+
home_original: DataUrl,
|
|
522
|
+
home_destination: str,
|
|
523
|
+
exclude: Set[str],
|
|
446
524
|
) -> bool:
|
|
447
|
-
"""
|
|
448
|
-
|
|
525
|
+
"""Create a soft link under :code:`name` in :code:`parent`, pointing to the
|
|
526
|
+
mirror of the target of the in-scope soft link :code:`reached_by`.
|
|
527
|
+
|
|
528
|
+
:returns: :code:`False` when the target is not mirrored, in which case
|
|
529
|
+
nothing is created and the caller has to fall back to
|
|
530
|
+
referring to the target itself.
|
|
531
|
+
"""
|
|
532
|
+
relative_target = _relative_to_home_original(
|
|
533
|
+
reached_by.soft_link_target, home_original
|
|
534
|
+
)
|
|
535
|
+
if any(
|
|
536
|
+
relative_target == path or relative_target.startswith(path + "/")
|
|
537
|
+
for path in exclude
|
|
538
|
+
):
|
|
449
539
|
return False
|
|
450
|
-
|
|
540
|
+
filename = _get_hdf5_filename(parent.file)
|
|
541
|
+
link = _hdf5_link_object(
|
|
542
|
+
parent_filename=filename,
|
|
543
|
+
parent_name=parent.name,
|
|
544
|
+
link_name=name,
|
|
545
|
+
target_filename=filename,
|
|
546
|
+
target_name=_normalize_hdf5_item_name(home_destination, relative_target),
|
|
547
|
+
)
|
|
548
|
+
if link is not None:
|
|
549
|
+
parent[name] = link
|
|
550
|
+
return True
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _relative_to_home_original(absolute_path: str, home_original: DataUrl) -> str:
|
|
554
|
+
"""HDF5 path of :code:`absolute_path` relative to :code:`home_original`'s path,
|
|
555
|
+
empty for :code:`home_original` itself.
|
|
556
|
+
"""
|
|
557
|
+
relative_path = os.path.relpath(absolute_path, home_original.data_path())
|
|
558
|
+
if relative_path == ".":
|
|
559
|
+
return ""
|
|
560
|
+
return relative_path
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _absolute_soft_link_target(parent: GroupType, link: h5py.SoftLink) -> str:
|
|
564
|
+
"""Absolute HDF5 path of the target of soft link :code:`link` in :code:`parent`."""
|
|
565
|
+
if link.path.startswith("/"):
|
|
566
|
+
return _normalize_hdf5_item_name(link.path)
|
|
567
|
+
return _normalize_hdf5_item_name(parent.name, link.path)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _soft_link_in_scope(
|
|
571
|
+
parent: GroupType, target_path: str, home_original: DataUrl
|
|
572
|
+
) -> bool:
|
|
573
|
+
"""Does the soft link target :code:`target_path` refer to a path within
|
|
574
|
+
:code:`home_original`'s path?
|
|
575
|
+
"""
|
|
576
|
+
if os.path.abspath(_get_hdf5_filename(parent.file)) != home_original.file_path():
|
|
451
577
|
return False
|
|
452
|
-
target_path
|
|
453
|
-
if not target_path.startswith("/"):
|
|
454
|
-
target_path = _normalize_hdf5_item_name(parent.name, target_path)
|
|
455
|
-
return _in_scope(target_path, home.data_path())
|
|
578
|
+
return _in_scope(target_path, home_original.data_path())
|
|
456
579
|
|
|
457
580
|
|
|
458
|
-
def _virtual_dataset_in_scope(original:
|
|
581
|
+
def _virtual_dataset_in_scope(original: h5py.Dataset, home_original: DataUrl) -> bool:
|
|
459
582
|
"""Does every virtual source of :code:`original` refer to a path
|
|
460
|
-
within :code:`
|
|
583
|
+
within :code:`home_original`'s path?
|
|
461
584
|
"""
|
|
462
|
-
|
|
585
|
+
home_original_path = home_original.data_path()
|
|
463
586
|
return all(
|
|
464
|
-
mapping.file_name == "." and _in_scope(mapping.dset_name,
|
|
587
|
+
mapping.file_name == "." and _in_scope(mapping.dset_name, home_original_path)
|
|
465
588
|
for mapping in original.virtual_sources()
|
|
466
589
|
)
|
|
467
590
|
|
|
468
591
|
|
|
469
|
-
def _in_scope(absolute_path: str,
|
|
592
|
+
def _in_scope(absolute_path: str, home_original_path: str) -> bool:
|
|
470
593
|
"""Is :code:`absolute_path` (an absolute HDF5 path) equal to
|
|
471
|
-
:code:`
|
|
594
|
+
:code:`home_original_path` or a descendant of it?
|
|
472
595
|
"""
|
|
473
|
-
prefix =
|
|
474
|
-
return absolute_path ==
|
|
596
|
+
prefix = home_original_path.rstrip("/") + "/"
|
|
597
|
+
return absolute_path == home_original_path or absolute_path.startswith(prefix)
|
|
475
598
|
|
|
476
599
|
|
|
477
600
|
def _create_hdf5_link(
|
|
@@ -596,12 +719,16 @@ def _item_conflict(parent: GroupType, name: str, overwrite: bool) -> bool:
|
|
|
596
719
|
|
|
597
720
|
|
|
598
721
|
def _attr_conflict(
|
|
599
|
-
destination: Union[DatasetType, GroupType], key: str, overwrite: bool
|
|
722
|
+
destination: Union[DatasetType, GroupType], key: str, value: Any, overwrite: bool
|
|
600
723
|
) -> bool:
|
|
601
724
|
"""Returns :code:`True` when these is a conflict and it cannot be resolved."""
|
|
602
725
|
if key not in destination.attrs:
|
|
603
726
|
return False
|
|
604
727
|
|
|
728
|
+
if numpy.array_equal(destination.attrs[key], value):
|
|
729
|
+
# Nothing to overwrite
|
|
730
|
+
return True
|
|
731
|
+
|
|
605
732
|
if not overwrite:
|
|
606
733
|
logger.warning(
|
|
607
734
|
"attribute %s already exists on %s: not overwritten",
|
|
@@ -620,7 +747,7 @@ def _update_attrs(
|
|
|
620
747
|
) -> None:
|
|
621
748
|
"""Copy :code:`original`'s attributes onto :code:`destination`."""
|
|
622
749
|
for key, value in original.attrs.items():
|
|
623
|
-
if
|
|
750
|
+
if _attr_conflict(destination, key, value, overwrite):
|
|
624
751
|
continue
|
|
625
752
|
destination.attrs[key] = value
|
|
626
753
|
|
|
@@ -654,6 +781,20 @@ def _get_hdf5_link(
|
|
|
654
781
|
return h5py.HardLink()
|
|
655
782
|
|
|
656
783
|
|
|
784
|
+
def _as_h5py_item(
|
|
785
|
+
item: Union[DatasetType, GroupType],
|
|
786
|
+
) -> Optional[Union[h5py.Dataset, h5py.Group]]:
|
|
787
|
+
"""The h5py object underlying :code:`item`, :code:`None` when there is none."""
|
|
788
|
+
# Duck-typed so it works for any h5py-like objects.
|
|
789
|
+
if isinstance(item, (h5py.Dataset, h5py.Group)):
|
|
790
|
+
return item
|
|
791
|
+
try:
|
|
792
|
+
# to be fixed in blissdata
|
|
793
|
+
return item._retry_handler.file_obj[item.name]
|
|
794
|
+
except (AttributeError, KeyError):
|
|
795
|
+
return None
|
|
796
|
+
|
|
797
|
+
|
|
657
798
|
def _is_group(item: Union[DatasetType, GroupType]) -> bool:
|
|
658
799
|
# Duck-typed so it works for any h5py-like objects.
|
|
659
800
|
return hasattr(item, "keys")
|