arrayview 0.43.4__tar.gz → 0.45.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.
- {arrayview-0.43.4 → arrayview-0.45.0}/PKG-INFO +1 -1
- {arrayview-0.43.4 → arrayview-0.45.0}/pyproject.toml +1 -1
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_analysis.py +11 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_io.py +49 -6
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_loading.py +144 -130
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_viewer.html +2213 -844
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_vscode_extension.py +1 -1
- arrayview-0.45.0/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/lifecycle_matrix.py +1 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_browser.py +1702 -135
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_lifecycle_contract.py +9 -2
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_mode_consistency.py +486 -6
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_nifti_series.py +31 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/visual_smoke.py +53 -12
- arrayview-0.43.4/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/.gitignore +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/LICENSE +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/README.md +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_diagnostics.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_diff.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_extra_ports.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_launch_plan.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_launch_trace.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_preferences.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_query.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_routes_websocket.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_server.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_session.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_source_safety.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_tutorial.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/_watch.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/src/arrayview/todo.md +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/capture_v_animation.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/conftest.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_1d_arrays.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_api.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_backend_shared.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_cli.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_config.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_dicom.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_display_opener_results.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_doctor_commands.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_drop_import.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_instance_registry.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_invocation_contract_matrix.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_io.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_launch_blackbox.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_launch_diagnostics.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_launch_execution_ownership.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_launch_plan.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_launch_trace.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_loading_server.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_preferences_ui.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_public_invocation_regressions.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_render_failure_reporting.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_render_progress.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_server_identity_status.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_session_spec.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_source_safety.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_spawn_ownership.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_torch.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_tutorial.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_tutorial_browser.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_value_formatting.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_view_handle_lifecycle.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_view_launch_execution.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_vscode_ack_protocol.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/test_window_level_preview.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/ui_audit.py +0 -0
- {arrayview-0.43.4 → arrayview-0.45.0}/tests/v_anim_frames/.gitkeep +0 -0
|
@@ -42,6 +42,11 @@ def _build_metadata(session) -> dict:
|
|
|
42
42
|
# Collection axes are appended after the image dimensions. They
|
|
43
43
|
# select a case; they must not influence the startup plane.
|
|
44
44
|
default_shape = target_shape[:spatial_ndim]
|
|
45
|
+
# NIfTI metadata identifies the first three axes as anatomical.
|
|
46
|
+
# Later axes may be echoes, parameters, or time points, so they
|
|
47
|
+
# must not change which anatomical plane opens first.
|
|
48
|
+
if getattr(session, "spatial_meta", None) is not None:
|
|
49
|
+
default_shape = default_shape[:3]
|
|
45
50
|
default_dims = _session_mod._startup_dims_for_data(session.data, default_shape)
|
|
46
51
|
if default_dims is not None:
|
|
47
52
|
meta["default_dims"] = [int(default_dims[0]), int(default_dims[1])]
|
|
@@ -65,6 +70,12 @@ def _build_metadata(session) -> dict:
|
|
|
65
70
|
[list(shape) for shape in row]
|
|
66
71
|
for row in session.data.ragged_spatial_shapes
|
|
67
72
|
]
|
|
73
|
+
case_voxel_spacings = getattr(session.data, "case_voxel_spacings", None)
|
|
74
|
+
if case_voxel_spacings:
|
|
75
|
+
meta["case_voxel_spacings"] = [
|
|
76
|
+
[list(spacing) for spacing in row]
|
|
77
|
+
for row in case_voxel_spacings
|
|
78
|
+
]
|
|
68
79
|
return meta
|
|
69
80
|
|
|
70
81
|
|
|
@@ -172,7 +172,9 @@ def _nifti_header_with_meta(filepath, *, native=False):
|
|
|
172
172
|
(abs(norm_rot[i, j]) for i in range(3) for j in range(3) if i != j),
|
|
173
173
|
default=0.0,
|
|
174
174
|
)
|
|
175
|
-
canonical_shape = tuple(
|
|
175
|
+
canonical_shape = tuple(
|
|
176
|
+
int(img.shape[int(axis)]) for axis in transform[:, 0]
|
|
177
|
+
) + tuple(int(size) for size in img.shape[3:])
|
|
176
178
|
meta = {
|
|
177
179
|
"affine": original_affine,
|
|
178
180
|
"affine_canonical": affine_canonical,
|
|
@@ -281,6 +283,11 @@ class _ProgressiveNiftiJob:
|
|
|
281
283
|
if self.future is not None and self.future.cancel():
|
|
282
284
|
self._finish(cancelled=True)
|
|
283
285
|
|
|
286
|
+
def has_plane(self, z_index) -> bool:
|
|
287
|
+
"""True if *z_index* is already decoded (a non-blocking peek)."""
|
|
288
|
+
with self._condition:
|
|
289
|
+
return bool(self._decoded[z_index]) or self._done
|
|
290
|
+
|
|
284
291
|
def plane(self, z_index):
|
|
285
292
|
with self._condition:
|
|
286
293
|
while not self._decoded[z_index] and not self._done:
|
|
@@ -440,7 +447,8 @@ class _ProgressiveNiftiSeriesMixin:
|
|
|
440
447
|
return None
|
|
441
448
|
with self._progressive_lock:
|
|
442
449
|
job = self._progressive_jobs.get(cache_key)
|
|
443
|
-
|
|
450
|
+
is_new_job = job is None
|
|
451
|
+
if is_new_job:
|
|
444
452
|
spec = _progressive_nifti_spec(filepath)
|
|
445
453
|
if spec is None:
|
|
446
454
|
return None
|
|
@@ -469,7 +477,18 @@ class _ProgressiveNiftiSeriesMixin:
|
|
|
469
477
|
overlay_prefetch=overlay_prefetch
|
|
470
478
|
).submit(job.run)
|
|
471
479
|
try:
|
|
472
|
-
|
|
480
|
+
# The request that starts the job waits on just the one plane, so
|
|
481
|
+
# the first frame of a fresh file shows up as soon as the decoder
|
|
482
|
+
# reaches it rather than waiting for the whole thing. Every later
|
|
483
|
+
# request for this same file instead waits for the whole volume
|
|
484
|
+
# (unless its plane is already decoded): scrolling through every
|
|
485
|
+
# plane that way finishes in less total time than paying a small
|
|
486
|
+
# wait on each individual plane (~200ms vs ~315ms measured on a
|
|
487
|
+
# 75-plane volume), so it also keeps later frames from feeling
|
|
488
|
+
# slower than the ones on the other two axes.
|
|
489
|
+
if is_new_job or job.has_plane(z_index):
|
|
490
|
+
return job.plane(z_index)
|
|
491
|
+
return job.volume()[:, :, z_index]
|
|
473
492
|
except _ProgressiveLoadCancelled:
|
|
474
493
|
raise
|
|
475
494
|
except Exception:
|
|
@@ -489,11 +508,12 @@ class _NiftiSeries(_ProgressiveNiftiSeriesMixin):
|
|
|
489
508
|
|
|
490
509
|
_av_lazy = True
|
|
491
510
|
|
|
492
|
-
def __init__(self, file_matrix, vol_shape, dtype, spatial_meta=None):
|
|
511
|
+
def __init__(self, file_matrix, vol_shape, dtype, spatial_meta=None, spacing_matrix=None):
|
|
493
512
|
self._file_matrix = file_matrix
|
|
494
513
|
self._vol_shape = tuple(vol_shape)
|
|
495
514
|
self._dtype = np.dtype(dtype)
|
|
496
515
|
self._spatial_meta = spatial_meta
|
|
516
|
+
self._spacing_matrix = spacing_matrix
|
|
497
517
|
n_patients = len(file_matrix)
|
|
498
518
|
n_modalities = len(file_matrix[0]) if file_matrix else 0
|
|
499
519
|
if n_modalities <= 1:
|
|
@@ -516,6 +536,10 @@ class _NiftiSeries(_ProgressiveNiftiSeriesMixin):
|
|
|
516
536
|
def dtype(self):
|
|
517
537
|
return self._dtype
|
|
518
538
|
|
|
539
|
+
@property
|
|
540
|
+
def case_voxel_spacings(self):
|
|
541
|
+
return self._spacing_matrix
|
|
542
|
+
|
|
519
543
|
def _get_volume(self, p_idx, m_idx):
|
|
520
544
|
cache_key = (p_idx, m_idx)
|
|
521
545
|
with self._cache_lock:
|
|
@@ -709,7 +733,10 @@ class _RaggedFileSeries(_ProgressiveNiftiSeriesMixin):
|
|
|
709
733
|
_av_lazy = True
|
|
710
734
|
_av_ragged = True
|
|
711
735
|
|
|
712
|
-
def __init__(
|
|
736
|
+
def __init__(
|
|
737
|
+
self, file_matrix, spatial_shapes, dtype, spatial_meta=None, all_nifti=False,
|
|
738
|
+
spacing_matrix=None,
|
|
739
|
+
):
|
|
713
740
|
self._file_matrix = file_matrix
|
|
714
741
|
self._spatial_shapes = [
|
|
715
742
|
[tuple(int(s) for s in shape) for shape in row]
|
|
@@ -717,6 +744,7 @@ class _RaggedFileSeries(_ProgressiveNiftiSeriesMixin):
|
|
|
717
744
|
]
|
|
718
745
|
self._dtype = np.dtype(dtype)
|
|
719
746
|
self._spatial_meta = spatial_meta
|
|
747
|
+
self._spacing_matrix = spacing_matrix
|
|
720
748
|
self._all_nifti = bool(all_nifti)
|
|
721
749
|
self._spatial_ndim = len(self._spatial_shapes[0][0])
|
|
722
750
|
self._vol_shape = tuple(
|
|
@@ -753,6 +781,10 @@ class _RaggedFileSeries(_ProgressiveNiftiSeriesMixin):
|
|
|
753
781
|
def ragged_spatial_shapes(self):
|
|
754
782
|
return self._spatial_shapes
|
|
755
783
|
|
|
784
|
+
@property
|
|
785
|
+
def case_voxel_spacings(self):
|
|
786
|
+
return self._spacing_matrix
|
|
787
|
+
|
|
756
788
|
def _normalize_collection_indices(self, idx_list):
|
|
757
789
|
n_p = len(self._file_matrix)
|
|
758
790
|
n_m = len(self._file_matrix[0]) if self._file_matrix else 0
|
|
@@ -990,12 +1022,14 @@ def _series_from_file_matrix(
|
|
|
990
1022
|
|
|
991
1023
|
if all_nifti:
|
|
992
1024
|
spatial_shapes = []
|
|
1025
|
+
voxel_sizes_list = []
|
|
993
1026
|
for fpath, img, item_meta in _iter_nifti_headers(
|
|
994
1027
|
all_paths, scan_progress=scan_progress, scan_label=scan_label
|
|
995
1028
|
):
|
|
996
1029
|
shape = tuple(item_meta["canonical_shape"])
|
|
997
1030
|
dtype = img.get_data_dtype()
|
|
998
1031
|
spatial_shapes.append(shape)
|
|
1032
|
+
voxel_sizes_list.append(tuple(item_meta["voxel_sizes"]))
|
|
999
1033
|
if ref_shape is None:
|
|
1000
1034
|
ref_shape = shape
|
|
1001
1035
|
ref_dtype = dtype
|
|
@@ -1011,16 +1045,21 @@ def _series_from_file_matrix(
|
|
|
1011
1045
|
f"{dtype}, expected {ref_dtype}."
|
|
1012
1046
|
)
|
|
1013
1047
|
shape_matrix = []
|
|
1048
|
+
spacing_matrix = []
|
|
1014
1049
|
pos = 0
|
|
1015
1050
|
for row in file_matrix:
|
|
1016
1051
|
row_shapes = []
|
|
1052
|
+
row_spacings = []
|
|
1017
1053
|
for path in row:
|
|
1018
1054
|
if path is None:
|
|
1019
1055
|
row_shapes.append(ref_shape)
|
|
1056
|
+
row_spacings.append(voxel_sizes_list[0] if voxel_sizes_list else (1.0, 1.0, 1.0))
|
|
1020
1057
|
else:
|
|
1021
1058
|
row_shapes.append(spatial_shapes[pos])
|
|
1059
|
+
row_spacings.append(voxel_sizes_list[pos])
|
|
1022
1060
|
pos += 1
|
|
1023
1061
|
shape_matrix.append(row_shapes)
|
|
1062
|
+
spacing_matrix.append(row_spacings)
|
|
1024
1063
|
shapes_differ = any(shape != ref_shape for shape in spatial_shapes)
|
|
1025
1064
|
if stack == "dense" and shapes_differ:
|
|
1026
1065
|
raise ValueError("Dense stacking requires every file to have the same shape.")
|
|
@@ -1031,9 +1070,13 @@ def _series_from_file_matrix(
|
|
|
1031
1070
|
ref_dtype,
|
|
1032
1071
|
spatial_meta=spatial_meta,
|
|
1033
1072
|
all_nifti=True,
|
|
1073
|
+
spacing_matrix=spacing_matrix,
|
|
1034
1074
|
)
|
|
1035
1075
|
else:
|
|
1036
|
-
series = _NiftiSeries(
|
|
1076
|
+
series = _NiftiSeries(
|
|
1077
|
+
file_matrix, ref_shape, ref_dtype, spatial_meta,
|
|
1078
|
+
spacing_matrix=spacing_matrix,
|
|
1079
|
+
)
|
|
1037
1080
|
if load == "eager":
|
|
1038
1081
|
for p, row in enumerate(file_matrix):
|
|
1039
1082
|
for m, path in enumerate(row):
|
|
@@ -401,148 +401,162 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
401
401
|
"pending": True,
|
|
402
402
|
}
|
|
403
403
|
signature_before_load = file_signature(abs_path) if not dir_patterns else None
|
|
404
|
+
# A collection scan+load runs synchronously on this request (no
|
|
405
|
+
# "background" fast path — see the `not dir_patterns` guard above),
|
|
406
|
+
# but the caller already navigates the browser to `requested_sid`
|
|
407
|
+
# before this returns. Without marking the sid pending here, the
|
|
408
|
+
# viewer's very first phase report can 404 (sid known to neither
|
|
409
|
+
# SESSIONS nor PENDING_SESSIONS yet) and the opener misreports launch
|
|
410
|
+
# as failed even though the load goes on to succeed.
|
|
411
|
+
dir_pending_sid = requested_sid if (dir_patterns and requested_sid) else None
|
|
412
|
+
if dir_pending_sid:
|
|
413
|
+
PENDING_SESSIONS.add(dir_pending_sid)
|
|
404
414
|
try:
|
|
405
|
-
from ._io import load_data_with_meta, load_dir_collection, list_array_keys
|
|
406
|
-
|
|
407
|
-
# Multi-array .npz/.mat: if no key provided, return the key list so
|
|
408
|
-
# the client can show a picker instead of blocking on terminal input.
|
|
409
|
-
_key = body.get("key")
|
|
410
|
-
_array_keys = None
|
|
411
|
-
if dir_patterns:
|
|
412
|
-
data, spatial_meta, dir_overlay_items, summary = await asyncio.to_thread(
|
|
413
|
-
load_dir_collection,
|
|
414
|
-
list(dir_patterns),
|
|
415
|
-
overlays=[tuple(item) for item in body.get("dir_overlay_specs", [])],
|
|
416
|
-
case_regex=body.get("dir_case_regex"),
|
|
417
|
-
exclude_cases=body.get("dir_exclude_cases", []),
|
|
418
|
-
load=body.get("load", "lazy"),
|
|
419
|
-
stack=body.get("stack", "auto"),
|
|
420
|
-
)
|
|
421
|
-
elif filepath.endswith(".npz") or filepath.endswith(".mat"):
|
|
422
|
-
_array_keys = await asyncio.to_thread(list_array_keys, filepath)
|
|
423
|
-
if len(_array_keys) > 1 and not _key:
|
|
424
|
-
return {"array_keys": _array_keys, "filepath": filepath}
|
|
425
|
-
if len(_array_keys) == 1 and not _key:
|
|
426
|
-
_key = _array_keys[0]["key"]
|
|
427
|
-
|
|
428
|
-
data, spatial_meta = await asyncio.to_thread(
|
|
429
|
-
load_data_with_meta,
|
|
430
|
-
filepath,
|
|
431
|
-
key=_key,
|
|
432
|
-
select=body.get("select"),
|
|
433
|
-
) if not dir_patterns else (data, spatial_meta)
|
|
434
|
-
except Exception as e:
|
|
435
|
-
if staging_dir:
|
|
436
|
-
cleanup_staging_directory(staging_dir)
|
|
437
|
-
return {"error": str(e)}
|
|
438
|
-
try:
|
|
439
|
-
session = await asyncio.to_thread(
|
|
440
|
-
Session, data, filepath=None if dir_patterns else filepath, name=name
|
|
441
|
-
)
|
|
442
|
-
except Exception as e:
|
|
443
|
-
if staging_dir:
|
|
444
|
-
cleanup_staging_directory(staging_dir)
|
|
445
|
-
return {"error": str(e)}
|
|
446
|
-
if requested_sid:
|
|
447
|
-
session.sid = requested_sid
|
|
448
|
-
session.release_on_disconnect = bool(
|
|
449
|
-
body.get("release_on_disconnect", False)
|
|
450
|
-
)
|
|
451
|
-
session.related_release_sids = [
|
|
452
|
-
str(value) for value in body.get("related_sids", [])
|
|
453
|
-
]
|
|
454
|
-
if staging_dir:
|
|
455
|
-
session._source_staging_dirs = [str(staging_dir)]
|
|
456
|
-
if not dir_patterns:
|
|
457
415
|
try:
|
|
458
|
-
|
|
416
|
+
from ._io import load_data_with_meta, load_dir_collection, list_array_keys
|
|
417
|
+
|
|
418
|
+
# Multi-array .npz/.mat: if no key provided, return the key list so
|
|
419
|
+
# the client can show a picker instead of blocking on terminal input.
|
|
420
|
+
_key = body.get("key")
|
|
421
|
+
_array_keys = None
|
|
422
|
+
if dir_patterns:
|
|
423
|
+
data, spatial_meta, dir_overlay_items, summary = await asyncio.to_thread(
|
|
424
|
+
load_dir_collection,
|
|
425
|
+
list(dir_patterns),
|
|
426
|
+
overlays=[tuple(item) for item in body.get("dir_overlay_specs", [])],
|
|
427
|
+
case_regex=body.get("dir_case_regex"),
|
|
428
|
+
exclude_cases=body.get("dir_exclude_cases", []),
|
|
429
|
+
load=body.get("load", "lazy"),
|
|
430
|
+
stack=body.get("stack", "auto"),
|
|
431
|
+
)
|
|
432
|
+
elif filepath.endswith(".npz") or filepath.endswith(".mat"):
|
|
433
|
+
_array_keys = await asyncio.to_thread(list_array_keys, filepath)
|
|
434
|
+
if len(_array_keys) > 1 and not _key:
|
|
435
|
+
return {"array_keys": _array_keys, "filepath": filepath}
|
|
436
|
+
if len(_array_keys) == 1 and not _key:
|
|
437
|
+
_key = _array_keys[0]["key"]
|
|
438
|
+
|
|
439
|
+
data, spatial_meta = await asyncio.to_thread(
|
|
440
|
+
load_data_with_meta,
|
|
441
|
+
filepath,
|
|
442
|
+
key=_key,
|
|
443
|
+
select=body.get("select"),
|
|
444
|
+
) if not dir_patterns else (data, spatial_meta)
|
|
459
445
|
except Exception as e:
|
|
460
446
|
if staging_dir:
|
|
461
447
|
cleanup_staging_directory(staging_dir)
|
|
462
448
|
return {"error": str(e)}
|
|
463
|
-
if signature_before_load == signature_after_load:
|
|
464
|
-
session.file_signature = signature_after_load
|
|
465
|
-
if dir_patterns:
|
|
466
|
-
session.collection_spatial_ndim = len(summary["spatial_shape"])
|
|
467
|
-
session.collection_identity = collection_identity
|
|
468
|
-
if spatial_meta is not None:
|
|
469
|
-
session.spatial_meta = spatial_meta
|
|
470
|
-
session.original_volume = data
|
|
471
|
-
if _array_keys and len(_array_keys) > 1:
|
|
472
|
-
session.array_keys = _array_keys
|
|
473
|
-
session.array_filepath = filepath
|
|
474
|
-
if body.get("rgb"):
|
|
475
449
|
try:
|
|
476
|
-
await asyncio.to_thread(
|
|
477
|
-
|
|
478
|
-
if staging_dir:
|
|
479
|
-
cleanup_staging_directory(staging_dir)
|
|
480
|
-
return {"error": str(e)}
|
|
481
|
-
overlay_sids = []
|
|
482
|
-
overlay_names = []
|
|
483
|
-
overlay_sessions = []
|
|
484
|
-
for item in dir_overlay_items if dir_patterns else []:
|
|
485
|
-
try:
|
|
486
|
-
overlay_session = await asyncio.to_thread(
|
|
487
|
-
Session, item["data"], filepath=None, name=item["name"]
|
|
450
|
+
session = await asyncio.to_thread(
|
|
451
|
+
Session, data, filepath=None if dir_patterns else filepath, name=name
|
|
488
452
|
)
|
|
489
453
|
except Exception as e:
|
|
490
454
|
if staging_dir:
|
|
491
455
|
cleanup_staging_directory(staging_dir)
|
|
492
456
|
return {"error": str(e)}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
session.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
457
|
+
if requested_sid:
|
|
458
|
+
session.sid = requested_sid
|
|
459
|
+
session.release_on_disconnect = bool(
|
|
460
|
+
body.get("release_on_disconnect", False)
|
|
461
|
+
)
|
|
462
|
+
session.related_release_sids = [
|
|
463
|
+
str(value) for value in body.get("related_sids", [])
|
|
464
|
+
]
|
|
465
|
+
if staging_dir:
|
|
466
|
+
session._source_staging_dirs = [str(staging_dir)]
|
|
467
|
+
if not dir_patterns:
|
|
468
|
+
try:
|
|
469
|
+
signature_after_load = file_signature(abs_path)
|
|
470
|
+
except Exception as e:
|
|
471
|
+
if staging_dir:
|
|
472
|
+
cleanup_staging_directory(staging_dir)
|
|
473
|
+
return {"error": str(e)}
|
|
474
|
+
if signature_before_load == signature_after_load:
|
|
475
|
+
session.file_signature = signature_after_load
|
|
476
|
+
if dir_patterns:
|
|
477
|
+
session.collection_spatial_ndim = len(summary["spatial_shape"])
|
|
478
|
+
session.collection_identity = collection_identity
|
|
479
|
+
if spatial_meta is not None:
|
|
480
|
+
session.spatial_meta = spatial_meta
|
|
481
|
+
session.original_volume = data
|
|
482
|
+
if _array_keys and len(_array_keys) > 1:
|
|
483
|
+
session.array_keys = _array_keys
|
|
484
|
+
session.array_filepath = filepath
|
|
485
|
+
if body.get("rgb"):
|
|
486
|
+
try:
|
|
487
|
+
await asyncio.to_thread(setup_rgb, session)
|
|
488
|
+
except ValueError as e:
|
|
489
|
+
if staging_dir:
|
|
490
|
+
cleanup_staging_directory(staging_dir)
|
|
491
|
+
return {"error": str(e)}
|
|
492
|
+
overlay_sids = []
|
|
493
|
+
overlay_names = []
|
|
494
|
+
overlay_sessions = []
|
|
495
|
+
for item in dir_overlay_items if dir_patterns else []:
|
|
496
|
+
try:
|
|
497
|
+
overlay_session = await asyncio.to_thread(
|
|
498
|
+
Session, item["data"], filepath=None, name=item["name"]
|
|
499
|
+
)
|
|
500
|
+
except Exception as e:
|
|
501
|
+
if staging_dir:
|
|
502
|
+
cleanup_staging_directory(staging_dir)
|
|
503
|
+
return {"error": str(e)}
|
|
504
|
+
overlay_sessions.append(overlay_session)
|
|
505
|
+
overlay_sids.append(overlay_session.sid)
|
|
506
|
+
overlay_names.append(item["name"])
|
|
507
|
+
if dir_patterns:
|
|
508
|
+
session.collection_overlay_sids = list(overlay_sids)
|
|
509
|
+
session.collection_overlay_names = list(overlay_names)
|
|
510
|
+
if requested_sid:
|
|
511
|
+
if not commit_session_group_unless_cancelled(
|
|
512
|
+
session.sid,
|
|
513
|
+
[session, *overlay_sessions],
|
|
514
|
+
):
|
|
515
|
+
CANCELLED_PENDING_SESSIONS.discard(session.sid)
|
|
516
|
+
if staging_dir:
|
|
517
|
+
cleanup_staging_directory(staging_dir)
|
|
518
|
+
return {"error": "load request was cancelled"}
|
|
519
|
+
else:
|
|
520
|
+
SESSIONS[session.sid] = session
|
|
521
|
+
for overlay_session in overlay_sessions:
|
|
522
|
+
SESSIONS[overlay_session.sid] = overlay_session
|
|
523
|
+
if body.get("watch"):
|
|
524
|
+
import arrayview._session as _session_mod
|
|
525
|
+
from ._watch import attach_file_watch
|
|
526
|
+
|
|
527
|
+
attach_file_watch(session, filepath, _session_mod.SERVER_PORT or 8000)
|
|
528
|
+
notified = False
|
|
529
|
+
if notify:
|
|
530
|
+
tab_url = None
|
|
531
|
+
if body.get("compare_sids"):
|
|
532
|
+
tab_url = (
|
|
533
|
+
f"/?sid={session.sid}"
|
|
534
|
+
f"&compare_sid={body['compare_sid']}"
|
|
535
|
+
f"&compare_sids={body['compare_sids']}"
|
|
536
|
+
)
|
|
537
|
+
if overlay_sids:
|
|
538
|
+
tab_url = (
|
|
539
|
+
f"/?sid={session.sid}"
|
|
540
|
+
f"&overlay_sid={','.join(overlay_sids)}"
|
|
541
|
+
f"&overlay_names={urllib.parse.quote(','.join(overlay_names))}"
|
|
542
|
+
)
|
|
543
|
+
if native_request_id:
|
|
544
|
+
tab_url = tab_url or f"/?sid={session.sid}"
|
|
545
|
+
tab_url += (
|
|
546
|
+
"&native_request_id="
|
|
547
|
+
+ urllib.parse.quote(native_request_id)
|
|
548
|
+
)
|
|
549
|
+
notified = await notify_shells(session.sid, name, url=tab_url, wait=False)
|
|
550
|
+
return {
|
|
551
|
+
"sid": session.sid,
|
|
552
|
+
"name": name,
|
|
553
|
+
"notified": notified,
|
|
554
|
+
"overlay_sids": overlay_sids,
|
|
555
|
+
"overlay_names": overlay_names,
|
|
556
|
+
}
|
|
557
|
+
finally:
|
|
558
|
+
if dir_pending_sid:
|
|
559
|
+
PENDING_SESSIONS.discard(dir_pending_sid)
|
|
546
560
|
|
|
547
561
|
@app.post("/notify/{sid}")
|
|
548
562
|
async def notify_existing_session(sid: str, request: Request):
|