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 @@ + +