diff --git a/lib/galaxy/authnz/psa_authnz.py b/lib/galaxy/authnz/psa_authnz.py
index fd6faaec032..c6706a51de4 100644
--- a/lib/galaxy/authnz/psa_authnz.py
+++ b/lib/galaxy/authnz/psa_authnz.py
@@ -225,8 +225,8 @@ class PSAAuthnz(IdentityProvider):
return (
extra_data.get("expires", None)
or extra_data.get("expires_in", None)
- or extra_data["refresh_token"].get("expires", None)
- or extra_data["refresh_token"].get("expires_in", None)
+ or (extra_data.get("refresh_token") or {}).get("expires", None)
+ or (extra_data.get("refresh_token") or {}).get("expires_in", None)
)
def authenticate(self, trans, idphint=None):
diff --git a/lib/galaxy/config/sample/datatypes_conf.xml.sample b/lib/galaxy/config/sample/datatypes_conf.xml.sample
index 1584410fdc7..235f86b135a 100644
--- a/lib/galaxy/config/sample/datatypes_conf.xml.sample
+++ b/lib/galaxy/config/sample/datatypes_conf.xml.sample
@@ -1190,6 +1190,7 @@
+
@@ -1297,6 +1298,7 @@
+
diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py
index bc8da6d4797..a0d5da2da4e 100644
--- a/lib/galaxy/datatypes/binary.py
+++ b/lib/galaxy/datatypes/binary.py
@@ -487,6 +487,12 @@ class CompressedZarrZipArchive(CompressedZipArchive):
dataset.metadata.zarr_format = format_version
def sniff(self, filename: str) -> bool:
+ """
+ >>> from galaxy.datatypes.sniff import get_test_fname
+ >>> fname = get_test_fname('Images.zarr.zip')
+ >>> CompressedZarrZipArchive().sniff(fname)
+ True
+ """
# Check if the zip file contains a zarr store.
# In theory, the zarr store must be in the root of the zip file.
# See: https://github.com/zarr-developers/zarr-python/issues/756#issuecomment-852134901
@@ -4874,6 +4880,322 @@ class Hic(Binary):
dataset.metadata.version = struct.unpack(" None:
+ if not dataset.dataset.purged:
+ # Try to make a metadata like spatialdata file itself
+ peek_lines = ["SpatialData object"]
+
+ # Show zarr format if available
+ if dataset.metadata.zarr_format:
+ peek_lines[0] += f" (Zarr Format v{dataset.metadata.zarr_format})"
+
+ # Show counts for each element type
+ if getattr(dataset.metadata, "images_count", 0):
+ peek_lines.append(f"├── Images ({dataset.metadata.images_count})")
+
+ if getattr(dataset.metadata, "labels_count", 0):
+ peek_lines.append(f"├── Labels ({dataset.metadata.labels_count})")
+
+ if getattr(dataset.metadata, "shapes_count", 0):
+ peek_lines.append(f"├── Shapes ({dataset.metadata.shapes_count})")
+
+ if getattr(dataset.metadata, "points_count", 0):
+ peek_lines.append(f"├── Points ({dataset.metadata.points_count})")
+
+ if dataset.metadata.tables:
+ peek_lines.append(f"└── Tables ({len(dataset.metadata.tables)})")
+ for tbl in dataset.metadata.tables:
+ # Add shape information if available
+ if dataset.metadata.table_shapes and tbl in dataset.metadata.table_shapes:
+ shape = dataset.metadata.table_shapes[tbl]
+ peek_lines.append(f" └── '{tbl}': AnnData {shape}")
+ else:
+ peek_lines.append(f" └── '{tbl}'")
+
+ # Show coordinate systems if available
+ if dataset.metadata.coordinate_systems:
+ peek_lines.append("")
+ peek_lines.append("with coordinate systems:")
+ for cs in dataset.metadata.coordinate_systems:
+ peek_lines.append(f" • {cs}")
+
+ dataset.peek = "\n".join(peek_lines)
+ dataset.blurb = f"SpatialData file ({nice_size(dataset.get_size())})"
+ if dataset.metadata.spatialdata_version:
+ dataset.blurb += f"\nVersion: {dataset.metadata.spatialdata_version}"
+ else:
+ dataset.peek = "file does not exist"
+ dataset.blurb = "file purged from disk"
+
+ def set_meta(self, dataset: DatasetProtocol, overwrite: bool = True, **kwd) -> None:
+ super().set_meta(dataset, overwrite=overwrite, **kwd)
+ try:
+ with zipfile.ZipFile(dataset.get_file_name()) as zf:
+ # Initialize element dictionaries to track elements by type
+ images = set()
+ labels = set()
+ shapes = set()
+ points = set()
+ tables = set()
+ coordinate_systems = set()
+ spatialdata_version = ""
+
+ # Find the root zarr directory
+ root_zarr = None
+ for file in zf.namelist():
+ if file.endswith(".zarr/.zattrs"):
+ root_zarr = file.replace("/.zattrs", "")
+ break
+
+ # Read root attributes for version info
+ if root_zarr:
+ root_attrs_path = f"{root_zarr}/.zattrs"
+ try:
+ with zf.open(root_attrs_path) as f:
+ root_attrs = json.load(f)
+ if "spatialdata_attrs" in root_attrs:
+ spatialdata_attrs = root_attrs["spatialdata_attrs"]
+ spatialdata_version = spatialdata_attrs.get("spatialdata_software_version", "")
+ except Exception:
+ pass
+
+ # Parse all files to extract elements and coordinate systems
+ for file in zf.namelist():
+ # Extract elements based on directory structure
+ # Expected structure: .zarr///...
+ if root_zarr and file.startswith(root_zarr + "/"):
+ rel_parts = file[len(root_zarr) + 1 :].split("/")
+ if len(rel_parts) >= 2:
+ element_type = rel_parts[0]
+ element_name = rel_parts[1]
+
+ # Skip metadata files and empty names
+ if element_name and not element_name.startswith("."):
+ if element_type == "images":
+ images.add(element_name)
+ elif element_type == "labels":
+ labels.add(element_name)
+ elif element_type == "shapes":
+ shapes.add(element_name)
+ elif element_type == "points":
+ points.add(element_name)
+ elif element_type == "tables":
+ tables.add(element_name)
+
+ # Extract coordinate system information from .zattrs files
+ if file.endswith(".zattrs"):
+ try:
+ with zf.open(file) as f:
+ attrs = json.load(f)
+
+ # Check for coordinate transformations
+ if "coordinateTransformations" in attrs:
+ transforms = attrs["coordinateTransformations"]
+ if isinstance(transforms, list):
+ for transform in transforms:
+ if isinstance(transform, dict) and "output" in transform:
+ output = transform["output"]
+ if isinstance(output, dict) and "name" in output:
+ coordinate_systems.add(output["name"])
+ elif isinstance(output, str):
+ coordinate_systems.add(output)
+
+ # Check for multiscales (images/labels)
+ if "multiscales" in attrs:
+ multiscales = attrs["multiscales"]
+ if isinstance(multiscales, list):
+ for ms in multiscales:
+ if isinstance(ms, dict) and "coordinateTransformations" in ms:
+ for ct in ms["coordinateTransformations"]:
+ if isinstance(ct, dict) and "output" in ct:
+ output = ct["output"]
+ if isinstance(output, dict) and "name" in output:
+ coordinate_systems.add(output["name"])
+ elif isinstance(output, str):
+ coordinate_systems.add(output)
+
+ # Check for spatialdata transform attribute (legacy)
+ if "transform" in attrs:
+ transform_dict = attrs["transform"]
+ if isinstance(transform_dict, dict):
+ coordinate_systems.update(transform_dict.keys())
+ except Exception:
+ pass
+
+ # Set metadata: counts for most elements, but keep tables and
+ # coordinate system names and table shapes for compatibility.
+ dataset.metadata.images_count = len(images)
+ dataset.metadata.labels_count = len(labels)
+ dataset.metadata.shapes_count = len(shapes)
+ dataset.metadata.points_count = len(points)
+
+ # Preserve table names and shapes (as before)
+ dataset.metadata.tables = sorted(tables)
+ table_shapes = {}
+ for table_name in tables:
+ try:
+ obs_index_path = f"{root_zarr}/tables/{table_name}/obs/_index/.zarray"
+ var_index_path = f"{root_zarr}/tables/{table_name}/var/_index/.zarray"
+ n_obs = None
+ n_vars = None
+ if obs_index_path in zf.namelist():
+ with zf.open(obs_index_path) as f:
+ obs_array = json.load(f)
+ n_obs = obs_array.get("shape", [None])[0]
+ if var_index_path in zf.namelist():
+ with zf.open(var_index_path) as f:
+ var_array = json.load(f)
+ n_vars = var_array.get("shape", [None])[0]
+ if n_obs is not None and n_vars is not None:
+ table_shapes[table_name] = (n_obs, n_vars)
+ except Exception:
+ pass
+ dataset.metadata.table_shapes = table_shapes
+ dataset.metadata.coordinate_systems = sorted(coordinate_systems)
+ dataset.metadata.spatialdata_version = spatialdata_version
+ except Exception:
+ pass
+
+ def sniff(self, filename: str) -> bool:
+ """
+ Check if the file is a valid SpatialData zarr archive.
+
+ SpatialData files are Zarr archives with specific structure containing
+ a root .zattrs file with spatialdata_attrs metadata and element directories
+ like images/, labels/, shapes/, points/, or tables/.
+
+ >>> from galaxy.datatypes.sniff import get_test_fname
+ >>> fname = get_test_fname('subsampled_visium.spatialdata.zip')
+ >>> SpatialData().sniff(fname)
+ True
+ >>> fname = get_test_fname('Images.zarr.zip')
+ >>> SpatialData().sniff(fname)
+ False
+ """
+
+ try:
+ with zipfile.ZipFile(filename) as zf:
+ # First, check if this is a zarr archive at all
+ if not super().sniff(filename):
+ return False
+
+ # Look for the root .zattrs file with spatialdata_attrs.
+ # This can distinguish spatialdata from other zarr archives.
+ for file in zf.namelist():
+ # Look for .zattrs file at the root of the zarr store
+ # The zarr store can be at root or one level deeper
+ parts = file.split("/")
+ # Root level: .zattrs or one level deep: .zarr/.zattrs
+ if file == ".zattrs" or (len(parts) == 2 and parts[0].endswith(".zarr") and parts[1] == ".zattrs"):
+ try:
+ with zf.open(file) as f:
+ attrs = json.load(f)
+ # Check for SpatialData-specific metadata
+ if "spatialdata_attrs" in attrs:
+ return True
+ except Exception:
+ pass
+
+ return False
+ except Exception:
+ # Any exception during parsing means it's not a valid spatialdata file
+ return False
+
+
@build_sniff_from_prefix
class Safetensors(Binary):
"""
diff --git a/lib/galaxy/datatypes/test/Images.zarr.zip b/lib/galaxy/datatypes/test/Images.zarr.zip
new file mode 100644
index 00000000000..766aea5417d
Binary files /dev/null and b/lib/galaxy/datatypes/test/Images.zarr.zip differ
diff --git a/lib/galaxy/datatypes/test/subsampled_visium.spatialdata.zip b/lib/galaxy/datatypes/test/subsampled_visium.spatialdata.zip
new file mode 100644
index 00000000000..1040d76d785
Binary files /dev/null and b/lib/galaxy/datatypes/test/subsampled_visium.spatialdata.zip differ
diff --git a/lib/galaxy/tool_util/toolbox/base.py b/lib/galaxy/tool_util/toolbox/base.py
index e627dce3a7d..f081f028e20 100644
--- a/lib/galaxy/tool_util/toolbox/base.py
+++ b/lib/galaxy/tool_util/toolbox/base.py
@@ -802,7 +802,7 @@ class AbstractToolBox(ManagesIntegratedToolPanelMixin):
# if we don't have a lineage_map for this tool we need to sort by version,
# so that the last tool in rval is the newest tool.
- rval.sort(key=lambda t: t.version)
+ rval.sort(key=lambda t: t.version_object)
if rval:
if get_all_versions:
return rval
diff --git a/test/functional/tools/multiple_versions_sorting_v110.xml b/test/functional/tools/multiple_versions_sorting_v110.xml
new file mode 100644
index 00000000000..c0c608956d5
--- /dev/null
+++ b/test/functional/tools/multiple_versions_sorting_v110.xml
@@ -0,0 +1,11 @@
+
+ '$out_file1'
+ ]]>
+
+
+
+
+
+
+
diff --git a/test/functional/tools/multiple_versions_sorting_v19.xml b/test/functional/tools/multiple_versions_sorting_v19.xml
new file mode 100644
index 00000000000..5cc5ec8b911
--- /dev/null
+++ b/test/functional/tools/multiple_versions_sorting_v19.xml
@@ -0,0 +1,11 @@
+
+ '$out_file1'
+ ]]>
+
+
+
+
+
+
+
diff --git a/test/functional/tools/sample_tool_conf.xml b/test/functional/tools/sample_tool_conf.xml
index 2b62538de4f..e1d5e36e5bc 100644
--- a/test/functional/tools/sample_tool_conf.xml
+++ b/test/functional/tools/sample_tool_conf.xml
@@ -251,6 +251,8 @@
+
+