arrayview 0.44.0__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.44.0 → arrayview-0.45.0}/PKG-INFO +1 -1
- {arrayview-0.44.0 → arrayview-0.45.0}/pyproject.toml +1 -1
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_analysis.py +6 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_io.py +46 -5
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_loading.py +144 -130
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_viewer.html +1074 -325
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_browser.py +1137 -24
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_mode_consistency.py +43 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/.gitignore +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/LICENSE +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/README.md +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_diagnostics.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_diff.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_extra_ports.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_launch_plan.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_launch_trace.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_preferences.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_query.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_routes_websocket.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_server.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_session.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_source_safety.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_tutorial.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_vscode_extension.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/_watch.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/src/arrayview/todo.md +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/capture_v_animation.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/conftest.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/lifecycle_matrix.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_1d_arrays.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_api.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_backend_shared.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_cli.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_config.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_dicom.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_display_opener_results.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_doctor_commands.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_drop_import.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_instance_registry.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_invocation_contract_matrix.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_io.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_launch_blackbox.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_launch_diagnostics.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_launch_execution_ownership.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_launch_plan.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_launch_trace.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_lifecycle_contract.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_loading_server.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_nifti_series.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_preferences_ui.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_public_invocation_regressions.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_render_failure_reporting.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_render_progress.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_server_identity_status.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_session_spec.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_source_safety.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_spawn_ownership.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_torch.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_tutorial.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_tutorial_browser.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_value_formatting.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_view_handle_lifecycle.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_view_launch_execution.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_vscode_ack_protocol.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/test_window_level_preview.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/ui_audit.py +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.44.0 → arrayview-0.45.0}/tests/visual_smoke.py +0 -0
|
@@ -70,6 +70,12 @@ def _build_metadata(session) -> dict:
|
|
|
70
70
|
[list(shape) for shape in row]
|
|
71
71
|
for row in session.data.ragged_spatial_shapes
|
|
72
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
|
+
]
|
|
73
79
|
return meta
|
|
74
80
|
|
|
75
81
|
|
|
@@ -283,6 +283,11 @@ class _ProgressiveNiftiJob:
|
|
|
283
283
|
if self.future is not None and self.future.cancel():
|
|
284
284
|
self._finish(cancelled=True)
|
|
285
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
|
+
|
|
286
291
|
def plane(self, z_index):
|
|
287
292
|
with self._condition:
|
|
288
293
|
while not self._decoded[z_index] and not self._done:
|
|
@@ -442,7 +447,8 @@ class _ProgressiveNiftiSeriesMixin:
|
|
|
442
447
|
return None
|
|
443
448
|
with self._progressive_lock:
|
|
444
449
|
job = self._progressive_jobs.get(cache_key)
|
|
445
|
-
|
|
450
|
+
is_new_job = job is None
|
|
451
|
+
if is_new_job:
|
|
446
452
|
spec = _progressive_nifti_spec(filepath)
|
|
447
453
|
if spec is None:
|
|
448
454
|
return None
|
|
@@ -471,7 +477,18 @@ class _ProgressiveNiftiSeriesMixin:
|
|
|
471
477
|
overlay_prefetch=overlay_prefetch
|
|
472
478
|
).submit(job.run)
|
|
473
479
|
try:
|
|
474
|
-
|
|
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]
|
|
475
492
|
except _ProgressiveLoadCancelled:
|
|
476
493
|
raise
|
|
477
494
|
except Exception:
|
|
@@ -491,11 +508,12 @@ class _NiftiSeries(_ProgressiveNiftiSeriesMixin):
|
|
|
491
508
|
|
|
492
509
|
_av_lazy = True
|
|
493
510
|
|
|
494
|
-
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):
|
|
495
512
|
self._file_matrix = file_matrix
|
|
496
513
|
self._vol_shape = tuple(vol_shape)
|
|
497
514
|
self._dtype = np.dtype(dtype)
|
|
498
515
|
self._spatial_meta = spatial_meta
|
|
516
|
+
self._spacing_matrix = spacing_matrix
|
|
499
517
|
n_patients = len(file_matrix)
|
|
500
518
|
n_modalities = len(file_matrix[0]) if file_matrix else 0
|
|
501
519
|
if n_modalities <= 1:
|
|
@@ -518,6 +536,10 @@ class _NiftiSeries(_ProgressiveNiftiSeriesMixin):
|
|
|
518
536
|
def dtype(self):
|
|
519
537
|
return self._dtype
|
|
520
538
|
|
|
539
|
+
@property
|
|
540
|
+
def case_voxel_spacings(self):
|
|
541
|
+
return self._spacing_matrix
|
|
542
|
+
|
|
521
543
|
def _get_volume(self, p_idx, m_idx):
|
|
522
544
|
cache_key = (p_idx, m_idx)
|
|
523
545
|
with self._cache_lock:
|
|
@@ -711,7 +733,10 @@ class _RaggedFileSeries(_ProgressiveNiftiSeriesMixin):
|
|
|
711
733
|
_av_lazy = True
|
|
712
734
|
_av_ragged = True
|
|
713
735
|
|
|
714
|
-
def __init__(
|
|
736
|
+
def __init__(
|
|
737
|
+
self, file_matrix, spatial_shapes, dtype, spatial_meta=None, all_nifti=False,
|
|
738
|
+
spacing_matrix=None,
|
|
739
|
+
):
|
|
715
740
|
self._file_matrix = file_matrix
|
|
716
741
|
self._spatial_shapes = [
|
|
717
742
|
[tuple(int(s) for s in shape) for shape in row]
|
|
@@ -719,6 +744,7 @@ class _RaggedFileSeries(_ProgressiveNiftiSeriesMixin):
|
|
|
719
744
|
]
|
|
720
745
|
self._dtype = np.dtype(dtype)
|
|
721
746
|
self._spatial_meta = spatial_meta
|
|
747
|
+
self._spacing_matrix = spacing_matrix
|
|
722
748
|
self._all_nifti = bool(all_nifti)
|
|
723
749
|
self._spatial_ndim = len(self._spatial_shapes[0][0])
|
|
724
750
|
self._vol_shape = tuple(
|
|
@@ -755,6 +781,10 @@ class _RaggedFileSeries(_ProgressiveNiftiSeriesMixin):
|
|
|
755
781
|
def ragged_spatial_shapes(self):
|
|
756
782
|
return self._spatial_shapes
|
|
757
783
|
|
|
784
|
+
@property
|
|
785
|
+
def case_voxel_spacings(self):
|
|
786
|
+
return self._spacing_matrix
|
|
787
|
+
|
|
758
788
|
def _normalize_collection_indices(self, idx_list):
|
|
759
789
|
n_p = len(self._file_matrix)
|
|
760
790
|
n_m = len(self._file_matrix[0]) if self._file_matrix else 0
|
|
@@ -992,12 +1022,14 @@ def _series_from_file_matrix(
|
|
|
992
1022
|
|
|
993
1023
|
if all_nifti:
|
|
994
1024
|
spatial_shapes = []
|
|
1025
|
+
voxel_sizes_list = []
|
|
995
1026
|
for fpath, img, item_meta in _iter_nifti_headers(
|
|
996
1027
|
all_paths, scan_progress=scan_progress, scan_label=scan_label
|
|
997
1028
|
):
|
|
998
1029
|
shape = tuple(item_meta["canonical_shape"])
|
|
999
1030
|
dtype = img.get_data_dtype()
|
|
1000
1031
|
spatial_shapes.append(shape)
|
|
1032
|
+
voxel_sizes_list.append(tuple(item_meta["voxel_sizes"]))
|
|
1001
1033
|
if ref_shape is None:
|
|
1002
1034
|
ref_shape = shape
|
|
1003
1035
|
ref_dtype = dtype
|
|
@@ -1013,16 +1045,21 @@ def _series_from_file_matrix(
|
|
|
1013
1045
|
f"{dtype}, expected {ref_dtype}."
|
|
1014
1046
|
)
|
|
1015
1047
|
shape_matrix = []
|
|
1048
|
+
spacing_matrix = []
|
|
1016
1049
|
pos = 0
|
|
1017
1050
|
for row in file_matrix:
|
|
1018
1051
|
row_shapes = []
|
|
1052
|
+
row_spacings = []
|
|
1019
1053
|
for path in row:
|
|
1020
1054
|
if path is None:
|
|
1021
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))
|
|
1022
1057
|
else:
|
|
1023
1058
|
row_shapes.append(spatial_shapes[pos])
|
|
1059
|
+
row_spacings.append(voxel_sizes_list[pos])
|
|
1024
1060
|
pos += 1
|
|
1025
1061
|
shape_matrix.append(row_shapes)
|
|
1062
|
+
spacing_matrix.append(row_spacings)
|
|
1026
1063
|
shapes_differ = any(shape != ref_shape for shape in spatial_shapes)
|
|
1027
1064
|
if stack == "dense" and shapes_differ:
|
|
1028
1065
|
raise ValueError("Dense stacking requires every file to have the same shape.")
|
|
@@ -1033,9 +1070,13 @@ def _series_from_file_matrix(
|
|
|
1033
1070
|
ref_dtype,
|
|
1034
1071
|
spatial_meta=spatial_meta,
|
|
1035
1072
|
all_nifti=True,
|
|
1073
|
+
spacing_matrix=spacing_matrix,
|
|
1036
1074
|
)
|
|
1037
1075
|
else:
|
|
1038
|
-
series = _NiftiSeries(
|
|
1076
|
+
series = _NiftiSeries(
|
|
1077
|
+
file_matrix, ref_shape, ref_dtype, spatial_meta,
|
|
1078
|
+
spacing_matrix=spacing_matrix,
|
|
1079
|
+
)
|
|
1039
1080
|
if load == "eager":
|
|
1040
1081
|
for p, row in enumerate(file_matrix):
|
|
1041
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):
|