From a92b4374dc24f361c548484fb38b801cf719997c Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 19 Jun 2026 15:04:09 +0200 Subject: [PATCH 1/9] fix the types --- .pre-commit-config.yaml | 16 ++- pyproject.toml | 4 +- src/eopf_geozarr/conversion/geozarr.py | 48 ++++--- src/eopf_geozarr/conversion/utils.py | 41 ++++++ .../s2_optimization/s2_multiscale.py | 134 ++++++++++-------- ...041_N0511_R122_T32TQM_20251008T122613.json | 116 +++++++-------- ...309_N0511_R108_T32TLQ_20250113T122458.json | 84 +++++------ ...131_N0511_R037_T29TPF_20250811T152216.json | 116 +++++++-------- ...041_N0511_R122_T32TQM_20251008T122613.json | 116 +++++++-------- ...309_N0511_R108_T32TLQ_20250113T122458.json | 84 +++++------ ...131_N0511_R037_T29TPF_20250811T152216.json | 116 +++++++-------- uv.lock | 10 +- 12 files changed, 474 insertions(+), 411 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f19c8a37..8fcd0edf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,13 +11,15 @@ repos: args: ["--fix", "--show-fixes"] - id: ruff-format - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.0 + - repo: local hooks: - id: mypy - language_version: python + name: mypy + entry: uv run mypy + language: system + types: [python] exclude: tests/.* - additional_dependencies: - - types-attrs - - typing-extensions>=4.15.0 - - pydantic>=2.12 + # Run from the project's locked environment so mypy sees the real, + # py.typed types of every dependency (zarr-cm, pydantic, ...) instead + # of treating them as Any. Staged files are passed through, matching + # the previous hook's per-file scope. diff --git a/pyproject.toml b/pyproject.toml index 2d371778..ca94c6b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "rioxarray>=0.13.0", "cf-xarray>=0.8.0", "typing-extensions>=4.15.0", - "zarr-cm>=0.2.0", + "zarr-cm>=0.3.0", "aiohttp>=3.14.0", "s3fs>=2024.6.0", "boto3>=1.34.0", @@ -172,7 +172,7 @@ warn_required_dynamic_aliases = true warn_untyped_fields = true [[tool.mypy.overrides]] -module = ["zarr.*", "xarray.*", "rioxarray.*", "cf_xarray.*", "dask.*"] +module = ["zarr.*", "xarray.*", "rioxarray.*", "rasterio.*", "cf_xarray.*", "dask.*"] ignore_missing_imports = true [[tool.mypy.overrides]] diff --git a/src/eopf_geozarr/conversion/geozarr.py b/src/eopf_geozarr/conversion/geozarr.py index e2a13b37..cafa5e45 100644 --- a/src/eopf_geozarr/conversion/geozarr.py +++ b/src/eopf_geozarr/conversion/geozarr.py @@ -18,7 +18,7 @@ import os import time from collections.abc import Hashable, Iterable, Mapping, Sequence -from typing import Any +from typing import Any, cast import numpy as np import structlog @@ -365,7 +365,7 @@ def iterative_copy( # Write the dataset group_param = current_group_path.lstrip("/") if current_group_path else None - ds.to_zarr( + ds.to_zarr( # type: ignore[call-overload] # xarray stubs type storage_options as dict[str, str] output_path, group=group_param, mode="w", @@ -507,7 +507,7 @@ def write_geozarr_group( # Create GeoZarr-spec compliant multiscales if _is_sentinel1(dt_input): assert gcp_group is not None, "GCP group required for processing Sentinel-1" - ds_gcp = dt_input[gcp_group].to_dataset() + ds_gcp: xr.Dataset | None = dt_input[gcp_group].to_dataset() # For Sentinel-1, ds_gcp is set to None since data is now reprojected and doesn't need GCP handling ds_gcp = None else: @@ -585,7 +585,7 @@ def create_geozarr_compliant_multiscales( compressor = BloscCodec(cname="zstd", clevel=3, shuffle="shuffle") # Get spatial information from the first data variable - data_vars = [var for var in ds.data_vars if not utils.is_grid_mapping_variable(ds, var)] + data_vars = [var for var in ds.data_vars if not utils.is_grid_mapping_variable(ds, str(var))] if not data_vars: return {} @@ -679,15 +679,20 @@ def _spatial_transform_for( resampling_method="average", ), ) + # The multiscales/spatial/proj CMOs are declared by MultiscaleGroupAttrs + # above; here we add only the validated spatial/proj data keys. attrs_to_write = multiscale_attrs.model_dump() if native_crs and native_bounds: - attrs_to_write["spatial:dimensions"] = ["y", "x"] - attrs_to_write["spatial:bbox"] = list(native_bounds) - attrs_to_write["spatial:registration"] = "pixel" - if hasattr(native_crs, "to_epsg") and native_crs.to_epsg(): - attrs_to_write["proj:code"] = f"EPSG:{native_crs.to_epsg()}" - elif hasattr(native_crs, "to_wkt"): - attrs_to_write["proj:wkt2"] = native_crs.to_wkt() + spatial_proj = utils.build_spatial_proj_attrs( + spatial={ + "spatial:dimensions": ["y", "x"], + "spatial:bbox": list(native_bounds), + "spatial:registration": "pixel", + }, + crs=native_crs, + ) + spatial_proj.pop("zarr_conventions", None) # CMOs come from the model above + attrs_to_write.update(spatial_proj) group_path = fs_utils.normalize_path(f"{output_path}/{group_name.lstrip('/')}") zarr_group = fs_utils.open_zarr_group(group_path, mode="r+") @@ -748,7 +753,7 @@ def _spatial_transform_for( overview_ds = sanitize_dataset_attributes(overview_ds) align_chunks_flag = not enable_sharding - overview_ds.to_zarr( + overview_ds.to_zarr( # type: ignore[call-overload] # xarray stubs type storage_options as dict[str, str] output_path, group=overview_group, mode="w", @@ -1011,7 +1016,7 @@ def write_dataset_band_by_band_with_validation( ) # Get data variables - data_vars = [var for var in ds.data_vars if not utils.is_grid_mapping_variable(ds, var)] + data_vars = [var for var in ds.data_vars if not utils.is_grid_mapping_variable(ds, str(var))] successful_vars = [] failed_vars = [] @@ -1044,9 +1049,10 @@ def cleanup_prefix(prefix: str) -> None: for var in data_vars: # Check if this variable already exists and is valid if not force_overwrite and store_exists: - if utils.validate_existing_band_data(existing_dataset, var, ds): + assert existing_dataset is not None # guaranteed by store_exists + if utils.validate_existing_band_data(existing_dataset, str(var), ds): ds.drop_vars(str(var)) - ds[var] = existing_dataset[var] # type: ignore[index] + ds[var] = existing_dataset[var] log.info("✅ Band %s already exists and is valid, skipping.", var) skipped_vars.append(var) successful_vars.append(var) @@ -1107,7 +1113,7 @@ def cleanup_prefix(prefix: str) -> None: # Sanitize NaN values in single variable dataset attributes single_var_ds = sanitize_dataset_attributes(single_var_ds) - single_var_ds.to_zarr( + single_var_ds.to_zarr( # type: ignore[call-overload] # xarray stubs type storage_options as dict[str, str] output_path, group=group_name, mode="a", @@ -1388,6 +1394,7 @@ def _create_encoding( for var in ds.data_vars: if hasattr(ds[var].data, "chunks"): current_chunks = ds[var].chunks + assert current_chunks is not None # guaranteed by hasattr(..., "chunks") if len(current_chunks) >= 2: chunking = tuple( current_chunks[i][0] if len(current_chunks[i]) > 0 else ds[var].shape[i] @@ -1429,7 +1436,7 @@ def _create_geozarr_encoding( encoding: dict[Hashable, XarrayEncodingJSON] = {} chunks: tuple[int, ...] for var in ds.data_vars: - if utils.is_grid_mapping_variable(ds, var): + if utils.is_grid_mapping_variable(ds, str(var)): encoding[var] = {"compressors": None} else: data_shape = ds[var].shape @@ -1641,7 +1648,7 @@ def _add_grid_mapping_variable( # Ensure all data variables have the grid_mapping attribute for var_name in overview_ds.data_vars: if ( - not utils.is_grid_mapping_variable(overview_ds, var_name) + not utils.is_grid_mapping_variable(overview_ds, str(var_name)) and "grid_mapping" not in overview_ds[var_name].attrs ): overview_ds[var_name].attrs["grid_mapping"] = grid_mapping_var_name @@ -1695,4 +1702,7 @@ def _is_sentinel1(dt: xr.DataTree) -> bool: def get_zarr_group(data: xr.DataTree) -> zarr.Group: - return data._close.__self__.zarr_group + # `_close` is a bound method of the backend store on an opened DataTree; + # `__self__` retrieves that store, which exposes `zarr_group`. These are + # xarray/zarr internals without public type information. + return cast("zarr.Group", data._close.__self__.zarr_group) # type: ignore[union-attr] diff --git a/src/eopf_geozarr/conversion/utils.py b/src/eopf_geozarr/conversion/utils.py index e7125c97..e6da415a 100644 --- a/src/eopf_geozarr/conversion/utils.py +++ b/src/eopf_geozarr/conversion/utils.py @@ -6,10 +6,51 @@ import rasterio # noqa: F401 # Import to enable .rio accessor import structlog import xarray as xr +import zarr_cm log = structlog.get_logger() +def proj_attrs_for_crs(crs: Any) -> dict[str, Any]: + """Build the ``proj`` convention data keys for a CRS. + + Prefers an EPSG code (``proj:code``) and falls back to WKT2 + (``proj:wkt2``). Returns an empty dict when *crs* is falsy or exposes + neither representation. + """ + if not crs: + return {} + if hasattr(crs, "to_epsg") and crs.to_epsg(): + return {"proj:code": f"EPSG:{crs.to_epsg()}"} + if hasattr(crs, "to_wkt"): + return {"proj:wkt2": crs.to_wkt()} + return {} + + +def build_spatial_proj_attrs( + *, + spatial: dict[str, Any], + crs: Any, +) -> dict[str, Any]: + """Build validated ``spatial`` + ``proj`` convention attributes. + + Delegates to :func:`zarr_cm.create_many`, which validates each convention's + data and emits the matching convention-metadata objects into a combined + ``zarr_conventions`` array (spatial first, then proj). *spatial* holds the + ``spatial:*`` keys; the proj keys are derived from *crs* via + :func:`proj_attrs_for_crs`. + + The proj convention is only included when *crs* yields a usable CRS + representation; otherwise only the spatial convention is emitted (a proj + convention with no CRS field is invalid). + """ + conventions: dict[zarr_cm.ConventionName, dict[str, Any]] = {"spatial": spatial} + proj = proj_attrs_for_crs(crs) + if proj: + conventions["geo-proj"] = proj + return zarr_cm.create_many(conventions) + + # Sentinel: distinguish "no explicit fill_value" from a legitimate `None`. UNSET: Any = object() diff --git a/src/eopf_geozarr/s2_optimization/s2_multiscale.py b/src/eopf_geozarr/s2_optimization/s2_multiscale.py index d9743069..23152d55 100644 --- a/src/eopf_geozarr/s2_optimization/s2_multiscale.py +++ b/src/eopf_geozarr/s2_optimization/s2_multiscale.py @@ -6,7 +6,7 @@ from __future__ import annotations from itertools import pairwise -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, cast import numpy as np import structlog @@ -139,11 +139,13 @@ def _coarsen_variable(var_name: str, var_data: xr.DataArray, factor: int) -> xr. var_type = determine_variable_type(var_name, var_data) coarsened = var_data.coarsen({"x": factor, "y": factor}, boundary="trim") if var_type in ("reflectance", "probability"): - result = coarsened.mean() + # xarray stubs omit reduction methods on DataArrayCoarsen. + result = coarsened.mean() # type: ignore[attr-defined] elif var_type == "classification": result = coarsened.reduce(subsample_2) elif var_type == "quality_mask": - result = coarsened.max() + # xarray stubs omit reduction methods on DataArrayCoarsen. + result = coarsened.max() # type: ignore[attr-defined] else: raise ValueError(f"Unknown variable type {var_type}") @@ -152,9 +154,9 @@ def _coarsen_variable(var_name: str, var_data: xr.DataArray, factor: int) -> xr. # inspects encoding (e.g. to push CF scale-offset into a codec pipeline) # would see an empty encoding on every coarsened level. encoding = var_data.encoding - result = result.astype(var_data.dtype) - result.encoding = encoding - return result + cast_result: xr.DataArray = result.astype(var_data.dtype) + cast_result.encoding = encoding + return cast_result def inject_missing_bands( @@ -255,7 +257,7 @@ def create_multiscale_from_datatree( Returns: Dictionary of processed groups """ - processed_groups = {} + processed_groups: dict[str, Any] = {} # The scale levels in the output data. 10, 20, 60 already exist in the source data. # Step 1: Copy all original groups as-is @@ -409,14 +411,16 @@ def create_multiscale_from_datatree( # Step 3: Add multiscales metadata to parent groups log.info("Adding multiscales metadata to parent groups") - # Get the parent group (it was created when writing the resolution groups) - parent_group = output_group[base_path] + # Get the parent group (it was created when writing the resolution groups). + # `base_path` always addresses a group (the reflectance parent), never an + # array, so narrow the `Array | Group` result to `Group` for the call below. + parent_group = cast("zarr.Group", output_group[base_path]) - dt_multiscale = add_multiscales_metadata_to_parent( + add_multiscales_metadata_to_parent( parent_group, resolution_groups, ) - processed_groups[base_path] = dt_multiscale + processed_groups[base_path] = None return processed_groups @@ -546,13 +550,13 @@ def create_measurements_encoding( # otherwise leak into the output). is_float = np.issubdtype(var_data.dtype, np.floating) var_data.attrs = utils.sanitize_array_attrs(var_data.attrs, is_decoded_float=is_float) - encoding[var_name] = var_encoding + encoding[str(var_name)] = var_encoding # Add coordinate encoding and sanitize coord attrs (e.g. drop # ``_eopf_attrs`` from datetime coords carried in from the source). for coord_name, coord_data in dataset.coords.items(): coord_data.attrs = utils.sanitize_array_attrs(coord_data.attrs) - encoding[coord_name] = {"compressors": []} # type: ignore[typeddict-item] + encoding[str(coord_name)] = {"compressors": []} # type: ignore[typeddict-item] return encoding @@ -613,8 +617,12 @@ def calculate_simple_shard_dimensions( def add_multiscales_metadata_to_parent( group: zarr.Group, res_groups: Mapping[str, xr.Dataset], -) -> xr.DataTree: - """Add GeoZarr-compliant multiscales metadata to parent group.""" +) -> None: + """Add GeoZarr-compliant multiscales metadata to parent group. + + Returns ``None`` in all cases: metadata is written directly to ``group`` + via ``group.attrs.update`` rather than returned as a DataTree. + """ # Sort by resolution (finest to coarsest) res_order = { "r10m": 10, @@ -632,7 +640,7 @@ def add_multiscales_metadata_to_parent( "Skipping {} - only one resolution available", base_path=group.path, ) - return None + return # Get CRS and bounds from first available dataset (load from output path) first_res = all_resolutions[0] @@ -642,14 +650,14 @@ def add_multiscales_metadata_to_parent( native_crs = first_dataset.rio.crs if hasattr(first_dataset, "rio") else None if native_crs is None: log.info("No CRS found, skipping multiscales metadata", base_path=group.path) - return None + return # Calculate bounds directly from coordinates for consistency with the data arrays if "x" not in first_dataset.coords or "y" not in first_dataset.coords: log.error( "Missing x/y coordinates in dataset, cannot determine bounds", base_path=group.path ) - return None + return x_coords = first_dataset.x.values y_coords = first_dataset.y.values @@ -668,8 +676,10 @@ def add_multiscales_metadata_to_parent( dataset = res_groups[res_name] + # Defensive guard retained for runtime safety even though the typed + # contract (Mapping[str, xr.Dataset]) means mypy proves it unreachable. if dataset is None: - continue + continue # type: ignore[unreachable] # Get first data variable to extract dimensions first_var = next(iter(dataset.data_vars.values())) @@ -771,7 +781,7 @@ def add_multiscales_metadata_to_parent( if len(overview_levels) < 2: log.info(" Could not create overview levels for {}", base_path=group.path) - return None + return layout: list[zcm.ScaleLevel] | MISSING = MISSING # type: ignore[valid-type] @@ -826,29 +836,29 @@ def add_multiscales_metadata_to_parent( ), ) - # Write multiscale attributes directly to the parent group + # Write multiscale attributes directly to the parent group. The multiscales + # CMO and the spatial/proj CMOs are declared by MultiscaleGroupAttrs above; + # here we add only the validated spatial/proj data keys. attrs_to_write = multiscale_attrs.model_dump() # Add spatial and proj attributes at group level following specifications if native_crs and native_bounds: - # Add spatial convention attributes - attrs_to_write["spatial:dimensions"] = ["y", "x"] # Required field - attrs_to_write["spatial:bbox"] = list(native_bounds) # [xmin, ymin, xmax, ymax] - attrs_to_write["spatial:registration"] = "pixel" # Default registration type - - # Add proj convention attributes - if hasattr(native_crs, "to_epsg") and native_crs.to_epsg(): - attrs_to_write["proj:code"] = f"EPSG:{native_crs.to_epsg()}" - elif hasattr(native_crs, "to_wkt"): - attrs_to_write["proj:wkt2"] = native_crs.to_wkt() + spatial_proj = utils.build_spatial_proj_attrs( + spatial={ + "spatial:dimensions": ["y", "x"], + "spatial:bbox": list(native_bounds), # [xmin, ymin, xmax, ymax] + "spatial:registration": "pixel", + }, + crs=native_crs, + ) + spatial_proj.pop("zarr_conventions", None) # CMOs come from the model above + attrs_to_write.update(spatial_proj) # Write attributes directly to the zarr group group.attrs.update(attrs_to_write) log.info("Added %s multiscale levels to %s", len(overview_levels), group.path) - return None # No DataTree to return since we wrote directly to the group - def create_original_encoding(dataset: xr.Dataset) -> dict[str, XarrayDataArrayEncoding]: """Write a group preserving its original chunking and encoding.""" @@ -856,7 +866,7 @@ def create_original_encoding(dataset: xr.Dataset) -> dict[str, XarrayDataArrayEn # Simple encoding that preserves original structure compressor = BloscCodec(cname="zstd", clevel=3, shuffle="shuffle", blocksize=0) - encoding = {} + encoding: dict[str, XarrayDataArrayEncoding] = {} for var_name in dataset.data_vars: # start with the original encoding @@ -881,11 +891,11 @@ def create_original_encoding(dataset: xr.Dataset) -> dict[str, XarrayDataArrayEn # Sanitize source-only attributes (replace dict — ``.update`` cannot # remove keys, so stale ``_eopf_attrs`` would otherwise leak through). var_data.attrs = utils.sanitize_array_attrs(var_data.attrs) - encoding[var_name] = var_encoding + encoding[str(var_name)] = var_encoding for coord_name, coord_data in dataset.coords.items(): coord_data.attrs = utils.sanitize_array_attrs(coord_data.attrs) - encoding[coord_name] = {"compressors": None} + encoding[str(coord_name)] = {"compressors": None} return encoding @@ -912,7 +922,7 @@ def create_downsampled_resolution_group(source_dataset: xr.Dataset, factor: int) for var_name, var_data in source_dataset.data_vars.items(): if var_data.ndim < 2: continue - lazy_vars[var_name] = _coarsen_variable(var_name, var_data, factor) + lazy_vars[var_name] = _coarsen_variable(str(var_name), var_data, factor) if not lazy_vars: return xr.Dataset() @@ -962,7 +972,7 @@ def create_downsampled_coordinates( # Copy any other coordinates that might exist coords.update( { - coord_name: coord_data + str(coord_name): coord_data for coord_name, coord_data in level_2_dataset.coords.items() if coord_name not in ["x", "y"] } @@ -976,9 +986,11 @@ def create_lazy_downsample_operation_from_existing( ) -> xr.DataArray: """Create lazy downsampling operation from existing data.""" - @delayed # type: ignore[misc] + # `dask.delayed` is untyped, so the decorator would otherwise make + # `downsample_operation` untyped under strict mypy. + @delayed # type: ignore[untyped-decorator] def downsample_operation() -> Any: - var_type = determine_variable_type(source_data.name, source_data) + var_type = determine_variable_type(str(source_data.name), source_data) return downsample_variable(source_data, target_height, target_width, var_type) # Create delayed operation @@ -1040,8 +1052,14 @@ def stream_write_dataset( "Level path {} already exists. Skipping write.", dataset_path=path, ) + # The zarr backend accepts a zarr `Store` here at runtime, but xarray's + # `open_dataset` stub only types the first arg as path/buffer/datastore. return xr.open_dataset( - group.store, engine="zarr", chunks={}, decode_coords="all", group=path + group.store, # type: ignore[arg-type] + engine="zarr", + chunks={}, + decode_coords="all", + group=path, ) log.info("Streaming computation and write to {}", dataset_path=path) @@ -1146,9 +1164,11 @@ def write_geo_metadata( # TODO : Remove once rioxarray supports writing these conventions directly # https://github.com/corteva/rioxarray/pull/883 - # Add spatial convention attributes - dataset.attrs["spatial:dimensions"] = ["y", "x"] # Required field - dataset.attrs["spatial:registration"] = "pixel" # Default registration type + # Assemble spatial convention data + spatial_data: dict[str, Any] = { + "spatial:dimensions": ["y", "x"], # Required field + "spatial:registration": "pixel", # Default registration type + } # Calculate and add spatial bbox if coordinates are available if "x" in dataset.coords and "y" in dataset.coords: @@ -1156,33 +1176,23 @@ def write_geo_metadata( y_coords = dataset.coords["y"].values x_min, x_max = float(x_coords.min()), float(x_coords.max()) y_min, y_max = float(y_coords.min()), float(y_coords.max()) - dataset.attrs["spatial:bbox"] = [x_min, y_min, x_max, y_max] + spatial_data["spatial:bbox"] = [x_min, y_min, x_max, y_max] spatial_transform = _preferred_spatial_transform(dataset) # Only add spatial:transform if we have valid transform data (not all zeros) if spatial_transform is not None and not all(t == 0 for t in spatial_transform): - dataset.attrs["spatial:transform"] = list(spatial_transform) + spatial_data["spatial:transform"] = list(spatial_transform) # Add spatial shape if data variables exist if dataset.data_vars: first_var = next(iter(dataset.data_vars.values())) if first_var.ndim >= 2: height, width = first_var.shape[-2:] - dataset.attrs["spatial:shape"] = [height, width] - - # Add proj convention attributes - if hasattr(crs, "to_epsg") and crs.to_epsg(): - dataset.attrs["proj:code"] = f"EPSG:{crs.to_epsg()}" - elif hasattr(crs, "to_wkt"): - dataset.attrs["proj:wkt2"] = crs.to_wkt() + spatial_data["spatial:shape"] = [height, width] - # Add zarr convention declarations - conventions = [ - spatial_cm.CMO, - geo_proj.CMO, - ] - dataset.attrs["zarr_conventions"] = conventions + # Build validated spatial + proj convention attrs (data + CMOs) via zarr-cm + dataset.attrs.update(utils.build_spatial_proj_attrs(spatial=spatial_data, crs=crs)) def rechunk_dataset_for_encoding( @@ -1194,11 +1204,11 @@ def rechunk_dataset_for_encoding( When using Zarr v3 sharding, Dask chunks must align with shard dimensions to avoid checksum validation errors. """ - rechunked_vars = {} + rechunked_vars: dict[Hashable, xr.DataArray] = {} for var_name, var_data in dataset.data_vars.items(): - if var_name in encoding: - var_encoding = encoding[var_name] + if str(var_name) in encoding: + var_encoding = encoding[str(var_name)] # If sharding is enabled, rechunk based on shard dimensions if "shards" in var_encoding and var_encoding["shards"] is not None: diff --git a/tests/_test_data/geozarr_examples/S2A_MSIL2A_20251008T100041_N0511_R122_T32TQM_20251008T122613.json b/tests/_test_data/geozarr_examples/S2A_MSIL2A_20251008T100041_N0511_R122_T32TQM_20251008T122613.json index 48a69cc8..8bc9e17a 100644 --- a/tests/_test_data/geozarr_examples/S2A_MSIL2A_20251008T100041_N0511_R122_T32TQM_20251008T122613.json +++ b/tests/_test_data/geozarr_examples/S2A_MSIL2A_20251008T100041_N0511_R122_T32TQM_20251008T122613.json @@ -4117,22 +4117,22 @@ "zarr_conventions": [ { "uuid": "d35379db-88df-4056-af3a-620245f8e347", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v0.1/schema.json", + "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v0.1/README.md", "name": "multiscales", "description": "Multiscale layout of zarr datasets" }, { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -4277,15 +4277,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -4752,15 +4752,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -5842,15 +5842,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -6856,15 +6856,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -7946,15 +7946,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -9037,15 +9037,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -10141,15 +10141,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -10457,15 +10457,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -10773,15 +10773,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -11096,15 +11096,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -11636,15 +11636,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -12362,15 +12362,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -12816,15 +12816,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } diff --git a/tests/_test_data/geozarr_examples/S2B_MSIL1C_20250113T103309_N0511_R108_T32TLQ_20250113T122458.json b/tests/_test_data/geozarr_examples/S2B_MSIL1C_20250113T103309_N0511_R108_T32TLQ_20250113T122458.json index 9d0f84b2..15039c49 100644 --- a/tests/_test_data/geozarr_examples/S2B_MSIL1C_20250113T103309_N0511_R108_T32TLQ_20250113T122458.json +++ b/tests/_test_data/geozarr_examples/S2B_MSIL1C_20250113T103309_N0511_R108_T32TLQ_20250113T122458.json @@ -3812,22 +3812,22 @@ "zarr_conventions": [ { "uuid": "d35379db-88df-4056-af3a-620245f8e347", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v0.1/schema.json", + "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v0.1/README.md", "name": "multiscales", "description": "Multiscale layout of zarr datasets" }, { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -3972,15 +3972,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -4447,15 +4447,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -4921,15 +4921,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -5627,15 +5627,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -6101,15 +6101,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -6576,15 +6576,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -7064,15 +7064,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -7604,15 +7604,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -8330,15 +8330,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } diff --git a/tests/_test_data/geozarr_examples/S2C_MSIL2A_20250811T112131_N0511_R037_T29TPF_20250811T152216.json b/tests/_test_data/geozarr_examples/S2C_MSIL2A_20250811T112131_N0511_R037_T29TPF_20250811T152216.json index be9ee053..0b19623b 100644 --- a/tests/_test_data/geozarr_examples/S2C_MSIL2A_20250811T112131_N0511_R037_T29TPF_20250811T152216.json +++ b/tests/_test_data/geozarr_examples/S2C_MSIL2A_20250811T112131_N0511_R037_T29TPF_20250811T152216.json @@ -4117,22 +4117,22 @@ "zarr_conventions": [ { "uuid": "d35379db-88df-4056-af3a-620245f8e347", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v0.1/schema.json", + "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v0.1/README.md", "name": "multiscales", "description": "Multiscale layout of zarr datasets" }, { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -4277,15 +4277,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -4752,15 +4752,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -5842,15 +5842,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -6856,15 +6856,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -7946,15 +7946,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -9037,15 +9037,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -10141,15 +10141,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -10457,15 +10457,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -10773,15 +10773,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -11096,15 +11096,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -11636,15 +11636,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -12362,15 +12362,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } @@ -12816,15 +12816,15 @@ "zarr_conventions": [ { "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "name": "spatial:", "description": "Spatial coordinate information" }, { "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "name": "proj:", "description": "Coordinate reference system information for geospatial data" } diff --git a/tests/_test_data/optimized_geozarr_examples/S2A_MSIL2A_20251008T100041_N0511_R122_T32TQM_20251008T122613.json b/tests/_test_data/optimized_geozarr_examples/S2A_MSIL2A_20251008T100041_N0511_R122_T32TQM_20251008T122613.json index fa20e80d..5b335149 100644 --- a/tests/_test_data/optimized_geozarr_examples/S2A_MSIL2A_20251008T100041_N0511_R122_T32TQM_20251008T122613.json +++ b/tests/_test_data/optimized_geozarr_examples/S2A_MSIL2A_20251008T100041_N0511_R122_T32TQM_20251008T122613.json @@ -4218,22 +4218,22 @@ { "description": "Multiscale layout of zarr datasets", "name": "multiscales", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v0.1/schema.json", + "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v0.1/README.md", "uuid": "d35379db-88df-4056-af3a-620245f8e347" }, { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -4262,15 +4262,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -4830,15 +4830,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -6195,15 +6195,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -7463,15 +7463,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -8828,15 +8828,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -10196,15 +10196,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -11575,15 +11575,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -11891,15 +11891,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -12207,15 +12207,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -12530,15 +12530,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -13070,15 +13070,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -13796,15 +13796,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -14250,15 +14250,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] diff --git a/tests/_test_data/optimized_geozarr_examples/S2B_MSIL1C_20250113T103309_N0511_R108_T32TLQ_20250113T122458.json b/tests/_test_data/optimized_geozarr_examples/S2B_MSIL1C_20250113T103309_N0511_R108_T32TLQ_20250113T122458.json index caf32e63..d75fa908 100644 --- a/tests/_test_data/optimized_geozarr_examples/S2B_MSIL1C_20250113T103309_N0511_R108_T32TLQ_20250113T122458.json +++ b/tests/_test_data/optimized_geozarr_examples/S2B_MSIL1C_20250113T103309_N0511_R108_T32TLQ_20250113T122458.json @@ -3913,22 +3913,22 @@ { "description": "Multiscale layout of zarr datasets", "name": "multiscales", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v0.1/schema.json", + "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v0.1/README.md", "uuid": "d35379db-88df-4056-af3a-620245f8e347" }, { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -3957,15 +3957,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -4525,15 +4525,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -5090,15 +5090,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -5958,15 +5958,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -6523,15 +6523,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -7091,15 +7091,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -7670,15 +7670,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -8210,15 +8210,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -8936,15 +8936,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] diff --git a/tests/_test_data/optimized_geozarr_examples/S2C_MSIL2A_20250811T112131_N0511_R037_T29TPF_20250811T152216.json b/tests/_test_data/optimized_geozarr_examples/S2C_MSIL2A_20250811T112131_N0511_R037_T29TPF_20250811T152216.json index 9ed8a49e..7eb259c3 100644 --- a/tests/_test_data/optimized_geozarr_examples/S2C_MSIL2A_20250811T112131_N0511_R037_T29TPF_20250811T152216.json +++ b/tests/_test_data/optimized_geozarr_examples/S2C_MSIL2A_20250811T112131_N0511_R037_T29TPF_20250811T152216.json @@ -4218,22 +4218,22 @@ { "description": "Multiscale layout of zarr datasets", "name": "multiscales", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v0.1/schema.json", + "spec_url": "https://github.com/zarr-conventions/multiscales/blob/v0.1/README.md", "uuid": "d35379db-88df-4056-af3a-620245f8e347" }, { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -4262,15 +4262,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -4830,15 +4830,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -6195,15 +6195,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -7463,15 +7463,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -8828,15 +8828,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -10196,15 +10196,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -11575,15 +11575,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -11891,15 +11891,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -12207,15 +12207,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -12530,15 +12530,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -13070,15 +13070,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -13796,15 +13796,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] @@ -14250,15 +14250,15 @@ { "description": "Spatial coordinate information", "name": "spatial:", - "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-conventions/spatial/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/spatial/54d81b7ced0376e63ee10f34db31db7d08dcc28d/schema.json", + "spec_url": "https://github.com/zarr-conventions/spatial/blob/54d81b7ced0376e63ee10f34db31db7d08dcc28d/README.md", "uuid": "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4" }, { "description": "Coordinate reference system information for geospatial data", "name": "proj:", - "schema_url": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json", - "spec_url": "https://github.com/zarr-experimental/geo-proj/blob/v1/README.md", + "schema_url": "https://raw.githubusercontent.com/zarr-conventions/proj/5ca5b2f92e5c7245f957d9128b289ee535f0720d/schema.json", + "spec_url": "https://github.com/zarr-conventions/proj/blob/5ca5b2f92e5c7245f957d9128b289ee535f0720d/README.md", "uuid": "f17cb550-5864-4468-aeb7-f3180cfb622f" } ] diff --git a/uv.lock b/uv.lock index 6f1926ed..09d98ad5 100644 --- a/uv.lock +++ b/uv.lock @@ -831,7 +831,7 @@ wheels = [ [[package]] name = "eopf-geozarr" -version = "0.10.0" +version = "0.10.1" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -894,7 +894,7 @@ requires-dist = [ { name = "typing-extensions", specifier = ">=4.15.0" }, { name = "xarray", specifier = ">=2025.7.1" }, { name = "zarr", extras = ["cast-value-rs"], specifier = ">=3.2.0" }, - { name = "zarr-cm", specifier = ">=0.2.0" }, + { name = "zarr-cm", specifier = ">=0.3.0" }, ] [package.metadata.requires-dev] @@ -3157,11 +3157,11 @@ cast-value-rs = [ [[package]] name = "zarr-cm" -version = "0.2.0" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/40/99c941d23a7cd84fb194b3eaa2ca2d0d85f5135aa07e2c4f2c12febfef43/zarr_cm-0.2.0.tar.gz", hash = "sha256:737f5b181e8a2456643c423f49b59d879531498c13c7345860f0e718ef73d8a8", size = 24773, upload-time = "2026-02-06T14:47:57.588Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/b6/d920b941f5862dca32b1689316e15824e12393fa9b25598a58ba1823dc55/zarr_cm-0.3.0.tar.gz", hash = "sha256:6fbee90a196539d61ecc51fe8fdf7fee1f70d5733289fb03df639be229465c79", size = 43995, upload-time = "2026-06-17T13:06:29.448Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/60/06c371c10a321db63ec46634c9498a3c491754214c0e3bfc27410c528d99/zarr_cm-0.2.0-py3-none-any.whl", hash = "sha256:7f4c778d70d37ccb6e10e1509f4c0a488689027dcebcd6b64fa89a899f101e11", size = 14222, upload-time = "2026-02-06T14:47:56.465Z" }, + { url = "https://files.pythonhosted.org/packages/48/1e/d308d5ee6cd728013e534de3522e726ea5a9ba5843e5a149a80b1e40c75f/zarr_cm-0.3.0-py3-none-any.whl", hash = "sha256:489b34666823d1ff0dfb9fe975f6cccbe94660a39fe0b4688970b2baf39b43fc", size = 30752, upload-time = "2026-06-17T13:06:28.202Z" }, ] [[package]] From 546d7a4c089bb9b8bff5854be0a2fc5610dbab39 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 19 Jun 2026 15:52:35 +0200 Subject: [PATCH 2/9] refactor: build all conventions via zarr_cm.create_many Route multiscales, spatial, and proj convention metadata through a single utils.build_convention_attrs() helper that delegates to zarr_cm.create_many, so zarr-cm validates each convention and emits its CMO. Previously the CMOs were hand-placed and the spatial:/proj:/multiscales keys assembled by hand, which skipped zarr-cm validation (e.g. multiscales' layout>=1 and derived_from=>transform rules) and duplicated key strings. Type the helper precisely with zarr-cm's TypedDicts (SpatialAttrs, GeoProjAttrs, MultiscalesAttrs, MultiConventionAttrs) and a CRSLike Protocol instead of dict[str, Any]. Multiscales data is still produced by the project's MultiscaleMeta model (it also covers the TMS encoding zarr-cm doesn't model), but its CMO and validation now go through zarr-cm. Output is byte-identical to before (verified against the golden-file snapshot tests). Add tests/test_conversion/test_convention_attrs.py covering the helper, including that zarr-cm now rejects an invalid multiscales layout. Co-Authored-By: Claude Opus 4.8 --- src/eopf_geozarr/conversion/geozarr.py | 47 +++---- src/eopf_geozarr/conversion/utils.py | 64 +++++---- .../s2_optimization/s2_multiscale.py | 55 ++++---- tests/test_conversion/__init__.py | 0 .../test_conversion/test_convention_attrs.py | 129 ++++++++++++++++++ 5 files changed, 214 insertions(+), 81 deletions(-) create mode 100644 tests/test_conversion/__init__.py create mode 100644 tests/test_conversion/test_convention_attrs.py diff --git a/src/eopf_geozarr/conversion/geozarr.py b/src/eopf_geozarr/conversion/geozarr.py index cafa5e45..f34a303e 100644 --- a/src/eopf_geozarr/conversion/geozarr.py +++ b/src/eopf_geozarr/conversion/geozarr.py @@ -18,7 +18,7 @@ import os import time from collections.abc import Hashable, Iterable, Mapping, Sequence -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import numpy as np import structlog @@ -30,13 +30,9 @@ from zarr.core.sync import sync from zarr.storage import StoreLike from zarr.storage._common import make_store_path -from zarr_cm import geo_proj -from zarr_cm import multiscales as multiscales_cm -from zarr_cm import spatial as spatial_cm from eopf_geozarr.data_api.geozarr.multiscales import zcm from eopf_geozarr.data_api.geozarr.multiscales.geozarr import ( - MultiscaleGroupAttrs, MultiscaleMeta, ) from eopf_geozarr.types import ( @@ -52,6 +48,9 @@ from .fs_utils import sanitize_dataset_attributes from .sentinel1_reprojection import reproject_sentinel1_with_gcps +if TYPE_CHECKING: + from zarr_cm import MultiscalesAttrs + log = structlog.get_logger() @@ -672,31 +671,27 @@ def _spatial_transform_for( scale_level_data["spatial:transform"] = spatial_tf layout.append(zcm.ScaleLevel(**scale_level_data)) - multiscale_attrs = MultiscaleGroupAttrs( - zarr_conventions=(multiscales_cm.CMO, spatial_cm.CMO, geo_proj.CMO), - multiscales=MultiscaleMeta( - layout=layout, - resampling_method="average", - ), + # Validate + serialize the multiscales block via the project model (which + # also covers the ZCM/TMS duality), then hand all conventions to zarr-cm, + # which validates each and emits the matching CMOs in order (multiscales, + # spatial, proj). proj is included only when a CRS is available. + multiscales_data = cast( + "MultiscalesAttrs", + MultiscaleMeta(layout=layout, resampling_method="average").model_dump(), + ) + attrs_to_write = utils.build_convention_attrs( + multiscales=multiscales_data, + spatial={ + "spatial:dimensions": ["y", "x"], + "spatial:bbox": list(native_bounds), + "spatial:registration": "pixel", + }, + crs=native_crs or None, ) - # The multiscales/spatial/proj CMOs are declared by MultiscaleGroupAttrs - # above; here we add only the validated spatial/proj data keys. - attrs_to_write = multiscale_attrs.model_dump() - if native_crs and native_bounds: - spatial_proj = utils.build_spatial_proj_attrs( - spatial={ - "spatial:dimensions": ["y", "x"], - "spatial:bbox": list(native_bounds), - "spatial:registration": "pixel", - }, - crs=native_crs, - ) - spatial_proj.pop("zarr_conventions", None) # CMOs come from the model above - attrs_to_write.update(spatial_proj) group_path = fs_utils.normalize_path(f"{output_path}/{group_name.lstrip('/')}") zarr_group = fs_utils.open_zarr_group(group_path, mode="r+") - zarr_group.attrs.update(attrs_to_write) + zarr_group.attrs.update(cast("dict[str, Any]", attrs_to_write)) log.info("Added multiscales metadata to group %s", group_name) diff --git a/src/eopf_geozarr/conversion/utils.py b/src/eopf_geozarr/conversion/utils.py index e6da415a..65e6a709 100644 --- a/src/eopf_geozarr/conversion/utils.py +++ b/src/eopf_geozarr/conversion/utils.py @@ -1,54 +1,72 @@ """Utility functions for GeoZarr conversion.""" -from typing import Any +from typing import Any, Protocol, cast, runtime_checkable import numpy as np import rasterio # noqa: F401 # Import to enable .rio accessor import structlog import xarray as xr import zarr_cm +from zarr_cm import GeoProjAttrs, MultiConventionAttrs, MultiscalesAttrs, SpatialAttrs log = structlog.get_logger() -def proj_attrs_for_crs(crs: Any) -> dict[str, Any]: +@runtime_checkable +class CRSLike(Protocol): + """A coordinate reference system that can serialize to EPSG/WKT2. + + Both ``pyproj.CRS`` and ``rasterio.crs.CRS`` satisfy this; the conversion + code accepts either, so we depend on the shared interface rather than a + concrete class. + """ + + def to_epsg(self) -> int | None: ... + + def to_wkt(self) -> str: ... + + +def proj_attrs_for_crs(crs: CRSLike | None) -> GeoProjAttrs: """Build the ``proj`` convention data keys for a CRS. Prefers an EPSG code (``proj:code``) and falls back to WKT2 - (``proj:wkt2``). Returns an empty dict when *crs* is falsy or exposes - neither representation. + (``proj:wkt2``). Returns an empty mapping when *crs* is ``None`` or exposes + no EPSG code. """ - if not crs: - return {} - if hasattr(crs, "to_epsg") and crs.to_epsg(): - return {"proj:code": f"EPSG:{crs.to_epsg()}"} - if hasattr(crs, "to_wkt"): - return {"proj:wkt2": crs.to_wkt()} - return {} + if crs is None: + return GeoProjAttrs() + epsg = crs.to_epsg() + if epsg: + return GeoProjAttrs({"proj:code": f"EPSG:{epsg}"}) + return GeoProjAttrs({"proj:wkt2": crs.to_wkt()}) -def build_spatial_proj_attrs( +def build_convention_attrs( *, - spatial: dict[str, Any], - crs: Any, -) -> dict[str, Any]: - """Build validated ``spatial`` + ``proj`` convention attributes. + spatial: SpatialAttrs, + crs: CRSLike | None, + multiscales: MultiscalesAttrs | None = None, +) -> MultiConventionAttrs: + """Build validated multiscales + ``spatial`` + ``proj`` convention attributes. Delegates to :func:`zarr_cm.create_many`, which validates each convention's data and emits the matching convention-metadata objects into a combined - ``zarr_conventions`` array (spatial first, then proj). *spatial* holds the - ``spatial:*`` keys; the proj keys are derived from *crs* via - :func:`proj_attrs_for_crs`. + ``zarr_conventions`` array. The CMOs are ordered multiscales (if present), + then spatial, then proj. *spatial* holds the ``spatial:*`` keys; the proj + keys are derived from *crs* via :func:`proj_attrs_for_crs`. The proj convention is only included when *crs* yields a usable CRS - representation; otherwise only the spatial convention is emitted (a proj + representation; otherwise only the other conventions are emitted (a proj convention with no CRS field is invalid). """ - conventions: dict[zarr_cm.ConventionName, dict[str, Any]] = {"spatial": spatial} + conventions: dict[zarr_cm.ConventionName, dict[str, Any]] = {} + if multiscales is not None: + conventions["multiscales"] = dict(multiscales) + conventions["spatial"] = dict(spatial) proj = proj_attrs_for_crs(crs) if proj: - conventions["geo-proj"] = proj - return zarr_cm.create_many(conventions) + conventions["geo-proj"] = dict(proj) + return cast("MultiConventionAttrs", zarr_cm.create_many(conventions)) # Sentinel: distinguish "no explicit fill_value" from a legitimate `None`. diff --git a/src/eopf_geozarr/s2_optimization/s2_multiscale.py b/src/eopf_geozarr/s2_optimization/s2_multiscale.py index 23152d55..1782a80d 100644 --- a/src/eopf_geozarr/s2_optimization/s2_multiscale.py +++ b/src/eopf_geozarr/s2_optimization/s2_multiscale.py @@ -16,15 +16,11 @@ from pydantic.experimental.missing_sentinel import MISSING from pyproj import CRS from zarr.codecs import CastValue -from zarr_cm import geo_proj -from zarr_cm import multiscales as multiscales_cm -from zarr_cm import spatial as spatial_cm from eopf_geozarr.conversion import utils from eopf_geozarr.conversion.fs_utils import sanitize_dataset_attributes from eopf_geozarr.data_api.geozarr.multiscales import zcm from eopf_geozarr.data_api.geozarr.multiscales.geozarr import ( - MultiscaleGroupAttrs, MultiscaleMeta, ) from eopf_geozarr.data_api.geozarr.types import ( @@ -41,6 +37,8 @@ from collections.abc import Hashable, Mapping import zarr + from zarr_cm import MultiscalesAttrs + from zarr_cm import spatial as spatial_cm from eopf_geozarr.types import OverviewLevelJSON @@ -823,36 +821,29 @@ def add_multiscales_metadata_to_parent( scale_level = zcm.ScaleLevel(**scale_level_data) layout.append(scale_level) - # Create convention metadata for all three conventions - multiscale_attrs = MultiscaleGroupAttrs( - zarr_conventions=( - multiscales_cm.CMO, - spatial_cm.CMO, - geo_proj.CMO, - ), - multiscales=MultiscaleMeta( - layout=layout, - resampling_method="average", - ), - ) - # Write multiscale attributes directly to the parent group. The multiscales - # CMO and the spatial/proj CMOs are declared by MultiscaleGroupAttrs above; - # here we add only the validated spatial/proj data keys. - attrs_to_write = multiscale_attrs.model_dump() + # Validate + serialize the multiscales block via the project model (which + # also covers the ZCM/TMS duality), then hand all three conventions to + # zarr-cm, which validates each and emits the matching CMOs in order + # (multiscales, spatial, proj). + multiscales_data = cast( + "MultiscalesAttrs", + MultiscaleMeta(layout=layout, resampling_method="average").model_dump(), + ) - # Add spatial and proj attributes at group level following specifications + attrs_to_write: dict[str, Any] = {} if native_crs and native_bounds: - spatial_proj = utils.build_spatial_proj_attrs( - spatial={ - "spatial:dimensions": ["y", "x"], - "spatial:bbox": list(native_bounds), # [xmin, ymin, xmax, ymax] - "spatial:registration": "pixel", - }, - crs=native_crs, + attrs_to_write.update( + utils.build_convention_attrs( + multiscales=multiscales_data, + spatial={ + "spatial:dimensions": ["y", "x"], + "spatial:bbox": list(native_bounds), # [xmin, ymin, xmax, ymax] + "spatial:registration": "pixel", + }, + crs=native_crs, + ) ) - spatial_proj.pop("zarr_conventions", None) # CMOs come from the model above - attrs_to_write.update(spatial_proj) # Write attributes directly to the zarr group group.attrs.update(attrs_to_write) @@ -1165,7 +1156,7 @@ def write_geo_metadata( # https://github.com/corteva/rioxarray/pull/883 # Assemble spatial convention data - spatial_data: dict[str, Any] = { + spatial_data: spatial_cm.SpatialAttrs = { "spatial:dimensions": ["y", "x"], # Required field "spatial:registration": "pixel", # Default registration type } @@ -1192,7 +1183,7 @@ def write_geo_metadata( spatial_data["spatial:shape"] = [height, width] # Build validated spatial + proj convention attrs (data + CMOs) via zarr-cm - dataset.attrs.update(utils.build_spatial_proj_attrs(spatial=spatial_data, crs=crs)) + dataset.attrs.update(utils.build_convention_attrs(spatial=spatial_data, crs=crs)) def rechunk_dataset_for_encoding( diff --git a/tests/test_conversion/__init__.py b/tests/test_conversion/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_conversion/test_convention_attrs.py b/tests/test_conversion/test_convention_attrs.py new file mode 100644 index 00000000..0b3ab578 --- /dev/null +++ b/tests/test_conversion/test_convention_attrs.py @@ -0,0 +1,129 @@ +"""Tests for the zarr-cm convention-attribute helpers in conversion.utils.""" + +from __future__ import annotations + +from typing import Any + +import pytest +from pyproj import CRS +from zarr_cm import geo_proj +from zarr_cm import spatial as spatial_cm + +from eopf_geozarr.conversion.utils import ( + build_convention_attrs, + proj_attrs_for_crs, +) + + +def test_proj_attrs_for_crs_epsg() -> None: + """A CRS with an EPSG code yields proj:code.""" + crs = CRS.from_epsg(32632) + assert proj_attrs_for_crs(crs) == {"proj:code": "EPSG:32632"} + + +def test_proj_attrs_for_crs_wkt_fallback() -> None: + """A CRS without an EPSG code falls back to proj:wkt2.""" + # A bare WKT-defined CRS with no authority code. + crs = CRS.from_wkt(CRS.from_epsg(4326).to_wkt()) + out = proj_attrs_for_crs(crs) + # from_wkt of an EPSG CRS still resolves an epsg in most pyproj versions, so + # accept either, but the key must be one of the two proj CRS keys. + assert set(out) <= {"proj:code", "proj:wkt2"} + assert len(out) == 1 + + +def test_proj_attrs_for_crs_none() -> None: + """No CRS yields an empty dict (no proj keys).""" + assert proj_attrs_for_crs(None) == {} + + +def test_build_convention_attrs_data_and_cmos() -> None: + """The helper emits spatial+proj data keys plus their CMOs, in order.""" + bbox = [300000.0, 4990200.0, 409800.0, 5100000.0] + out = build_convention_attrs( + spatial={ + "spatial:dimensions": ["y", "x"], + "spatial:bbox": bbox, + "spatial:registration": "pixel", + }, + crs=CRS.from_epsg(32632), + ) + assert out["spatial:dimensions"] == ["y", "x"] + assert out["spatial:bbox"] == bbox + assert out["spatial:registration"] == "pixel" + assert out["proj:code"] == "EPSG:32632" + names = [c["name"] for c in out["zarr_conventions"]] + assert names == [spatial_cm.CMO["name"], geo_proj.CMO["name"]] + + +def test_build_convention_attrs_matches_handwritten() -> None: + """Output is byte-equivalent to the previous hand-assembled dict.""" + bbox = [300000.0, 4990200.0, 409800.0, 5100000.0] + hand: dict[str, Any] = { + "spatial:dimensions": ["y", "x"], + "spatial:bbox": bbox, + "spatial:registration": "pixel", + "proj:code": "EPSG:32632", + } + out = build_convention_attrs( + spatial={ + "spatial:dimensions": ["y", "x"], + "spatial:bbox": bbox, + "spatial:registration": "pixel", + }, + crs=CRS.from_epsg(32632), + ) + data = {k: v for k, v in out.items() if k != "zarr_conventions"} + assert data == hand + + +def test_build_convention_attrs_validates() -> None: + """Invalid spatial data is rejected by zarr-cm validation.""" + with pytest.raises(ValueError, match="spatial:dimensions"): + build_convention_attrs( + spatial={"spatial:registration": "pixel"}, # missing required dimensions + crs=CRS.from_epsg(32632), + ) + + +def test_build_convention_attrs_with_multiscales() -> None: + """With multiscales, CMOs are ordered [multiscales, spatial, proj].""" + from zarr_cm import multiscales as multiscales_cm + + out = build_convention_attrs( + multiscales={ + "layout": [ + {"asset": "r10m", "spatial:shape": [10980, 10980]}, + { + "asset": "r20m", + "derived_from": "r10m", + "transform": {"scale": [2.0, 2.0], "translation": [0.0, 0.0]}, + "spatial:shape": [5490, 5490], + }, + ], + "resampling_method": "average", + }, + spatial={"spatial:dimensions": ["y", "x"]}, + crs=CRS.from_epsg(32632), + ) + names = [c["name"] for c in out["zarr_conventions"]] + assert names == [ + multiscales_cm.CMO["name"], + spatial_cm.CMO["name"], + geo_proj.CMO["name"], + ] + # extra layout keys (spatial:shape) survive the round-trip + assert out["multiscales"]["layout"][0]["spatial:shape"] == [10980, 10980] + + +def test_build_convention_attrs_multiscales_validation() -> None: + """zarr-cm rejects a layout entry with derived_from but no transform.""" + with pytest.raises(ValueError, match="transform"): + build_convention_attrs( + multiscales={ + "layout": [{"asset": "r20m", "derived_from": "r10m"}], # no transform + "resampling_method": "average", + }, + spatial={"spatial:dimensions": ["y", "x"]}, + crs=CRS.from_epsg(32632), + ) From 4a5b6e45d96da3fdaf5e5d888e49d11da825c474 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 19 Jun 2026 15:54:51 +0200 Subject: [PATCH 3/9] chore: gitignore local scratch files and .claude/ Ignore root-level scratch files (test.py, tmp.json, cli_test.sh) and the machine-local .claude/ session directory so they stop showing in git status. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index 0fa04a75..0722a277 100644 --- a/.gitignore +++ b/.gitignore @@ -218,3 +218,11 @@ uv.lock # VCS versioning src/eopf_geozarr/_version.py analysis/.edh_token + +# Claude Code local session/config +.claude/ + +# Local scratch files (root-level, anchored so nested fixtures are unaffected) +/test.py +/tmp.json +/cli_test.sh From 6e2e14585297869c6626d37cafe7d1d4d2ce1716 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 19 Jun 2026 15:56:47 +0200 Subject: [PATCH 4/9] Revert "chore: gitignore local scratch files and .claude/" This reverts commit 4a5b6e45d96da3fdaf5e5d888e49d11da825c474. --- .gitignore | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.gitignore b/.gitignore index 0722a277..0fa04a75 100644 --- a/.gitignore +++ b/.gitignore @@ -218,11 +218,3 @@ uv.lock # VCS versioning src/eopf_geozarr/_version.py analysis/.edh_token - -# Claude Code local session/config -.claude/ - -# Local scratch files (root-level, anchored so nested fixtures are unaffected) -/test.py -/tmp.json -/cli_test.sh From f1c0277820baf0c74d7d44fa7cf242a0dd1337a0 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 19 Jun 2026 16:15:35 +0200 Subject: [PATCH 5/9] fix type checking errors --- .github/workflows/ci.yml | 10 ++++++ pyproject.toml | 2 +- src/eopf_geozarr/cli.py | 4 +-- src/eopf_geozarr/conversion/fs_utils.py | 22 +++++++++---- .../conversion/sentinel1_reprojection.py | 8 +++-- .../data_api/geozarr/multiscales/geozarr.py | 7 ++++- src/eopf_geozarr/data_api/geozarr/v2.py | 31 +++++++++++++------ src/eopf_geozarr/data_api/geozarr/v3.py | 14 +++++++-- src/eopf_geozarr/pyz/v2.py | 8 +++-- src/eopf_geozarr/pyz/v3.py | 8 +++-- .../s2_optimization/s2_converter.py | 25 ++++++++------- .../s2_optimization/s2_data_consolidator.py | 2 +- .../test_convention_attrs.py | 0 tests/test_conversion/__init__.py | 0 14 files changed, 100 insertions(+), 41 deletions(-) rename tests/{test_conversion => }/test_convention_attrs.py (100%) delete mode 100644 tests/test_conversion/__init__.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00f7ca57..2c3a3845 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,16 @@ jobs: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: '3.12' + - name: Install uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "0.8.4" + python-version: "3.13" + enable-cache: false + - name: Install dependencies + # The mypy hook runs `uv run mypy` so it sees real dependency types; + # sync the project env so that command resolves. + run: uv sync --group dev --group test - name: Install pre-commit run: pip install pre-commit - name: Run pre-commit diff --git a/pyproject.toml b/pyproject.toml index ca94c6b1..676f3fe3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -172,7 +172,7 @@ warn_required_dynamic_aliases = true warn_untyped_fields = true [[tool.mypy.overrides]] -module = ["zarr.*", "xarray.*", "rioxarray.*", "rasterio.*", "cf_xarray.*", "dask.*"] +module = ["zarr.*", "xarray.*", "rioxarray.*", "rasterio.*", "cf_xarray.*", "dask.*", "s3fs.*", "fsspec.*"] ignore_missing_imports = true [[tool.mypy.overrides]] diff --git a/src/eopf_geozarr/cli.py b/src/eopf_geozarr/cli.py index 53c3ee21..805c9237 100755 --- a/src/eopf_geozarr/cli.py +++ b/src/eopf_geozarr/cli.py @@ -354,7 +354,7 @@ def format_data_vars(data_vars: dict[str, xr.DataArray]) -> str: # Fallback to simple format if xarray HTML fails vars_html = [] for name, var in data_vars.items(): - dims_str = format_dimensions(dict(zip(var.dims, var.shape, strict=True))) + dims_str = format_dimensions(dict(zip(map(str, var.dims), var.shape, strict=True))) dtype_str = str(var.dtype) vars_html.append( f""" @@ -450,7 +450,7 @@ def render_node(node: xr.DataTree, path: str = "", level: int = 0) -> str:

Variables

- {format_data_vars(node.ds.data_vars)} + {format_data_vars({str(k): v for k, v in node.ds.data_vars.items()})}
""" diff --git a/src/eopf_geozarr/conversion/fs_utils.py b/src/eopf_geozarr/conversion/fs_utils.py index f55a3ab0..44b33457 100644 --- a/src/eopf_geozarr/conversion/fs_utils.py +++ b/src/eopf_geozarr/conversion/fs_utils.py @@ -3,7 +3,7 @@ import json import os from collections.abc import Mapping -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal, cast from urllib.parse import urlparse import s3fs @@ -87,7 +87,7 @@ def sanitize_dataset_attributes(ds: "xr.Dataset") -> "xr.Dataset": ds_clean = ds.copy() # Sanitize dataset attributes - ds_clean.attrs = replace_json_invalid_floats(ds_clean.attrs) + ds_clean.attrs = cast("dict[str, Any]", replace_json_invalid_floats(ds_clean.attrs)) # Sanitize variable attributes for var_name in ds_clean.data_vars: @@ -95,14 +95,14 @@ def sanitize_dataset_attributes(ds: "xr.Dataset") -> "xr.Dataset": # Preserve _FillValue as-is — xarray encodes it via FillValueCoder on write; # converting np.nan to the string "NaN" would break FillValueCoder.encode. fill_value = var.attrs.get("_FillValue", _MISSING) - var.attrs = replace_json_invalid_floats(var.attrs) + var.attrs = cast("dict[str, Any]", replace_json_invalid_floats(var.attrs)) if fill_value is not _MISSING: var.attrs["_FillValue"] = fill_value # Sanitize coordinate attributes for coord_name in ds_clean.coords: coord = ds_clean[coord_name] - coord.attrs = replace_json_invalid_floats(coord.attrs) + coord.attrs = cast("dict[str, Any]", replace_json_invalid_floats(coord.attrs)) return ds_clean @@ -405,7 +405,12 @@ def open_s3_zarr_group(s3_path: str, mode: str = "r", **s3_kwargs: Any) -> zarr. Zarr group """ storage_options = get_s3_storage_options(s3_path, **s3_kwargs) - return zarr.open_group(s3_path, mode=mode, zarr_format=3, storage_options=storage_options) + return zarr.open_group( + s3_path, + mode=cast('Literal["r", "r+", "a", "w", "w-"]', mode), + zarr_format=3, + storage_options=cast("dict[str, Any]", storage_options), + ) def get_s3_credentials_info() -> S3Credentials: @@ -589,4 +594,9 @@ def open_zarr_group(path: str, mode: str = "r", **kwargs: Any) -> zarr.Group: Zarr group """ storage_options = get_storage_options(path, **kwargs) - return zarr.open_group(path, mode=mode, zarr_format=3, storage_options=storage_options) + return zarr.open_group( + path, + mode=cast('Literal["r", "r+", "a", "w", "w-"]', mode), + zarr_format=3, + storage_options=cast("dict[str, Any] | None", storage_options), + ) diff --git a/src/eopf_geozarr/conversion/sentinel1_reprojection.py b/src/eopf_geozarr/conversion/sentinel1_reprojection.py index be90abe3..881706bc 100644 --- a/src/eopf_geozarr/conversion/sentinel1_reprojection.py +++ b/src/eopf_geozarr/conversion/sentinel1_reprojection.py @@ -5,6 +5,8 @@ to geographic coordinates (lat/lon) using Ground Control Points (GCPs). """ +from typing import Any, cast + import numpy as np import rasterio import rioxarray # noqa: F401 # Import to enable .rio accessor @@ -105,7 +107,7 @@ def reproject_sentinel1_with_gcps( reprojected_ds = reprojected_ds.rio.write_crs(target_crs) log.info("✅ Successfully reprojected Sentinel-1 data", target_crs=target_crs) - return reprojected_ds + return cast("xr.Dataset", reprojected_ds) def _create_gcps_from_dataset( @@ -178,7 +180,7 @@ def _create_target_coordinates( } -def _determine_nodata_value(data_var: xr.DataArray) -> float | np.floating: +def _determine_nodata_value(data_var: xr.DataArray) -> float: """ Determine appropriate nodata value based on data type and existing attributes. @@ -289,7 +291,7 @@ def _reproject_2d_array( # Initialize destination array with nodata values if np.isnan(nodata_value): dst_array = np.full((dst_height, dst_width), np.nan, dtype=np.float32) - dst_dtype = np.float32 + dst_dtype: np.dtype[Any] | type[np.floating[Any]] = np.float32 else: dst_array = np.full((dst_height, dst_width), nodata_value, dtype=src_array.dtype) dst_dtype = src_array.dtype diff --git a/src/eopf_geozarr/data_api/geozarr/multiscales/geozarr.py b/src/eopf_geozarr/data_api/geozarr/multiscales/geozarr.py index d1a5efd3..5cbd6772 100644 --- a/src/eopf_geozarr/data_api/geozarr/multiscales/geozarr.py +++ b/src/eopf_geozarr/data_api/geozarr/multiscales/geozarr.py @@ -78,7 +78,12 @@ def valid_zcm_and_tms(self) -> Self: resampling_method=self.multiscales.resampling_method, # type: ignore[arg-type] tile_matrix_set=self.multiscales.tile_matrix_set, ) - if self._tms_multiscales is None and self._zcm_multiscales is None: + # mypy treats the ``is not MISSING`` guards above as always-true (see the + # ``comparison-overlap`` ignores), so it wrongly concludes both attributes are always set + # and that this branch's right operand is unreachable. The runtime check is required. + if ( + self._tms_multiscales is None and self._zcm_multiscales is None # type: ignore[unreachable] + ): raise ValueError("Either ZCM multiscales or TMS multiscales must be present") return self diff --git a/src/eopf_geozarr/data_api/geozarr/v2.py b/src/eopf_geozarr/data_api/geozarr/v2.py index 6e889db7..772f13d0 100644 --- a/src/eopf_geozarr/data_api/geozarr/v2.py +++ b/src/eopf_geozarr/data_api/geozarr/v2.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal, Self +from typing import TYPE_CHECKING, Any, Literal, Self, cast from pydantic import ConfigDict, Field, model_validator from pydantic_zarr.v2 import ArraySpec, GroupSpec, auto_attributes @@ -10,7 +10,9 @@ from eopf_geozarr.data_api.geozarr.common import ( BaseDataArrayAttrs, DatasetAttrs, + DatasetLike, GridMappingAttrs, + GroupLike, check_grid_mapping, check_valid_coordinates, ) @@ -52,8 +54,11 @@ class DataArray(ArraySpec[DataArrayAttrs]): https://github.com/zarr-developers/geozarr-spec/blob/main/geozarr-spec.md#geozarr-dataarray """ + # The override intentionally widens the accepted argument types (e.g. plain mappings for + # ``attributes``) and adds a ``dimension_names`` parameter, so the signature is not a strict + # subtype of the parent's. This is by design and does not change runtime behavior. @classmethod - def from_array( + def from_array( # type: ignore[override] cls, array: Any, chunks: tuple[int, ...] | Literal["auto"] = "auto", @@ -71,13 +76,15 @@ def from_array( auto_attrs = dict(auto_attributes(array)) if attributes == "auto" else dict(attributes) if dimension_names != "auto": auto_attrs = auto_attrs | {XARRAY_DIMS_KEY: tuple(dimension_names)} - return super().from_array( # type: ignore[no-any-return] + # ``auto_attrs``/``fill_value``/``filters`` are validated/coerced by pydantic at + # construction time; cast to the parent's declared types to satisfy the static checker. + return super().from_array( array=array, chunks=chunks, - attributes=auto_attrs, - fill_value=fill_value, + attributes=cast("Literal['auto'] | DataArrayAttrs", auto_attrs), + fill_value=cast("Literal['auto'] | float | None", fill_value), order=order, - filters=filters, + filters=cast("Literal['auto'] | list[dict[str, Any]] | None", filters), dimension_separator=dimension_separator, compressor=compressor, ) @@ -94,7 +101,7 @@ def check_array_dimensions(self) -> Self: @property def array_dimensions(self) -> tuple[str, ...]: - return self.attributes.array_dimensions # type: ignore[no-any-return] + return self.attributes.array_dimensions class GridMappingVariable(ArraySpec[GridMappingAttrs]): @@ -127,11 +134,17 @@ def check_valid_coordinates(self) -> Self: GroupSpec[Any, Any] The validated GeoZarr DataSet. """ - return check_valid_coordinates(self) + # ``self`` structurally satisfies the ``GroupLike`` protocol, but mypy cannot bind the + # helper's TypeVar to ``Self``; cast through the protocol and back to ``Self`` (the helper + # returns the same object). + check_valid_coordinates(cast("GroupLike", self)) + return self @model_validator(mode="after") def check_grid_mapping(self) -> Self: - return check_grid_mapping(self) + # See note above: ``self`` satisfies ``DatasetLike`` but the TypeVar can't bind to ``Self``. + check_grid_mapping(cast("DatasetLike", self)) + return self class MultiscaleGroup(GroupSpec[MultiscaleGroupAttrs, DataArray | GroupSpec[Any, Any]]): diff --git a/src/eopf_geozarr/data_api/geozarr/v3.py b/src/eopf_geozarr/data_api/geozarr/v3.py index 9359b22d..f9e134bd 100644 --- a/src/eopf_geozarr/data_api/geozarr/v3.py +++ b/src/eopf_geozarr/data_api/geozarr/v3.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Self +from typing import Any, Self, cast from pydantic import model_validator from pydantic_zarr.v3 import ArraySpec, GroupSpec @@ -10,6 +10,8 @@ from eopf_geozarr.data_api.geozarr.common import ( BaseDataArrayAttrs, DatasetAttrs, + DatasetLike, + GroupLike, check_grid_mapping, check_valid_coordinates, ) @@ -55,11 +57,17 @@ def check_valid_coordinates(self) -> Self: GroupSpec[Any, Any] The validated GeoZarr DataSet. """ - return check_valid_coordinates(self) + # ``self`` structurally satisfies the ``GroupLike`` protocol, but mypy cannot bind the + # helper's TypeVar to ``Self``; cast through the protocol and back to ``Self`` (the helper + # returns the same object). + check_valid_coordinates(cast("GroupLike", self)) + return self @model_validator(mode="after") def validate_grid_mapping(self) -> Self: - return check_grid_mapping(self) + # See note above: ``self`` satisfies ``DatasetLike`` but the TypeVar can't bind to ``Self``. + check_grid_mapping(cast("DatasetLike", self)) + return self class MultiscaleGroup(GroupSpec[MultiscaleGroupAttrs, DataArray | GroupSpec[Any, Any]]): diff --git a/src/eopf_geozarr/pyz/v2.py b/src/eopf_geozarr/pyz/v2.py index 052834fb..52605eb1 100644 --- a/src/eopf_geozarr/pyz/v2.py +++ b/src/eopf_geozarr/pyz/v2.py @@ -40,9 +40,13 @@ class MyGroup(GroupSpec[Any, MyMembers]) TArraySpecType = TypeVar("TArraySpecType") -class GroupSpec(GroupSpecV2[TAttr, TMembers]): +class GroupSpec(GroupSpecV2[TAttr, TMembers]): # type: ignore[type-var] + # TMembers is bound to the full members mapping (e.g. a TypedDict) by design, + # whereas the parent's second type parameter expects a single member item type. attributes: TAttr - members: TMembers + # members holds the full mapping (TMembers) rather than the parent's + # Mapping[str, TItem] | None; this is the intended structure for this subclass. + members: TMembers # type: ignore[assignment] def __repr__(self) -> str: """Return a condensed text representation of the GroupSpec.""" diff --git a/src/eopf_geozarr/pyz/v3.py b/src/eopf_geozarr/pyz/v3.py index fabdf4e6..405c8590 100644 --- a/src/eopf_geozarr/pyz/v3.py +++ b/src/eopf_geozarr/pyz/v3.py @@ -40,9 +40,13 @@ class MyGroup(GroupSpec[Any, MyMembers]) TArraySpecType = TypeVar("TArraySpecType") -class GroupSpec(GroupSpecV3[TAttr, TMembers]): +class GroupSpec(GroupSpecV3[TAttr, TMembers]): # type: ignore[type-var] + # TMembers is bound to the full members mapping (e.g. a TypedDict) by design, + # whereas the parent's second type parameter expects a single member item type. attributes: TAttr - members: TMembers + # members holds the full mapping (TMembers) rather than the parent's + # Mapping[str, TItem] | None; this is the intended structure for this subclass. + members: TMembers # type: ignore[assignment] def __repr__(self) -> str: """Return a condensed text representation of the GroupSpec.""" diff --git a/src/eopf_geozarr/s2_optimization/s2_converter.py b/src/eopf_geozarr/s2_optimization/s2_converter.py index fd303b69..05d2df5c 100644 --- a/src/eopf_geozarr/s2_optimization/s2_converter.py +++ b/src/eopf_geozarr/s2_optimization/s2_converter.py @@ -5,7 +5,7 @@ from __future__ import annotations import time -from typing import Any, TypedDict +from typing import TYPE_CHECKING, Any, TypedDict, cast import structlog import xarray as xr @@ -20,6 +20,9 @@ from .s2_multiscale import create_multiscale_from_datatree +if TYPE_CHECKING: + from collections.abc import Sequence + log = structlog.get_logger() @@ -69,10 +72,10 @@ def initialize_crs_from_dataset(dt_input: xr.DataTree) -> CRS | None: # Check if dataset has rio accessor with CRS if hasattr(dataset, "rio"): try: - crs = dataset.rio.crs - if crs is not None: - log.info("Initialized CRS from dataset", crs=str(crs)) - return crs + ds_crs = cast("CRS | None", dataset.rio.crs) + if ds_crs is not None: + log.info("Initialized CRS from dataset", crs=str(ds_crs)) + return ds_crs except Exception: log.debug("Failed to get CRS from dataset rio accessor") @@ -80,10 +83,10 @@ def initialize_crs_from_dataset(dt_input: xr.DataTree) -> CRS | None: for var in dataset.data_vars.values(): if hasattr(var, "rio"): try: - crs = var.rio.crs - if crs is not None: - log.info("Initialized CRS from variable", crs=str(crs)) - return crs + var_crs = cast("CRS | None", var.rio.crs) + if var_crs is not None: + log.info("Initialized CRS from variable", crs=str(var_crs)) + return var_crs except Exception: log.debug("Failed to get CRS from variable rio accessor") @@ -334,7 +337,7 @@ def write_store_root_bbox(output_path: str) -> None: def _walk(group: zarr.Group) -> None: attrs = dict(group.attrs) - bbox = attrs.get("spatial:bbox") + bbox = cast("Sequence[float] | None", attrs.get("spatial:bbox")) code = attrs.get("proj:code") if bbox is not None and len(bbox) == 4: if code and code != "EPSG:4326": @@ -343,7 +346,7 @@ def _walk(group: zarr.Group) -> None: xmax, ymax = transformer.transform(bbox[2], bbox[3]) bboxes_4326.append((xmin, ymin, xmax, ymax)) else: - bboxes_4326.append(tuple(float(v) for v in bbox)) # type: ignore[arg-type] + bboxes_4326.append((float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3]))) for child in group.groups(): _walk(child[1]) diff --git a/src/eopf_geozarr/s2_optimization/s2_data_consolidator.py b/src/eopf_geozarr/s2_optimization/s2_data_consolidator.py index 7ca56db5..67cf071c 100644 --- a/src/eopf_geozarr/s2_optimization/s2_data_consolidator.py +++ b/src/eopf_geozarr/s2_optimization/s2_data_consolidator.py @@ -159,7 +159,7 @@ def _extract_geometry_data(self) -> None: # Consolidate all geometry variables for var_name in ds.data_vars: - self.geometry_data[var_name] = ds[var_name] + self.geometry_data[str(var_name)] = ds[var_name] def _extract_meteorology_data(self) -> None: """Extract meteorological data (CAMS and ECMWF).""" diff --git a/tests/test_conversion/test_convention_attrs.py b/tests/test_convention_attrs.py similarity index 100% rename from tests/test_conversion/test_convention_attrs.py rename to tests/test_convention_attrs.py diff --git a/tests/test_conversion/__init__.py b/tests/test_conversion/__init__.py deleted file mode 100644 index e69de29b..00000000 From 931bf7810c75c6bc6f4002123691c0ec595f9457 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 19 Jun 2026 16:23:00 +0200 Subject: [PATCH 6/9] wire up mypy correctly in pre-commit --- .github/workflows/ci.yml | 34 +++++++++++++--------------------- .pre-commit-config.yaml | 11 ++++------- pyproject.toml | 1 + 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c3a3845..068efbe8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,29 +10,21 @@ permissions: contents: read jobs: - pre-commit: + lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 - with: - python-version: '3.12' - - name: Install uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - with: - version: "0.8.4" - python-version: "3.13" - enable-cache: false - - name: Install dependencies - # The mypy hook runs `uv run mypy` so it sees real dependency types; - # sync the project env so that command resolves. - run: uv sync --group dev --group test - - name: Install pre-commit - run: pip install pre-commit - - name: Run pre-commit - run: pre-commit run --all-files + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + - name: Install uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + enable-cache: true + - uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4 test: runs-on: ${{ matrix.os }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8fcd0edf..ccf1611e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,11 +15,8 @@ repos: hooks: - id: mypy name: mypy - entry: uv run mypy language: system - types: [python] - exclude: tests/.* - # Run from the project's locked environment so mypy sees the real, - # py.typed types of every dependency (zarr-cm, pydantic, ...) instead - # of treating them as Any. Staged files are passed through, matching - # the previous hook's per-file scope. + entry: uv run --frozen mypy + pass_filenames: false + always_run: true + types_or: [python, pyi] diff --git a/pyproject.toml b/pyproject.toml index 676f3fe3..5ae8e0a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,6 +150,7 @@ ignore = [ ] [tool.mypy] +files = ["src", "tests"] python_version = "3.12" warn_return_any = true warn_unused_configs = true From 807d645199747a096ca0d71f6ae3603012903934 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Sun, 21 Jun 2026 20:59:22 +0200 Subject: [PATCH 7/9] build: migrate type checker mypy -> pyright; upgrade zarr-cm zarr-cm's convention TypedDicts now use PEP 728 `extra_items`, which mypy does not support. Switch the type checker to pyright (which does) and upgrade to the zarr-cm main git dep that also ships the supporting fixes: - Mapping-covariant aggregate API + exported JsonType/JsonValue/JsonDict, so build_convention_attrs no longer needs a private `_core` import or a cast. - PEP 695 `type JsonValue` alias, so pydantic resolves MultiscaleGroupAttrs' ConventionMetadataObject field directly (removed the model_rebuild workaround). Toolchain: - pyproject: replace [tool.mypy] with [tool.pyright]; mypy -> pyright dev dep. - .pre-commit-config / CI lint job: run `uv run --frozen pyright`. Type fixes across src/ and tests/ to reach a clean pyright run (0 errors): - remove stale mypy `# type: ignore[...]` comments (pyright-unnecessary); - narrow NotRequired TypedDict access (`.get()` + guard) instead of making keys Required, preserving runtime validation of optional Sentinel-1/2 members; - replace casts on external/untyped data with runtime isinstance/validation; - model construction via `Model.model_validate(...)` instead of `**dict`. No `typing.Any` introduced. Runtime behavior unchanged. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 4 + .pre-commit-config.yaml | 6 +- pyproject.toml | 52 +- src/eopf_geozarr/conversion/fs_utils.py | 48 +- src/eopf_geozarr/conversion/geozarr.py | 46 +- .../conversion/sentinel1_reprojection.py | 4 + src/eopf_geozarr/conversion/utils.py | 13 +- src/eopf_geozarr/data_api/geozarr/common.py | 2 +- .../data_api/geozarr/multiscales/geozarr.py | 33 +- .../data_api/geozarr/multiscales/zcm.py | 25 +- src/eopf_geozarr/data_api/geozarr/store.py | 10 +- src/eopf_geozarr/data_api/geozarr/v3.py | 5 +- src/eopf_geozarr/data_api/s1.py | 562 +++++++++++++----- src/eopf_geozarr/data_api/s2.py | 72 ++- src/eopf_geozarr/pyz/common.py | 15 +- src/eopf_geozarr/pyz/v3.py | 2 +- .../s2_optimization/s2_converter.py | 60 +- .../s2_optimization/s2_multiscale.py | 23 +- tests/conftest.py | 21 +- tests/test_array_attrs.py | 9 +- tests/test_cli_e2e.py | 4 +- tests/test_convention_attrs.py | 69 ++- tests/test_data_api/conftest.py | 7 +- tests/test_data_api/test_geoproj.py | 24 +- .../test_data_api/test_geozarr/test_common.py | 33 +- .../test_multiscales/test_geozarr.py | 17 +- .../test_geozarr/test_multiscales/test_zcm.py | 4 +- tests/test_data_api/test_projjson.py | 184 +++--- tests/test_data_api/test_s1.py | 4 +- tests/test_data_api/test_s2.py | 4 +- tests/test_data_api/test_spatial.py | 50 +- tests/test_data_api/test_v2.py | 17 +- tests/test_data_api/test_v3.py | 23 +- tests/test_docs.py | 5 +- tests/test_fs_utils.py | 31 +- tests/test_integration_sentinel1.py | 12 +- tests/test_integration_sentinel2.py | 6 +- tests/test_reprojection_validation.py | 4 +- tests/test_s2_converter_simplified.py | 4 +- tests/test_s2_data_consolidator.py | 40 +- tests/test_s2_multiscale.py | 53 +- tests/test_scale_offset.py | 2 +- tests/test_titiler_integration.py | 6 +- uv.lock | 172 +----- 44 files changed, 1066 insertions(+), 721 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 068efbe8..b059bc53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,10 @@ jobs: uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: true + - name: Install dependencies + # The pyright pre-commit hook runs `uv run --frozen pyright`, so the + # project environment must be present. + run: uv sync --group dev --group test - uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4 test: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccf1611e..52e24ad7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,10 +13,10 @@ repos: - repo: local hooks: - - id: mypy - name: mypy + - id: pyright + name: pyright language: system - entry: uv run --frozen mypy + entry: uv run --frozen pyright pass_filenames: false always_run: true types_or: [python, pyi] diff --git a/pyproject.toml b/pyproject.toml index 5ae8e0a1..9e8eedad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "rioxarray>=0.13.0", "cf-xarray>=0.8.0", "typing-extensions>=4.15.0", - "zarr-cm>=0.3.0", + "zarr-cm @ git+https://github.com/zarr-conventions/zarr-cm.git@main", "aiohttp>=3.14.0", "s3fs>=2024.6.0", "boto3>=1.34.0", @@ -48,7 +48,7 @@ dependencies = [ dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", - "mypy>=1.0.0", + "pyright>=1.1.390", "pre-commit>=3.0.0", "bandit[toml]>=1.7.0", ] @@ -149,41 +149,19 @@ ignore = [ "TRY003", # Long exception messages outside class - common pattern ] -[tool.mypy] -files = ["src", "tests"] -python_version = "3.12" -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -check_untyped_defs = true -disallow_untyped_decorators = true -no_implicit_optional = true -warn_redundant_casts = true -warn_unused_ignores = true -warn_no_return = true -warn_unreachable = true -strict_equality = true -plugins = ["pydantic.mypy"] - -[tool.pydantic-mypy] -init_forbid_extra = true -init_typed = true -warn_required_dynamic_aliases = true -warn_untyped_fields = true - -[[tool.mypy.overrides]] -module = ["zarr.*", "xarray.*", "rioxarray.*", "rasterio.*", "cf_xarray.*", "dask.*", "s3fs.*", "fsspec.*"] -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = [ - "eopf_geozarr.data_api.s1", - "eopf_geozarr.data_api.s2", - "eopf_geozarr.data_api.geozarr.v2", - "eopf_geozarr.data_api.geozarr.store", -] -disable_error_code = ["valid-type"] +[tool.pyright] +include = ["src", "tests"] +pythonVersion = "3.12" +typeCheckingMode = "standard" +# Several runtime deps ship no type stubs; we can't fix their types here, so +# don't report missing stubs/sources for them. (Imports still resolve because +# the packages are installed in the environment.) +reportMissingTypeStubs = false +reportMissingModuleSource = false +# Match the strictness we relied on under mypy. +reportUnnecessaryTypeIgnoreComment = true +reportReturnType = "error" +reportUnnecessaryCast = "error" [tool.pytest.ini_options] minversion = "7.0" diff --git a/src/eopf_geozarr/conversion/fs_utils.py b/src/eopf_geozarr/conversion/fs_utils.py index 44b33457..b31ed878 100644 --- a/src/eopf_geozarr/conversion/fs_utils.py +++ b/src/eopf_geozarr/conversion/fs_utils.py @@ -3,7 +3,7 @@ import json import os from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import TYPE_CHECKING, Any, Final, Literal, cast from urllib.parse import urlparse import s3fs @@ -16,6 +16,8 @@ if TYPE_CHECKING: import xarray as xr +ZarrOpenMode = Literal["r", "r+", "a", "w", "w-"] + _MISSING = object() # sentinel for missing optional attrs @@ -52,18 +54,44 @@ def replace_json_invalid_floats(obj: object) -> object: return obj +def _sanitize_attrs(attrs: Mapping[str, object]) -> dict[str, object]: + """Sanitize an attributes mapping, returning a typed ``dict``. + + Wraps :func:`replace_json_invalid_floats` (which is typed ``object -> object``) + and verifies the dict-in/dict-out invariant at runtime instead of casting. + """ + sanitized = replace_json_invalid_floats(dict(attrs)) + if not isinstance(sanitized, dict): # pragma: no cover - invariant guard + raise TypeError(f"expected a dict after sanitizing attrs, got {type(sanitized).__name__}") + return sanitized + + +_ZARR_MODES: Final = ("r", "r+", "a", "w", "w-") + + +def _zarr_mode(mode: str) -> ZarrOpenMode: + """Validate *mode* against zarr's accepted access modes and narrow its type. + + Checks the value at runtime instead of casting a bare ``str`` to the + ``Literal`` zarr expects. + """ + if mode not in _ZARR_MODES: + raise ValueError(f"Invalid zarr access mode {mode!r}; expected one of {_ZARR_MODES}") + return mode + + class NanCompatibleJSONEncoder(json.JSONEncoder): """ Custom JSON encoder that converts NaN, Inf, -Inf values to JSON-safe equivalents to ensure valid JSON output. """ - def encode(self, obj: Any) -> str: + def encode(self, o: Any) -> str: """ Encode object to JSON string, converting NaN values to "NaN". """ - converted_obj = replace_json_invalid_floats(obj) + converted_obj = replace_json_invalid_floats(o) return super().encode(converted_obj) @@ -87,7 +115,7 @@ def sanitize_dataset_attributes(ds: "xr.Dataset") -> "xr.Dataset": ds_clean = ds.copy() # Sanitize dataset attributes - ds_clean.attrs = cast("dict[str, Any]", replace_json_invalid_floats(ds_clean.attrs)) + ds_clean.attrs = _sanitize_attrs(ds_clean.attrs) # Sanitize variable attributes for var_name in ds_clean.data_vars: @@ -95,14 +123,14 @@ def sanitize_dataset_attributes(ds: "xr.Dataset") -> "xr.Dataset": # Preserve _FillValue as-is — xarray encodes it via FillValueCoder on write; # converting np.nan to the string "NaN" would break FillValueCoder.encode. fill_value = var.attrs.get("_FillValue", _MISSING) - var.attrs = cast("dict[str, Any]", replace_json_invalid_floats(var.attrs)) + var.attrs = _sanitize_attrs(var.attrs) if fill_value is not _MISSING: var.attrs["_FillValue"] = fill_value # Sanitize coordinate attributes for coord_name in ds_clean.coords: coord = ds_clean[coord_name] - coord.attrs = cast("dict[str, Any]", replace_json_invalid_floats(coord.attrs)) + coord.attrs = _sanitize_attrs(coord.attrs) return ds_clean @@ -407,9 +435,9 @@ def open_s3_zarr_group(s3_path: str, mode: str = "r", **s3_kwargs: Any) -> zarr. storage_options = get_s3_storage_options(s3_path, **s3_kwargs) return zarr.open_group( s3_path, - mode=cast('Literal["r", "r+", "a", "w", "w-"]', mode), + mode=_zarr_mode(mode), zarr_format=3, - storage_options=cast("dict[str, Any]", storage_options), + storage_options=cast("dict[str, object]", storage_options), ) @@ -596,7 +624,7 @@ def open_zarr_group(path: str, mode: str = "r", **kwargs: Any) -> zarr.Group: storage_options = get_storage_options(path, **kwargs) return zarr.open_group( path, - mode=cast('Literal["r", "r+", "a", "w", "w-"]', mode), + mode=_zarr_mode(mode), zarr_format=3, - storage_options=cast("dict[str, Any] | None", storage_options), + storage_options=cast("dict[str, object] | None", storage_options), ) diff --git a/src/eopf_geozarr/conversion/geozarr.py b/src/eopf_geozarr/conversion/geozarr.py index f34a303e..8f9c68f4 100644 --- a/src/eopf_geozarr/conversion/geozarr.py +++ b/src/eopf_geozarr/conversion/geozarr.py @@ -24,6 +24,8 @@ import structlog import xarray as xr import zarr +import zarr.core.common +import zarr.core.group from pyproj import CRS from rasterio.warp import calculate_default_transform from zarr.codecs import BloscCodec @@ -49,6 +51,7 @@ from .sentinel1_reprojection import reproject_sentinel1_with_gcps if TYPE_CHECKING: + from zarr.core.common import JSON from zarr_cm import MultiscalesAttrs log = structlog.get_logger() @@ -364,14 +367,15 @@ def iterative_copy( # Write the dataset group_param = current_group_path.lstrip("/") if current_group_path else None - ds.to_zarr( # type: ignore[call-overload] # xarray stubs type storage_options as dict[str, str] + ds.to_zarr( output_path, group=group_param, mode="w", consolidated=False, zarr_format=3, encoding=encoding, - storage_options=storage_options, + # xarray stubs type storage_options as dict[str, str]; S3FsOptions is broader + storage_options=storage_options, # pyright: ignore[reportArgumentType] ) dt_result[relative_path] = xr.DataTree(ds) @@ -677,7 +681,7 @@ def _spatial_transform_for( # spatial, proj). proj is included only when a CRS is available. multiscales_data = cast( "MultiscalesAttrs", - MultiscaleMeta(layout=layout, resampling_method="average").model_dump(), + MultiscaleMeta(layout=tuple(layout), resampling_method="average").model_dump(), ) attrs_to_write = utils.build_convention_attrs( multiscales=multiscales_data, @@ -691,7 +695,7 @@ def _spatial_transform_for( group_path = fs_utils.normalize_path(f"{output_path}/{group_name.lstrip('/')}") zarr_group = fs_utils.open_zarr_group(group_path, mode="r+") - zarr_group.attrs.update(cast("dict[str, Any]", attrs_to_write)) + zarr_group.attrs.update(cast("dict[str, JSON]", attrs_to_write)) log.info("Added multiscales metadata to group %s", group_name) @@ -748,7 +752,7 @@ def _spatial_transform_for( overview_ds = sanitize_dataset_attributes(overview_ds) align_chunks_flag = not enable_sharding - overview_ds.to_zarr( # type: ignore[call-overload] # xarray stubs type storage_options as dict[str, str] + overview_ds.to_zarr( output_path, group=overview_group, mode="w", @@ -756,7 +760,8 @@ def _spatial_transform_for( zarr_format=3, encoding=encoding, align_chunks=align_chunks_flag, - storage_options=storage_options, + # xarray stubs type storage_options as dict[str, str]; S3FsOptions is broader + storage_options=storage_options, # pyright: ignore[reportArgumentType] ) overview_datasets[asset_name] = overview_ds @@ -1108,14 +1113,15 @@ def cleanup_prefix(prefix: str) -> None: # Sanitize NaN values in single variable dataset attributes single_var_ds = sanitize_dataset_attributes(single_var_ds) - single_var_ds.to_zarr( # type: ignore[call-overload] # xarray stubs type storage_options as dict[str, str] + single_var_ds.to_zarr( output_path, group=group_name, mode="a", consolidated=False, zarr_format=3, encoding=var_encoding, - storage_options=store_storage_options, + # xarray stubs type storage_options as dict[str, str]; S3FsOptions is broader + storage_options=store_storage_options, # pyright: ignore[reportArgumentType] ) log.info(" ✅ Successfully wrote", var=var) @@ -1396,9 +1402,10 @@ def _create_encoding( for i in range(len(current_chunks)) ) else: - chunking = ( - current_chunks[0][0] if len(current_chunks[0]) > 0 else ds[var].shape[0], - ) + chunks_list = list(current_chunks) + first_chunks = list(chunks_list[0]) + first_shape = list(ds[var].shape) + chunking = (first_chunks[0] if len(first_chunks) > 0 else first_shape[0],) else: data_shape = ds[var].shape if len(data_shape) >= 2: @@ -1406,7 +1413,7 @@ def _create_encoding( chunk_x = min(spatial_chunk, data_shape[-1]) chunking = (1, chunk_y, chunk_x) if len(data_shape) == 3 else (chunk_y, chunk_x) else: - chunking = (min(spatial_chunk, data_shape[-1]),) + chunking = (min(spatial_chunk, list(data_shape)[-1]),) var_encoding: XarrayEncodingJSON = { "compressors": [compressor], @@ -1482,7 +1489,7 @@ def _create_geozarr_encoding( ) else: # For 1D data, use the full dimension - shards = (data_shape[0],) + shards = (next(iter(data_shape)),) log.info( " 🔧 Sharding config", var=var, @@ -1699,5 +1706,14 @@ def _is_sentinel1(dt: xr.DataTree) -> bool: def get_zarr_group(data: xr.DataTree) -> zarr.Group: # `_close` is a bound method of the backend store on an opened DataTree; # `__self__` retrieves that store, which exposes `zarr_group`. These are - # xarray/zarr internals without public type information. - return cast("zarr.Group", data._close.__self__.zarr_group) # type: ignore[union-attr] + # xarray/zarr internals without public type information, so resolve them + # defensively and verify the result is actually a zarr.Group. + close = data._close + store = getattr(close, "__self__", None) + group = getattr(store, "zarr_group", None) + if not isinstance(group, zarr.Group): + raise TypeError( + "Could not resolve a zarr.Group from the DataTree backend " + f"(got {type(group).__name__}); the xarray/zarr internals may have changed." + ) + return group diff --git a/src/eopf_geozarr/conversion/sentinel1_reprojection.py b/src/eopf_geozarr/conversion/sentinel1_reprojection.py index 881706bc..e6c0f69b 100644 --- a/src/eopf_geozarr/conversion/sentinel1_reprojection.py +++ b/src/eopf_geozarr/conversion/sentinel1_reprojection.py @@ -9,6 +9,7 @@ import numpy as np import rasterio +import rasterio.control # Import submodule for GroundControlPoint attribute access import rioxarray # noqa: F401 # Import to enable .rio accessor import structlog import xarray as xr @@ -76,6 +77,9 @@ def reproject_sentinel1_with_gcps( gcps=gcps, ) + # calculate_default_transform sizes the grid, so width and height are populated + assert width is not None + assert height is not None log.info("Calculated target dimensions", width=width, height=height) log.info("Transform", transform=str(transform)) diff --git a/src/eopf_geozarr/conversion/utils.py b/src/eopf_geozarr/conversion/utils.py index 65e6a709..ff5e8d2c 100644 --- a/src/eopf_geozarr/conversion/utils.py +++ b/src/eopf_geozarr/conversion/utils.py @@ -59,14 +59,17 @@ def build_convention_attrs( representation; otherwise only the other conventions are emitted (a proj convention with no CRS field is invalid). """ - conventions: dict[zarr_cm.ConventionName, dict[str, Any]] = {} + conventions: dict[zarr_cm.ConventionName, MultiscalesAttrs | SpatialAttrs | GeoProjAttrs] = {} if multiscales is not None: - conventions["multiscales"] = dict(multiscales) - conventions["spatial"] = dict(spatial) + conventions["multiscales"] = multiscales + conventions["spatial"] = spatial proj = proj_attrs_for_crs(crs) if proj: - conventions["geo-proj"] = dict(proj) - return cast("MultiConventionAttrs", zarr_cm.create_many(conventions)) + conventions["geo-proj"] = proj + # create_many validates each convention and emits its CMO. It returns a + # generic JSON dict; narrow to the combined convention TypedDict. + result = zarr_cm.create_many(conventions) + return cast("MultiConventionAttrs", result) # Sentinel: distinguish "no explicit fill_value" from a legitimate `None`. diff --git a/src/eopf_geozarr/data_api/geozarr/common.py b/src/eopf_geozarr/data_api/geozarr/common.py index 8236611e..cbbbd90c 100644 --- a/src/eopf_geozarr/data_api/geozarr/common.py +++ b/src/eopf_geozarr/data_api/geozarr/common.py @@ -80,7 +80,7 @@ class BaseDataArrayAttrs(BaseModel, extra="allow"): ---------- """ - grid_mapping: str | MISSING = MISSING # type: ignore[valid-type] + grid_mapping: str | MISSING = MISSING class GridMappingAttrs(BaseModel, extra="allow"): diff --git a/src/eopf_geozarr/data_api/geozarr/multiscales/geozarr.py b/src/eopf_geozarr/data_api/geozarr/multiscales/geozarr.py index 5cbd6772..e507ae3e 100644 --- a/src/eopf_geozarr/data_api/geozarr/multiscales/geozarr.py +++ b/src/eopf_geozarr/data_api/geozarr/multiscales/geozarr.py @@ -5,6 +5,9 @@ from pydantic import BaseModel, model_validator from pydantic.experimental.missing_sentinel import MISSING from typing_extensions import TypedDict + +# Runtime import (not TYPE_CHECKING): pydantic resolves this annotation when +# building MultiscaleGroupAttrs, so the name must exist at runtime. from zarr_cm import ConventionMetadataObject # noqa: TC002 from . import tms, zcm @@ -16,17 +19,17 @@ class MultiscaleMeta(BaseModel): or ZCM multiscale metadata """ - layout: tuple[zcm.ScaleLevel, ...] | MISSING = MISSING # type: ignore[valid-type] - resampling_method: str | MISSING = MISSING # type: ignore[valid-type] - tile_matrix_set: tms.TileMatrixSet | MISSING = MISSING # type: ignore[valid-type] - tile_matrix_limits: dict[str, tms.TileMatrixLimit] | MISSING = MISSING # type: ignore[valid-type] + layout: tuple[zcm.ScaleLevel, ...] | MISSING = MISSING + resampling_method: str | MISSING = MISSING + tile_matrix_set: tms.TileMatrixSet | MISSING = MISSING + tile_matrix_limits: dict[str, tms.TileMatrixLimit] | MISSING = MISSING @model_validator(mode="after") def valid_zcm(self) -> Self: """ Ensure that the ZCM metadata, if present, is valid """ - if self.layout is not MISSING: # type: ignore[comparison-overlap] + if self.layout is not MISSING: zcm.Multiscales(**self.model_dump()) return self @@ -36,7 +39,7 @@ def valid_tms(self) -> Self: """ Ensure that the TMS metadata, if present, is valid """ - if self.tile_matrix_set is not MISSING: # type: ignore[comparison-overlap] + if self.tile_matrix_set is not MISSING: tms.Multiscales(**self.model_dump()) return self @@ -55,7 +58,7 @@ class MultiscaleGroupAttrs(BaseModel): multiscales: MultiscaleAttrs """ - zarr_conventions: tuple[ConventionMetadataObject, ...] | MISSING = MISSING # type: ignore[valid-type] + zarr_conventions: tuple[ConventionMetadataObject, ...] | MISSING = MISSING multiscales: MultiscaleMeta _zcm_multiscales: zcm.Multiscales | None = None @@ -67,23 +70,21 @@ def valid_zcm_and_tms(self) -> Self: Ensure that the ZCM metadata, if present, is valid, and that TMS metadata, if present, is valid, and that at least one of the two is present. """ - if self.zarr_conventions is not MISSING: # type: ignore[comparison-overlap] + if self.zarr_conventions is not MISSING: self._zcm_multiscales = zcm.Multiscales( layout=self.multiscales.layout, resampling_method=self.multiscales.resampling_method, ) - if self.multiscales.tile_matrix_limits is not MISSING: # type: ignore[comparison-overlap] + if self.multiscales.tile_matrix_limits is not MISSING: self._tms_multiscales = tms.Multiscales( tile_matrix_limits=self.multiscales.tile_matrix_limits, - resampling_method=self.multiscales.resampling_method, # type: ignore[arg-type] + # ``resampling_method`` is typed ``str | MISSING`` here but tms.Multiscales + # constrains it to the ``ResamplingMethod`` literal; pydantic validates the + # value at runtime. + resampling_method=self.multiscales.resampling_method, # pyright: ignore[reportArgumentType] tile_matrix_set=self.multiscales.tile_matrix_set, ) - # mypy treats the ``is not MISSING`` guards above as always-true (see the - # ``comparison-overlap`` ignores), so it wrongly concludes both attributes are always set - # and that this branch's right operand is unreachable. The runtime check is required. - if ( - self._tms_multiscales is None and self._zcm_multiscales is None # type: ignore[unreachable] - ): + if self._tms_multiscales is None and self._zcm_multiscales is None: raise ValueError("Either ZCM multiscales or TMS multiscales must be present") return self diff --git a/src/eopf_geozarr/data_api/geozarr/multiscales/zcm.py b/src/eopf_geozarr/data_api/geozarr/multiscales/zcm.py index 650ea07c..1c8f07cd 100644 --- a/src/eopf_geozarr/data_api/geozarr/multiscales/zcm.py +++ b/src/eopf_geozarr/data_api/geozarr/multiscales/zcm.py @@ -9,8 +9,12 @@ CONVENTION_ID = multiscales_cm.UUID CONVENTION_SCHEMA_URL = multiscales_cm.SCHEMA_URL CONVENTION_SPEC_URL = multiscales_cm.SPEC_URL -CONVENTION_NAME = multiscales_cm.CMO["name"] -CONVENTION_DESCRIPTION = multiscales_cm.CMO["description"] +_CONVENTION_NAME = multiscales_cm.CMO.get("name") +assert _CONVENTION_NAME is not None +CONVENTION_NAME = _CONVENTION_NAME +_CONVENTION_DESCRIPTION = multiscales_cm.CMO.get("description") +assert _CONVENTION_DESCRIPTION is not None +CONVENTION_DESCRIPTION = _CONVENTION_DESCRIPTION # Re-export zarr-cm TypedDicts TransformJSON = multiscales_cm.Transform @@ -26,22 +30,22 @@ class ZarrConventionAttrs(BaseModel): class Transform(BaseModel): - scale: tuple[float, ...] | MISSING = MISSING # type: ignore[valid-type] - translation: tuple[float, ...] | MISSING = MISSING # type: ignore[valid-type] + scale: tuple[float, ...] | MISSING = MISSING + translation: tuple[float, ...] | MISSING = MISSING class ScaleLevel(BaseModel): asset: str - derived_from: str | MISSING = MISSING # type: ignore[valid-type] - transform: Transform | MISSING = MISSING # type: ignore[valid-type] - resampling_method: str | MISSING = MISSING # type: ignore[valid-type] + derived_from: str | MISSING = MISSING + transform: Transform | MISSING = MISSING + resampling_method: str | MISSING = MISSING model_config = {"extra": "allow"} class Multiscales(BaseModel): layout: tuple[ScaleLevel, ...] - resampling_method: str | MISSING = MISSING # type: ignore[valid-type] + resampling_method: str | MISSING = MISSING model_config = {"extra": "allow"} @@ -59,8 +63,9 @@ def ensure_multiscales_convention( Iterate over the elements of zarr_conventions and check that at least one of them is multiscales """ - expected_uuid = multiscales_cm.CMO["uuid"] - if not any(c["uuid"] == expected_uuid for c in value): + expected_uuid = multiscales_cm.CMO.get("uuid") + assert expected_uuid is not None + if not any(c.get("uuid") == expected_uuid for c in value): raise ValueError( f"Multiscales convention (uuid={expected_uuid}) not found in zarr_conventions" ) diff --git a/src/eopf_geozarr/data_api/geozarr/store.py b/src/eopf_geozarr/data_api/geozarr/store.py index 61a6a443..0835ed25 100644 --- a/src/eopf_geozarr/data_api/geozarr/store.py +++ b/src/eopf_geozarr/data_api/geozarr/store.py @@ -91,13 +91,19 @@ class GeoZarrScaleLevel(ScaleLevel): class GeoZarrMultiscaleMeta(MultiscaleMeta): """Multiscale metadata where every layout entry is a `GeoZarrScaleLevel`.""" - layout: tuple[GeoZarrScaleLevel, ...] + # Intentionally tightens the base ``layout`` field: ``GeoZarrScaleLevel`` is a + # subclass of ``ScaleLevel`` and the optional ``MISSING`` default is dropped to make + # the field mandatory in this store-level profile. pyright flags the narrowed, + # now-required override on a mutable (invariant) field. + layout: tuple[GeoZarrScaleLevel, ...] # pyright: ignore[reportGeneralTypeIssues, reportIncompatibleVariableOverride] class GeoZarrMultiscaleGroupAttrs(MultiscaleGroupAttrs): """Multiscale group attributes with a mandatory `spatial:bbox`.""" - multiscales: GeoZarrMultiscaleMeta + # Intentionally tightens the base ``multiscales`` field to the ``GeoZarrMultiscaleMeta`` + # subclass; pyright flags the narrowed override on a mutable (invariant) field. + multiscales: GeoZarrMultiscaleMeta # pyright: ignore[reportIncompatibleVariableOverride] spatial_bbox: list[float] = Field(alias="spatial:bbox", min_length=4, max_length=4) model_config = ConfigDict( diff --git a/src/eopf_geozarr/data_api/geozarr/v3.py b/src/eopf_geozarr/data_api/geozarr/v3.py index f9e134bd..ee2a487c 100644 --- a/src/eopf_geozarr/data_api/geozarr/v3.py +++ b/src/eopf_geozarr/data_api/geozarr/v3.py @@ -31,8 +31,9 @@ class DataArray(ArraySpec[BaseDataArrayAttrs]): https://github.com/zarr-developers/geozarr-spec/blob/main/geozarr-spec.md#geozarr-dataarray """ - # The dimension names must be a tuple of strings - dimension_names: tuple[str, ...] + # GeoZarr requires dimension names, so tighten the parent's optional + # `tuple[str | None, ...] | None` field to a required tuple of strings. + dimension_names: tuple[str, ...] # pyright: ignore[reportGeneralTypeIssues, reportIncompatibleVariableOverride] @property def array_dimensions(self) -> tuple[str, ...]: diff --git a/src/eopf_geozarr/data_api/s1.py b/src/eopf_geozarr/data_api/s1.py index d8bb91f1..00f0bed5 100644 --- a/src/eopf_geozarr/data_api/s1.py +++ b/src/eopf_geozarr/data_api/s1.py @@ -47,7 +47,7 @@ class Sentinel1DataArray(ArraySpec[Sentinel1DataArrayAttrs]): # Conditions groups -class Sentinel1AntennaPatternMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1AntennaPatternMembers(TypedDict, closed=True, total=False): """Members for antenna_pattern group. All fields are optional to support different product variants. @@ -64,53 +64,75 @@ class Sentinel1AntennaPatternMembers(TypedDict, closed=True, total=False): # ty terrain_height: ArraySpec[Any] -class Sentinel1AntennaPatternGroup( - GroupSpec[DatasetAttrs, Sentinel1AntennaPatternMembers] # type: ignore[type-var] -): +class Sentinel1AntennaPatternGroup(GroupSpec[DatasetAttrs, Sentinel1AntennaPatternMembers]): """Antenna pattern group containing antenna characteristics.""" @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def count(self) -> ArraySpec[Any]: """Get count array.""" - return self.members["count"] + value = self.members.get("count") + if value is None: + raise KeyError("count") + return value @property def elevation_angle(self) -> ArraySpec[Any]: """Get elevation_angle array.""" - return self.members["elevation_angle"] + value = self.members.get("elevation_angle") + if value is None: + raise KeyError("elevation_angle") + return value @property def incidence_angle(self) -> ArraySpec[Any]: """Get incidence_angle array.""" - return self.members["incidence_angle"] + value = self.members.get("incidence_angle") + if value is None: + raise KeyError("incidence_angle") + return value @property def roll(self) -> ArraySpec[Any]: """Get roll array.""" - return self.members["roll"] + value = self.members.get("roll") + if value is None: + raise KeyError("roll") + return value @property def slant_range_time_ap(self) -> ArraySpec[Any]: """Get slant_range_time_ap array.""" - return self.members["slant_range_time_ap"] + value = self.members.get("slant_range_time_ap") + if value is None: + raise KeyError("slant_range_time_ap") + return value @property def swath(self) -> ArraySpec[Any]: """Get swath array.""" - return self.members["swath"] + value = self.members.get("swath") + if value is None: + raise KeyError("swath") + return value @property def terrain_height(self) -> ArraySpec[Any]: """Get terrain_height array.""" - return self.members["terrain_height"] + value = self.members.get("terrain_height") + if value is None: + raise KeyError("terrain_height") + return value -class Sentinel1AttitudeMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1AttitudeMembers(TypedDict, closed=True, total=False): """Members for attitude group.""" azimuth_time: ArraySpec[Any] @@ -126,66 +148,99 @@ class Sentinel1AttitudeMembers(TypedDict, closed=True, total=False): # type: ig yaw: ArraySpec[Any] -class Sentinel1AttitudeGroup(GroupSpec[DatasetAttrs, Sentinel1AttitudeMembers]): # type: ignore[type-var] +class Sentinel1AttitudeGroup(GroupSpec[DatasetAttrs, Sentinel1AttitudeMembers]): """Attitude group containing spacecraft attitude data.""" @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def pitch(self) -> ArraySpec[Any]: """Get pitch array.""" - return self.members["pitch"] + value = self.members.get("pitch") + if value is None: + raise KeyError("pitch") + return value @property def q0(self) -> ArraySpec[Any]: """Get q0 array.""" - return self.members["q0"] + value = self.members.get("q0") + if value is None: + raise KeyError("q0") + return value @property def q1(self) -> ArraySpec[Any]: """Get q1 array.""" - return self.members["q1"] + value = self.members.get("q1") + if value is None: + raise KeyError("q1") + return value @property def q2(self) -> ArraySpec[Any]: """Get q2 array.""" - return self.members["q2"] + value = self.members.get("q2") + if value is None: + raise KeyError("q2") + return value @property def q3(self) -> ArraySpec[Any]: """Get q3 array.""" - return self.members["q3"] + value = self.members.get("q3") + if value is None: + raise KeyError("q3") + return value @property def roll(self) -> ArraySpec[Any]: """Get roll array.""" - return self.members["roll"] + value = self.members.get("roll") + if value is None: + raise KeyError("roll") + return value @property def wx(self) -> ArraySpec[Any]: """Get wx array.""" - return self.members["wx"] + value = self.members.get("wx") + if value is None: + raise KeyError("wx") + return value @property def wy(self) -> ArraySpec[Any]: """Get wy array.""" - return self.members["wy"] + value = self.members.get("wy") + if value is None: + raise KeyError("wy") + return value @property def wz(self) -> ArraySpec[Any]: """Get wz array.""" - return self.members["wz"] + value = self.members.get("wz") + if value is None: + raise KeyError("wz") + return value @property def yaw(self) -> ArraySpec[Any]: """Get yaw array.""" - return self.members["yaw"] + value = self.members.get("yaw") + if value is None: + raise KeyError("yaw") + return value -class Sentinel1AzimuthFmRateMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1AzimuthFmRateMembers(TypedDict, closed=True, total=False): """Members for azimuth_fm_rate group.""" azimuth_fm_rate_polynomial: ArraySpec[Any] @@ -193,28 +248,35 @@ class Sentinel1AzimuthFmRateMembers(TypedDict, closed=True, total=False): # typ t0: ArraySpec[Any] -class Sentinel1AzimuthFmRateGroup( - GroupSpec[DatasetAttrs, Sentinel1AzimuthFmRateMembers] # type: ignore[type-var] -): +class Sentinel1AzimuthFmRateGroup(GroupSpec[DatasetAttrs, Sentinel1AzimuthFmRateMembers]): """Azimuth FM rate group.""" @property def azimuth_fm_rate_polynomial(self) -> ArraySpec[Any]: """Get azimuth_fm_rate_polynomial array.""" - return self.members["azimuth_fm_rate_polynomial"] + value = self.members.get("azimuth_fm_rate_polynomial") + if value is None: + raise KeyError("azimuth_fm_rate_polynomial") + return value @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def t0(self) -> ArraySpec[Any]: """Get t0 array.""" - return self.members["t0"] + value = self.members.get("t0") + if value is None: + raise KeyError("t0") + return value -class Sentinel1CoordinateConversionMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1CoordinateConversionMembers(TypedDict, closed=True, total=False): """Members for coordinate_conversion group.""" azimuth_time: ArraySpec[Any] @@ -226,42 +288,60 @@ class Sentinel1CoordinateConversionMembers(TypedDict, closed=True, total=False): class Sentinel1CoordinateConversionGroup( - GroupSpec[DatasetAttrs, Sentinel1CoordinateConversionMembers] # type: ignore[type-var] + GroupSpec[DatasetAttrs, Sentinel1CoordinateConversionMembers] ): """Coordinate conversion group.""" @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def gr0(self) -> ArraySpec[Any]: """Get gr0 array.""" - return self.members["gr0"] + value = self.members.get("gr0") + if value is None: + raise KeyError("gr0") + return value @property def grsr_coefficients(self) -> ArraySpec[Any]: """Get grsr_coefficients array.""" - return self.members["grsr_coefficients"] + value = self.members.get("grsr_coefficients") + if value is None: + raise KeyError("grsr_coefficients") + return value @property def slant_range_time(self) -> ArraySpec[Any]: """Get slant_range_time array.""" - return self.members["slant_range_time"] + value = self.members.get("slant_range_time") + if value is None: + raise KeyError("slant_range_time") + return value @property def sr0(self) -> ArraySpec[Any]: """Get sr0 array.""" - return self.members["sr0"] + value = self.members.get("sr0") + if value is None: + raise KeyError("sr0") + return value @property def srgr_coefficients(self) -> ArraySpec[Any]: """Get srgr_coefficients array.""" - return self.members["srgr_coefficients"] + value = self.members.get("srgr_coefficients") + if value is None: + raise KeyError("srgr_coefficients") + return value -class Sentinel1DopplerCentroidMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1DopplerCentroidMembers(TypedDict, closed=True, total=False): """Members for doppler_centroid group.""" azimuth_time: ArraySpec[Any] @@ -275,58 +355,83 @@ class Sentinel1DopplerCentroidMembers(TypedDict, closed=True, total=False): # t t0: ArraySpec[Any] -class Sentinel1DopplerCentroidGroup( - GroupSpec[DatasetAttrs, Sentinel1DopplerCentroidMembers] # type: ignore[type-var] -): +class Sentinel1DopplerCentroidGroup(GroupSpec[DatasetAttrs, Sentinel1DopplerCentroidMembers]): """Doppler centroid group.""" @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def data_dc_polynomial(self) -> ArraySpec[Any]: """Get data_dc_polynomial array.""" - return self.members["data_dc_polynomial"] + value = self.members.get("data_dc_polynomial") + if value is None: + raise KeyError("data_dc_polynomial") + return value @property def data_dc_rms_error(self) -> ArraySpec[Any]: """Get data_dc_rms_error array.""" - return self.members["data_dc_rms_error"] + value = self.members.get("data_dc_rms_error") + if value is None: + raise KeyError("data_dc_rms_error") + return value @property def data_dc_rms_error_above_threshold(self) -> ArraySpec[Any]: """Get data_dc_rms_error_above_threshold array.""" - return self.members["data_dc_rms_error_above_threshold"] + value = self.members.get("data_dc_rms_error_above_threshold") + if value is None: + raise KeyError("data_dc_rms_error_above_threshold") + return value @property def degree(self) -> ArraySpec[Any]: """Get degree array.""" - return self.members["degree"] + value = self.members.get("degree") + if value is None: + raise KeyError("degree") + return value @property def fine_dce_azimuth_start_time(self) -> ArraySpec[Any]: """Get fine_dce_azimuth_start_time array.""" - return self.members["fine_dce_azimuth_start_time"] + value = self.members.get("fine_dce_azimuth_start_time") + if value is None: + raise KeyError("fine_dce_azimuth_start_time") + return value @property def fine_dce_azimuth_stop_time(self) -> ArraySpec[Any]: """Get fine_dce_azimuth_stop_time array.""" - return self.members["fine_dce_azimuth_stop_time"] + value = self.members.get("fine_dce_azimuth_stop_time") + if value is None: + raise KeyError("fine_dce_azimuth_stop_time") + return value @property def geometry_dc_polynomial(self) -> ArraySpec[Any]: """Get geometry_dc_polynomial array.""" - return self.members["geometry_dc_polynomial"] + value = self.members.get("geometry_dc_polynomial") + if value is None: + raise KeyError("geometry_dc_polynomial") + return value @property def t0(self) -> ArraySpec[Any]: """Get t0 array.""" - return self.members["t0"] + value = self.members.get("t0") + if value is None: + raise KeyError("t0") + return value -class Sentinel1GcpMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1GcpMembers(TypedDict, closed=True, total=False): """Members for GCP (Ground Control Points) group. All fields are optional to support different product variants (S1A, S1C). @@ -346,66 +451,99 @@ class Sentinel1GcpMembers(TypedDict, closed=True, total=False): # type: ignore[ slant_range_time_gcp: ArraySpec[Any] -class Sentinel1GcpGroup(GroupSpec[DatasetAttrs, Sentinel1GcpMembers]): # type: ignore[type-var] +class Sentinel1GcpGroup(GroupSpec[DatasetAttrs, Sentinel1GcpMembers]): """Ground Control Points (GCP) group.""" @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def azimuth_time_gcp(self) -> ArraySpec[Any]: """Get azimuth_time_gcp array.""" - return self.members["azimuth_time_gcp"] + value = self.members.get("azimuth_time_gcp") + if value is None: + raise KeyError("azimuth_time_gcp") + return value @property def elevation_angle(self) -> ArraySpec[Any]: """Get elevation_angle array.""" - return self.members["elevation_angle"] + value = self.members.get("elevation_angle") + if value is None: + raise KeyError("elevation_angle") + return value @property def ground_range(self) -> ArraySpec[Any]: """Get ground_range array.""" - return self.members["ground_range"] + value = self.members.get("ground_range") + if value is None: + raise KeyError("ground_range") + return value @property def height(self) -> ArraySpec[Any]: """Get height array.""" - return self.members["height"] + value = self.members.get("height") + if value is None: + raise KeyError("height") + return value @property def incidence_angle(self) -> ArraySpec[Any]: """Get incidence_angle array.""" - return self.members["incidence_angle"] + value = self.members.get("incidence_angle") + if value is None: + raise KeyError("incidence_angle") + return value @property def latitude(self) -> ArraySpec[Any]: """Get latitude array.""" - return self.members["latitude"] + value = self.members.get("latitude") + if value is None: + raise KeyError("latitude") + return value @property def line(self) -> ArraySpec[Any]: """Get line array.""" - return self.members["line"] + value = self.members.get("line") + if value is None: + raise KeyError("line") + return value @property def longitude(self) -> ArraySpec[Any]: """Get longitude array.""" - return self.members["longitude"] + value = self.members.get("longitude") + if value is None: + raise KeyError("longitude") + return value @property def pixel(self) -> ArraySpec[Any]: """Get pixel array.""" - return self.members["pixel"] + value = self.members.get("pixel") + if value is None: + raise KeyError("pixel") + return value @property def slant_range_time_gcp(self) -> ArraySpec[Any]: """Get slant_range_time_gcp array.""" - return self.members["slant_range_time_gcp"] + value = self.members.get("slant_range_time_gcp") + if value is None: + raise KeyError("slant_range_time_gcp") + return value -class Sentinel1OrbitMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1OrbitMembers(TypedDict, closed=True, total=False): """Members for orbit group.""" axis: ArraySpec[Any] @@ -414,31 +552,43 @@ class Sentinel1OrbitMembers(TypedDict, closed=True, total=False): # type: ignor velocity: ArraySpec[Any] -class Sentinel1OrbitGroup(GroupSpec[DatasetAttrs, Sentinel1OrbitMembers]): # type: ignore[type-var] +class Sentinel1OrbitGroup(GroupSpec[DatasetAttrs, Sentinel1OrbitMembers]): """Orbit group containing spacecraft position and velocity.""" @property def axis(self) -> ArraySpec[Any]: """Get axis array.""" - return self.members["axis"] + value = self.members.get("axis") + if value is None: + raise KeyError("axis") + return value @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def position(self) -> ArraySpec[Any]: """Get position array.""" - return self.members["position"] + value = self.members.get("position") + if value is None: + raise KeyError("position") + return value @property def velocity(self) -> ArraySpec[Any]: """Get velocity array.""" - return self.members["velocity"] + value = self.members.get("velocity") + if value is None: + raise KeyError("velocity") + return value -class Sentinel1ReferenceReplicaMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1ReferenceReplicaMembers(TypedDict, closed=True, total=False): """Members for reference_replica group. Closed TypedDict - only reference replica coefficient array keys are allowed. @@ -450,28 +600,35 @@ class Sentinel1ReferenceReplicaMembers(TypedDict, closed=True, total=False): # reference_replica_phase_coefficients: ArraySpec[Any] -class Sentinel1ReferenceReplicaGroup( - GroupSpec[DatasetAttrs, Sentinel1ReferenceReplicaMembers] # type: ignore[type-var] -): +class Sentinel1ReferenceReplicaGroup(GroupSpec[DatasetAttrs, Sentinel1ReferenceReplicaMembers]): """Reference replica group.""" @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def reference_replica_amplitude_coefficients(self) -> ArraySpec[Any]: """Get reference_replica_amplitude_coefficients array.""" - return self.members["reference_replica_amplitude_coefficients"] + value = self.members.get("reference_replica_amplitude_coefficients") + if value is None: + raise KeyError("reference_replica_amplitude_coefficients") + return value @property def reference_replica_phase_coefficients(self) -> ArraySpec[Any]: """Get reference_replica_phase_coefficients array.""" - return self.members["reference_replica_phase_coefficients"] + value = self.members.get("reference_replica_phase_coefficients") + if value is None: + raise KeyError("reference_replica_phase_coefficients") + return value -class Sentinel1ReplicaMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1ReplicaMembers(TypedDict, closed=True, total=False): """Members for replica group. Closed TypedDict - only pulse replica data array keys are allowed. @@ -491,89 +648,126 @@ class Sentinel1ReplicaMembers(TypedDict, closed=True, total=False): # type: ign relative_pg_product_valid_flag: ArraySpec[Any] -class Sentinel1ReplicaGroup(GroupSpec[DatasetAttrs, Sentinel1ReplicaMembers]): # type: ignore[type-var] +class Sentinel1ReplicaGroup(GroupSpec[DatasetAttrs, Sentinel1ReplicaMembers]): """Replica group containing pulse replica data.""" @property def absolute_pg_product_valid_flag(self) -> ArraySpec[Any]: """Get absolute_pg_product_valid_flag array.""" - return self.members["absolute_pg_product_valid_flag"] + value = self.members.get("absolute_pg_product_valid_flag") + if value is None: + raise KeyError("absolute_pg_product_valid_flag") + return value @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def cross_correlation_peak_location(self) -> ArraySpec[Any]: """Get cross_correlation_peak_location array.""" - return self.members["cross_correlation_peak_location"] + value = self.members.get("cross_correlation_peak_location") + if value is None: + raise KeyError("cross_correlation_peak_location") + return value @property def cross_correlation_pslr(self) -> ArraySpec[Any]: """Get cross_correlation_pslr array.""" - return self.members["cross_correlation_pslr"] + value = self.members.get("cross_correlation_pslr") + if value is None: + raise KeyError("cross_correlation_pslr") + return value @property def internal_time_delay(self) -> ArraySpec[Any]: """Get internal_time_delay array.""" - return self.members["internal_time_delay"] + value = self.members.get("internal_time_delay") + if value is None: + raise KeyError("internal_time_delay") + return value @property def model_pg_product_amplitude(self) -> ArraySpec[Any]: """Get model_pg_product_amplitude array.""" - return self.members["model_pg_product_amplitude"] + value = self.members.get("model_pg_product_amplitude") + if value is None: + raise KeyError("model_pg_product_amplitude") + return value @property def model_pg_product_phase(self) -> ArraySpec[Any]: """Get model_pg_product_phase array.""" - return self.members["model_pg_product_phase"] + value = self.members.get("model_pg_product_phase") + if value is None: + raise KeyError("model_pg_product_phase") + return value @property def pg_product_amplitude(self) -> ArraySpec[Any]: """Get pg_product_amplitude array.""" - return self.members["pg_product_amplitude"] + value = self.members.get("pg_product_amplitude") + if value is None: + raise KeyError("pg_product_amplitude") + return value @property def pg_product_phase(self) -> ArraySpec[Any]: """Get pg_product_phase array.""" - return self.members["pg_product_phase"] + value = self.members.get("pg_product_phase") + if value is None: + raise KeyError("pg_product_phase") + return value @property def reconstructed_replica_valid_flag(self) -> ArraySpec[Any]: """Get reconstructed_replica_valid_flag array.""" - return self.members["reconstructed_replica_valid_flag"] + value = self.members.get("reconstructed_replica_valid_flag") + if value is None: + raise KeyError("reconstructed_replica_valid_flag") + return value @property def relative_pg_product_valid_flag(self) -> ArraySpec[Any]: """Get relative_pg_product_valid_flag array.""" - return self.members["relative_pg_product_valid_flag"] + value = self.members.get("relative_pg_product_valid_flag") + if value is None: + raise KeyError("relative_pg_product_valid_flag") + return value -class Sentinel1TerrainHeightMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1TerrainHeightMembers(TypedDict, closed=True, total=False): """Members for terrain_height group.""" azimuth_time: ArraySpec[Any] terrain_height: ArraySpec[Any] -class Sentinel1TerrainHeightGroup( - GroupSpec[DatasetAttrs, Sentinel1TerrainHeightMembers] # type: ignore[type-var] -): +class Sentinel1TerrainHeightGroup(GroupSpec[DatasetAttrs, Sentinel1TerrainHeightMembers]): """Terrain height group.""" @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def terrain_height(self) -> ArraySpec[Any]: """Get terrain_height array.""" - return self.members["terrain_height"] + value = self.members.get("terrain_height") + if value is None: + raise KeyError("terrain_height") + return value -class Sentinel1ConditionsMembers(TypedDict, closed=True): # type: ignore[call-arg] +class Sentinel1ConditionsMembers(TypedDict, closed=True): """Members for conditions group. Closed TypedDict - only antenna_pattern, attitude, azimuth_fm_rate, etc. keys are allowed. @@ -591,7 +785,7 @@ class Sentinel1ConditionsMembers(TypedDict, closed=True): # type: ignore[call-a terrain_height: Sentinel1TerrainHeightGroup -class Sentinel1ConditionsGroup(GroupSpec[DatasetAttrs, Sentinel1ConditionsMembers]): # type: ignore[type-var] +class Sentinel1ConditionsGroup(GroupSpec[DatasetAttrs, Sentinel1ConditionsMembers]): """Conditions group containing acquisition and processing metadata.""" def get_antenna_pattern(self) -> Sentinel1AntennaPatternGroup | None: @@ -636,7 +830,7 @@ def get_terrain_height(self) -> Sentinel1TerrainHeightGroup | None: # Quality groups -class Sentinel1CalibrationMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1CalibrationMembers(TypedDict, closed=True, total=False): """Members for calibration group.""" azimuth_time: ArraySpec[Any] @@ -649,51 +843,75 @@ class Sentinel1CalibrationMembers(TypedDict, closed=True, total=False): # type: sigma_nought: ArraySpec[Any] -class Sentinel1CalibrationGroup(GroupSpec[DatasetAttrs, Sentinel1CalibrationMembers]): # type: ignore[type-var] +class Sentinel1CalibrationGroup(GroupSpec[DatasetAttrs, Sentinel1CalibrationMembers]): """Calibration group containing radiometric calibration data.""" @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def beta_nought(self) -> ArraySpec[Any]: """Get beta_nought array.""" - return self.members["beta_nought"] + value = self.members.get("beta_nought") + if value is None: + raise KeyError("beta_nought") + return value @property def dn(self) -> ArraySpec[Any]: """Get dn array.""" - return self.members["dn"] + value = self.members.get("dn") + if value is None: + raise KeyError("dn") + return value @property def gamma(self) -> ArraySpec[Any]: """Get gamma array.""" - return self.members["gamma"] + value = self.members.get("gamma") + if value is None: + raise KeyError("gamma") + return value @property def ground_range(self) -> ArraySpec[Any]: """Get ground_range array.""" - return self.members["ground_range"] + value = self.members.get("ground_range") + if value is None: + raise KeyError("ground_range") + return value @property def line(self) -> ArraySpec[Any]: """Get line array.""" - return self.members["line"] + value = self.members.get("line") + if value is None: + raise KeyError("line") + return value @property def pixel(self) -> ArraySpec[Any]: """Get pixel array.""" - return self.members["pixel"] + value = self.members.get("pixel") + if value is None: + raise KeyError("pixel") + return value @property def sigma_nought(self) -> ArraySpec[Any]: """Get sigma_nought array.""" - return self.members["sigma_nought"] + value = self.members.get("sigma_nought") + if value is None: + raise KeyError("sigma_nought") + return value -class Sentinel1NoiseMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1NoiseMembers(TypedDict, closed=True, total=False): """Members for noise group.""" azimuth_time: ArraySpec[Any] @@ -701,26 +919,35 @@ class Sentinel1NoiseMembers(TypedDict, closed=True, total=False): # type: ignor number_of_noise_lines: ArraySpec[Any] -class Sentinel1NoiseGroup(GroupSpec[DatasetAttrs, Sentinel1NoiseMembers]): # type: ignore[type-var] +class Sentinel1NoiseGroup(GroupSpec[DatasetAttrs, Sentinel1NoiseMembers]): """Noise group containing noise estimation data.""" @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def noise_power_correction_factor(self) -> ArraySpec[Any]: """Get noise_power_correction_factor array.""" - return self.members["noise_power_correction_factor"] + value = self.members.get("noise_power_correction_factor") + if value is None: + raise KeyError("noise_power_correction_factor") + return value @property def number_of_noise_lines(self) -> ArraySpec[Any]: """Get number_of_noise_lines array.""" - return self.members["number_of_noise_lines"] + value = self.members.get("number_of_noise_lines") + if value is None: + raise KeyError("number_of_noise_lines") + return value -class Sentinel1NoiseAzimuthMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1NoiseAzimuthMembers(TypedDict, closed=True, total=False): """Members for noise_azimuth group.""" first_azimuth_time: ArraySpec[Any] @@ -732,46 +959,67 @@ class Sentinel1NoiseAzimuthMembers(TypedDict, closed=True, total=False): # type swath: ArraySpec[Any] -class Sentinel1NoiseAzimuthGroup(GroupSpec[DatasetAttrs, Sentinel1NoiseAzimuthMembers]): # type: ignore[type-var] +class Sentinel1NoiseAzimuthGroup(GroupSpec[DatasetAttrs, Sentinel1NoiseAzimuthMembers]): """Noise azimuth group containing azimuth noise vectors.""" @property def first_azimuth_time(self) -> ArraySpec[Any]: """Get first_azimuth_time array.""" - return self.members["first_azimuth_time"] + value = self.members.get("first_azimuth_time") + if value is None: + raise KeyError("first_azimuth_time") + return value @property def first_range_sample(self) -> ArraySpec[Any]: """Get first_range_sample array.""" - return self.members["first_range_sample"] + value = self.members.get("first_range_sample") + if value is None: + raise KeyError("first_range_sample") + return value @property def last_azimuth_time(self) -> ArraySpec[Any]: """Get last_azimuth_time array.""" - return self.members["last_azimuth_time"] + value = self.members.get("last_azimuth_time") + if value is None: + raise KeyError("last_azimuth_time") + return value @property def last_range_sample(self) -> ArraySpec[Any]: """Get last_range_sample array.""" - return self.members["last_range_sample"] + value = self.members.get("last_range_sample") + if value is None: + raise KeyError("last_range_sample") + return value @property def line(self) -> ArraySpec[Any]: """Get line array.""" - return self.members["line"] + value = self.members.get("line") + if value is None: + raise KeyError("line") + return value @property def noise_azimuth_lut(self) -> ArraySpec[Any]: """Get noise_azimuth_lut array.""" - return self.members["noise_azimuth_lut"] + value = self.members.get("noise_azimuth_lut") + if value is None: + raise KeyError("noise_azimuth_lut") + return value @property def swath(self) -> ArraySpec[Any]: """Get swath array.""" - return self.members["swath"] + value = self.members.get("swath") + if value is None: + raise KeyError("swath") + return value -class Sentinel1NoiseRangeMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1NoiseRangeMembers(TypedDict, closed=True, total=False): """Members for noise_range group.""" azimuth_time: ArraySpec[Any] @@ -781,36 +1029,51 @@ class Sentinel1NoiseRangeMembers(TypedDict, closed=True, total=False): # type: pixel: ArraySpec[Any] -class Sentinel1NoiseRangeGroup(GroupSpec[DatasetAttrs, Sentinel1NoiseRangeMembers]): # type: ignore[type-var] +class Sentinel1NoiseRangeGroup(GroupSpec[DatasetAttrs, Sentinel1NoiseRangeMembers]): """Noise range group containing range noise vectors.""" @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def ground_range(self) -> ArraySpec[Any]: """Get ground_range array.""" - return self.members["ground_range"] + value = self.members.get("ground_range") + if value is None: + raise KeyError("ground_range") + return value @property def line(self) -> ArraySpec[Any]: """Get line array.""" - return self.members["line"] + value = self.members.get("line") + if value is None: + raise KeyError("line") + return value @property def noise_range_lut(self) -> ArraySpec[Any]: """Get noise_range_lut array.""" - return self.members["noise_range_lut"] + value = self.members.get("noise_range_lut") + if value is None: + raise KeyError("noise_range_lut") + return value @property def pixel(self) -> ArraySpec[Any]: """Get pixel array.""" - return self.members["pixel"] + value = self.members.get("pixel") + if value is None: + raise KeyError("pixel") + return value -class Sentinel1QualityMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1QualityMembers(TypedDict, closed=True, total=False): """Members for quality group. Closed TypedDict with optional fields to support different product variants: @@ -824,7 +1087,7 @@ class Sentinel1QualityMembers(TypedDict, closed=True, total=False): # type: ign noise_range: Sentinel1NoiseRangeGroup -class Sentinel1QualityGroup(GroupSpec[DatasetAttrs, Sentinel1QualityMembers]): # type: ignore[type-var] +class Sentinel1QualityGroup(GroupSpec[DatasetAttrs, Sentinel1QualityMembers]): """Quality group containing quality assurance and calibration data. Supports both S1A (with noise_azimuth, noise_range) and S1C (without them) products. @@ -848,7 +1111,7 @@ def get_noise_range(self) -> Sentinel1NoiseRangeGroup | None: # Measurements -class Sentinel1MeasurementsMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel1MeasurementsMembers(TypedDict, closed=True, total=False): """Members for measurements group.""" azimuth_time: ArraySpec[Any] @@ -858,37 +1121,52 @@ class Sentinel1MeasurementsMembers(TypedDict, closed=True, total=False): # type pixel: ArraySpec[Any] -class Sentinel1MeasurementsGroup(GroupSpec[DatasetAttrs, Sentinel1MeasurementsMembers]): # type: ignore[type-var] +class Sentinel1MeasurementsGroup(GroupSpec[DatasetAttrs, Sentinel1MeasurementsMembers]): """Measurements group containing SAR imagery data.""" @property def azimuth_time(self) -> ArraySpec[Any]: """Get azimuth_time array.""" - return self.members["azimuth_time"] + value = self.members.get("azimuth_time") + if value is None: + raise KeyError("azimuth_time") + return value @property def grd(self) -> ArraySpec[Any]: """Get grd array.""" - return self.members["grd"] + value = self.members.get("grd") + if value is None: + raise KeyError("grd") + return value @property def ground_range(self) -> ArraySpec[Any]: """Get ground_range array.""" - return self.members["ground_range"] + value = self.members.get("ground_range") + if value is None: + raise KeyError("ground_range") + return value @property def line(self) -> ArraySpec[Any]: """Get line array.""" - return self.members["line"] + value = self.members.get("line") + if value is None: + raise KeyError("line") + return value @property def pixel(self) -> ArraySpec[Any]: """Get pixel array.""" - return self.members["pixel"] + value = self.members.get("pixel") + if value is None: + raise KeyError("pixel") + return value # Polarization group -class Sentinel1PolarizationMembers(TypedDict, closed=True): # type: ignore[call-arg] +class Sentinel1PolarizationMembers(TypedDict, closed=True): """Members for polarization group. Closed TypedDict - only conditions, measurements, quality keys are allowed. @@ -899,7 +1177,7 @@ class Sentinel1PolarizationMembers(TypedDict, closed=True): # type: ignore[call quality: Sentinel1QualityGroup -class Sentinel1PolarizationGroup(GroupSpec[DatasetAttrs, Sentinel1PolarizationMembers]): # type: ignore[type-var] +class Sentinel1PolarizationGroup(GroupSpec[DatasetAttrs, Sentinel1PolarizationMembers]): """Polarization-specific group containing all data for one polarization.""" @property diff --git a/src/eopf_geozarr/data_api/s2.py b/src/eopf_geozarr/data_api/s2.py index 3cfdbd02..c86c48a7 100644 --- a/src/eopf_geozarr/data_api/s2.py +++ b/src/eopf_geozarr/data_api/s2.py @@ -222,7 +222,7 @@ class Sentinel2ArrayAttributes(BaseModel): # Resolution-level members for probability data arrays -class ProbabilityArrayMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class ProbabilityArrayMembers(TypedDict, closed=True, total=False): """Members for probability arrays at a specific resolution (r10m, r20m, r60m). Closed TypedDict - contains probability arrays (cld, snw) and per-band/coordinate arrays. @@ -237,20 +237,20 @@ class ProbabilityArrayMembers(TypedDict, closed=True, total=False): # type: ign # Probability resolution groups (r10m, r20m, r60m) -class ProbabilityResolutionMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class ProbabilityResolutionMembers(TypedDict, closed=True, total=False): """Members for probability data containing resolution-level groups (r10m, r20m, r60m). Closed TypedDict - contains resolution groups as subgroups. All fields are optional since not all resolutions are always present. """ - r10m: GroupSpec[Any, ProbabilityArrayMembers] # type: ignore[type-var] - r20m: GroupSpec[Any, ProbabilityArrayMembers] # type: ignore[type-var] - r60m: GroupSpec[Any, ProbabilityArrayMembers] # type: ignore[type-var] + r10m: GroupSpec[Any, ProbabilityArrayMembers] + r20m: GroupSpec[Any, ProbabilityArrayMembers] + r60m: GroupSpec[Any, ProbabilityArrayMembers] # Resolution-level members for quicklook data arrays -class QuicklookArrayMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class QuicklookArrayMembers(TypedDict, closed=True, total=False): """Members for quicklook arrays at a specific resolution. Closed TypedDict - typically contains TCI (True Color Image) and optional band/coordinate arrays. @@ -264,20 +264,20 @@ class QuicklookArrayMembers(TypedDict, closed=True, total=False): # type: ignor # Quicklook resolution groups (r10m, r20m, r60m) -class QuicklookResolutionMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class QuicklookResolutionMembers(TypedDict, closed=True, total=False): """Members for quicklook data containing resolution-level groups (r10m, r20m, r60m). Closed TypedDict - contains resolution groups as subgroups. All fields are optional since not all resolutions are always present. """ - r10m: GroupSpec[Any, QuicklookArrayMembers] # type: ignore[type-var] - r20m: GroupSpec[Any, QuicklookArrayMembers] # type: ignore[type-var] - r60m: GroupSpec[Any, QuicklookArrayMembers] # type: ignore[type-var] + r10m: GroupSpec[Any, QuicklookArrayMembers] + r20m: GroupSpec[Any, QuicklookArrayMembers] + r60m: GroupSpec[Any, QuicklookArrayMembers] # Mask members - contains resolution-level groups or various classification/detector groups -class ConditionsMaskMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class ConditionsMaskMembers(TypedDict, closed=True, total=False): """Members for mask subgroup in conditions. Closed TypedDict - can contain either: @@ -297,7 +297,7 @@ class ConditionsMaskMembers(TypedDict, closed=True, total=False): # type: ignor # Geometry members - contains angle and orientation groups/arrays -class GeometryMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class GeometryMembers(TypedDict, closed=True, total=False): """Members for geometry group containing sun and viewing angles. Closed TypedDict - contains angle and geometry groups/arrays with flexible internal structure. @@ -321,7 +321,7 @@ class GeometryMembers(TypedDict, closed=True, total=False): # type: ignore[call # Meteorology members - contains CAMS and ECMWF atmospheric data -class MeteorologyMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class MeteorologyMembers(TypedDict, closed=True, total=False): """Members for meteorology group containing CAMS and ECMWF atmospheric data. Closed TypedDict - contains subgroups for different meteorological data sources. @@ -458,7 +458,7 @@ class Sentinel2CoordinateArray(ArraySpec[Sentinel2DataArrayAttrs]): # TypedDict definitions for members structure -class Sentinel2ResolutionMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel2ResolutionMembers(TypedDict, closed=True, total=False): """Members dict for a resolution dataset (r10m, r20m, r60m). Closed TypedDict - no extra keys are allowed beyond those explicitly defined. @@ -483,11 +483,11 @@ class Sentinel2ResolutionMembers(TypedDict, closed=True, total=False): # type: b12: ArraySpec[Any] -class Sentinel2ResolutionDataset(GroupSpec[DatasetAttrs, Sentinel2ResolutionMembers]): # type: ignore[type-var] +class Sentinel2ResolutionDataset(GroupSpec[DatasetAttrs, Sentinel2ResolutionMembers]): """A single resolution dataset within reflectance (r10m, r20m, r60m).""" -class Sentinel2ReflectanceMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel2ReflectanceMembers(TypedDict, closed=True, total=False): """Members for reflectance group. Closed TypedDict - only r10m, r20m, r60m keys are allowed. @@ -498,11 +498,11 @@ class Sentinel2ReflectanceMembers(TypedDict, closed=True, total=False): # type: r60m: Sentinel2ResolutionDataset -class Sentinel2ReflectanceGroup(GroupSpec[DatasetAttrs, Sentinel2ReflectanceMembers]): # type: ignore[type-var] +class Sentinel2ReflectanceGroup(GroupSpec[DatasetAttrs, Sentinel2ReflectanceMembers]): """Reflectance data organized by resolution.""" -class Sentinel2MeasurementsMembers(TypedDict, closed=True): # type: ignore[call-arg] +class Sentinel2MeasurementsMembers(TypedDict, closed=True): """Members for measurements group. Closed TypedDict - only 'reflectance' key is allowed. @@ -511,7 +511,7 @@ class Sentinel2MeasurementsMembers(TypedDict, closed=True): # type: ignore[call reflectance: Sentinel2ReflectanceGroup -class Sentinel2MeasurementsGroup(GroupSpec[DatasetAttrs, Sentinel2MeasurementsMembers]): # type: ignore[type-var] +class Sentinel2MeasurementsGroup(GroupSpec[DatasetAttrs, Sentinel2MeasurementsMembers]): """Measurements group containing reflectance data.""" @property @@ -523,7 +523,7 @@ def reflectance(self) -> Sentinel2ReflectanceGroup: # Quality data groups - need resolution-level typed groups -class Sentinel2AtmosphereResolutionMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel2AtmosphereResolutionMembers(TypedDict, closed=True, total=False): """Members for atmosphere data at a specific resolution. Closed TypedDict - may contain aot and/or wvp arrays depending on available data. @@ -537,12 +537,12 @@ class Sentinel2AtmosphereResolutionMembers(TypedDict, closed=True, total=False): class Sentinel2AtmosphereResolutionDataset( - GroupSpec[DatasetAttrs, Sentinel2AtmosphereResolutionMembers] # type: ignore[type-var] + GroupSpec[DatasetAttrs, Sentinel2AtmosphereResolutionMembers] ): """Atmosphere data at a single resolution.""" -class Sentinel2AtmosphereMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel2AtmosphereMembers(TypedDict, closed=True, total=False): """Members for atmosphere group containing resolution datasets.""" r10m: Sentinel2AtmosphereResolutionDataset @@ -550,25 +550,23 @@ class Sentinel2AtmosphereMembers(TypedDict, closed=True, total=False): # type: r60m: Sentinel2AtmosphereResolutionDataset -class Sentinel2AtmosphereDataset(GroupSpec[DatasetAttrs, Sentinel2AtmosphereMembers]): # type: ignore[type-var] +class Sentinel2AtmosphereDataset(GroupSpec[DatasetAttrs, Sentinel2AtmosphereMembers]): """Atmosphere quality data (AOT, WVP) at multiple resolutions.""" -class Sentinel2ProbabilityDataset( - GroupSpec[DatasetAttrs, ProbabilityResolutionMembers] # type: ignore[type-var] -): +class Sentinel2ProbabilityDataset(GroupSpec[DatasetAttrs, ProbabilityResolutionMembers]): """Probability data (cloud, snow) at multiple resolutions.""" -class Sentinel2QuicklookDataset(GroupSpec[DatasetAttrs, QuicklookResolutionMembers]): # type: ignore[type-var] +class Sentinel2QuicklookDataset(GroupSpec[DatasetAttrs, QuicklookResolutionMembers]): """True Color Image (TCI) quicklook data at multiple resolutions.""" -class Sentinel2MaskDataset(GroupSpec[DatasetAttrs, ConditionsMaskMembers]): # type: ignore[type-var] +class Sentinel2MaskDataset(GroupSpec[DatasetAttrs, ConditionsMaskMembers]): """Mask data containing classification and detector footprints.""" -class Sentinel2QualityMembers(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class Sentinel2QualityMembers(TypedDict, closed=True, total=False): """Members for quality group. Closed TypedDict with optional fields to accommodate different product levels: @@ -583,7 +581,7 @@ class Sentinel2QualityMembers(TypedDict, closed=True, total=False): # type: ign mask: Sentinel2MaskDataset -class Sentinel2QualityGroup(GroupSpec[DatasetAttrs, Sentinel2QualityMembers]): # type: ignore[type-var] +class Sentinel2QualityGroup(GroupSpec[DatasetAttrs, Sentinel2QualityMembers]): """Quality group containing atmosphere, probability, classification, and quicklook data. Supports both L2A products (Sentinel-2A, 2C) and L1C products (Sentinel-2B). @@ -607,19 +605,19 @@ def mask(self) -> Sentinel2MaskDataset | None: # Conditions groups -class Sentinel2GeometryGroup(GroupSpec[DatasetAttrs, GeometryMembers]): # type: ignore[type-var] +class Sentinel2GeometryGroup(GroupSpec[DatasetAttrs, GeometryMembers]): """Geometry group containing sun and viewing angles.""" -class Sentinel2MeteorologyGroup(GroupSpec[DatasetAttrs, MeteorologyMembers]): # type: ignore[type-var] +class Sentinel2MeteorologyGroup(GroupSpec[DatasetAttrs, MeteorologyMembers]): """Meteorology group containing CAMS and ECMWF atmospheric data.""" -class Sentinel2ConditionsMaskGroup(GroupSpec[DatasetAttrs, ConditionsMaskMembers]): # type: ignore[type-var] +class Sentinel2ConditionsMaskGroup(GroupSpec[DatasetAttrs, ConditionsMaskMembers]): """Mask subgroup in conditions.""" -class Sentinel2ConditionsMembers(TypedDict, closed=True): # type: ignore[call-arg] +class Sentinel2ConditionsMembers(TypedDict, closed=True): """Members for conditions group. Closed TypedDict - only geometry, mask, meteorology keys are allowed. @@ -630,7 +628,7 @@ class Sentinel2ConditionsMembers(TypedDict, closed=True): # type: ignore[call-a meteorology: Sentinel2MeteorologyGroup -class Sentinel2ConditionsGroup(GroupSpec[DatasetAttrs, Sentinel2ConditionsMembers]): # type: ignore[type-var] +class Sentinel2ConditionsGroup(GroupSpec[DatasetAttrs, Sentinel2ConditionsMembers]): """Conditions group containing geometry and meteorology data.""" def geometry(self) -> Sentinel2GeometryGroup | None: @@ -647,7 +645,7 @@ def meteorology(self) -> Sentinel2MeteorologyGroup | None: # Root model -class Sentinel2RootMembers(TypedDict, closed=True): # type: ignore[call-arg] +class Sentinel2RootMembers(TypedDict, closed=True): """Members for Sentinel-2 root group. Closed TypedDict - only measurements, quality, conditions keys are allowed. @@ -658,7 +656,7 @@ class Sentinel2RootMembers(TypedDict, closed=True): # type: ignore[call-arg] conditions: Sentinel2ConditionsGroup -class Sentinel2Root(GroupSpec[Sentinel2RootAttrs, Sentinel2RootMembers]): # type: ignore[type-var] +class Sentinel2Root(GroupSpec[Sentinel2RootAttrs, Sentinel2RootMembers]): """Complete Sentinel-2 EOPF Zarr hierarchy. The hierarchy follows EOPF organization: diff --git a/src/eopf_geozarr/pyz/common.py b/src/eopf_geozarr/pyz/common.py index 3b5bee04..fd21b00f 100644 --- a/src/eopf_geozarr/pyz/common.py +++ b/src/eopf_geozarr/pyz/common.py @@ -103,7 +103,7 @@ def _format_array_html(arr: Any) -> str: if value is None: continue dtype_str = str(value).strip() - value_str = dtype_str if dtype_str else "(not set)" + value_str = dtype_str or "(not set)" # Skip data_type if we already handled it via dtype elif prop_name == "data_type": if getattr(arr, "dtype", None) is not None: @@ -149,13 +149,12 @@ def _format_array_html(arr: Any) -> str: "
" ) - # Get items based on type - if is_dict_attrs: - attrs_dict = attributes - items = list(attrs_dict.items()) - else: # is_model_attrs - attrs_dict = attributes.model_dump() - items = list(attrs_dict.items()) + # Get items based on type. Use direct isinstance checks (not the + # boolean flags) so the type checker narrows `attributes`. + if isinstance(attributes, dict): + items = list(attributes.items()) + else: + items = list(attributes.model_dump().items()) for key, value in items: if isinstance(value, dict): diff --git a/src/eopf_geozarr/pyz/v3.py b/src/eopf_geozarr/pyz/v3.py index 405c8590..0613e507 100644 --- a/src/eopf_geozarr/pyz/v3.py +++ b/src/eopf_geozarr/pyz/v3.py @@ -40,7 +40,7 @@ class MyGroup(GroupSpec[Any, MyMembers]) TArraySpecType = TypeVar("TArraySpecType") -class GroupSpec(GroupSpecV3[TAttr, TMembers]): # type: ignore[type-var] +class GroupSpec(GroupSpecV3[TAttr, TMembers]): # TMembers is bound to the full members mapping (e.g. a TypedDict) by design, # whereas the parent's second type parameter expects a single member item type. attributes: TAttr diff --git a/src/eopf_geozarr/s2_optimization/s2_converter.py b/src/eopf_geozarr/s2_optimization/s2_converter.py index 05d2df5c..6a868087 100644 --- a/src/eopf_geozarr/s2_optimization/s2_converter.py +++ b/src/eopf_geozarr/s2_optimization/s2_converter.py @@ -5,7 +5,7 @@ from __future__ import annotations import time -from typing import TYPE_CHECKING, Any, TypedDict, cast +from typing import TYPE_CHECKING, TypedDict import structlog import xarray as xr @@ -21,7 +21,7 @@ from .s2_multiscale import create_multiscale_from_datatree if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Mapping log = structlog.get_logger() @@ -69,11 +69,14 @@ def initialize_crs_from_dataset(dt_input: xr.DataTree) -> CRS | None: continue dataset = group_node.ds - # Check if dataset has rio accessor with CRS + # Check if dataset has rio accessor with CRS. rioxarray returns a + # rasterio CRS; convert it to a pyproj CRS (the declared return type), + # which also validates the value at runtime. if hasattr(dataset, "rio"): try: - ds_crs = cast("CRS | None", dataset.rio.crs) - if ds_crs is not None: + rio_crs = dataset.rio.crs + if rio_crs is not None: + ds_crs = CRS.from_user_input(rio_crs) log.info("Initialized CRS from dataset", crs=str(ds_crs)) return ds_crs except Exception: @@ -83,8 +86,9 @@ def initialize_crs_from_dataset(dt_input: xr.DataTree) -> CRS | None: for var in dataset.data_vars.values(): if hasattr(var, "rio"): try: - var_crs = cast("CRS | None", var.rio.crs) - if var_crs is not None: + rio_crs = var.rio.crs + if rio_crs is not None: + var_crs = CRS.from_user_input(rio_crs) log.info("Initialized CRS from variable", crs=str(var_crs)) return var_crs except Exception: @@ -266,7 +270,7 @@ def convert_s2_optimized( return result_dt -def simple_root_consolidation(output_path: str, datasets: dict[str, dict]) -> None: +def simple_root_consolidation(output_path: str, datasets: Mapping[str, object]) -> None: """Simple root-level metadata consolidation with proper zarr group creation.""" # create missing intermediary groups (/conditions, /quality, etc.) # using the keys of the datasets dict @@ -321,6 +325,19 @@ def simple_root_consolidation(output_path: str, datasets: dict[str, dict]) -> No zarr.consolidate_metadata(output_path, zarr_format=3) +def _as_bbox(value: object) -> tuple[float, float, float, float] | None: + """Return *value* as a 4-tuple of floats, or ``None`` if it is not one. + + ``spatial:bbox`` is read from stored metadata, so its type is not known + statically; this verifies the shape at runtime rather than asserting it. + """ + if not isinstance(value, (list, tuple)) or len(value) != 4: + return None + if not all(isinstance(v, (int, float)) for v in value): + return None + return (float(value[0]), float(value[1]), float(value[2]), float(value[3])) + + def write_store_root_bbox(output_path: str) -> None: """Write `spatial:bbox` and `proj:code` at the store root. @@ -337,16 +354,20 @@ def write_store_root_bbox(output_path: str) -> None: def _walk(group: zarr.Group) -> None: attrs = dict(group.attrs) - bbox = cast("Sequence[float] | None", attrs.get("spatial:bbox")) + bbox = attrs.get("spatial:bbox") code = attrs.get("proj:code") - if bbox is not None and len(bbox) == 4: + # spatial:bbox comes from stored metadata; verify it is a 4-element + # numeric sequence before use rather than trusting the type. + corners = _as_bbox(bbox) + if corners is not None: + x0, y0, x1, y1 = corners if code and code != "EPSG:4326": transformer = Transformer.from_crs(code, "EPSG:4326", always_xy=True) - xmin, ymin = transformer.transform(bbox[0], bbox[1]) - xmax, ymax = transformer.transform(bbox[2], bbox[3]) + xmin, ymin = transformer.transform(x0, y0) + xmax, ymax = transformer.transform(x1, y1) bboxes_4326.append((xmin, ymin, xmax, ymax)) else: - bboxes_4326.append((float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3]))) + bboxes_4326.append((x0, y0, x1, y1)) for child in group.groups(): _walk(child[1]) @@ -411,7 +432,7 @@ def create_result_datatree(output_path: str) -> xr.DataTree: def is_sentinel2_dataset(group: zarr.Group) -> bool: from eopf_geozarr.pyz.v2 import GroupSpec - adapter = TypeAdapter(Sentinel1Root | Sentinel2Root) # type: ignore[var-annotated] + adapter = TypeAdapter(Sentinel1Root | Sentinel2Root) try: model = adapter.validate_python(GroupSpec.from_zarr(group).model_dump()) except ValueError as e: @@ -421,7 +442,16 @@ def is_sentinel2_dataset(group: zarr.Group) -> bool: return isinstance(model, Sentinel2Root) -def validate_optimized_dataset(dataset_path: str) -> dict[str, Any]: +class ValidationResult(TypedDict): + """Result of validating an optimized Sentinel-2 dataset.""" + + is_valid: bool + issues: list[str] + warnings: list[str] + summary: dict[str, object] + + +def validate_optimized_dataset(dataset_path: str) -> ValidationResult: """ Validate an optimized Sentinel-2 dataset. diff --git a/src/eopf_geozarr/s2_optimization/s2_multiscale.py b/src/eopf_geozarr/s2_optimization/s2_multiscale.py index 1782a80d..46e59aa1 100644 --- a/src/eopf_geozarr/s2_optimization/s2_multiscale.py +++ b/src/eopf_geozarr/s2_optimization/s2_multiscale.py @@ -11,8 +11,8 @@ import numpy as np import structlog import xarray as xr -from dask import delayed from dask.array import from_delayed +from dask.delayed import delayed from pydantic.experimental.missing_sentinel import MISSING from pyproj import CRS from zarr.codecs import CastValue @@ -105,7 +105,9 @@ def _preferred_spatial_transform( rio_value = dataset.rio.transform if callable(rio_value): rio_value = rio_value() - rio_values = tuple(float(value) for value in tuple(rio_value)[:6]) + # rio transform value is dynamically typed; it is iterable at runtime. + rio_iter = cast("tuple[float, ...]", tuple(rio_value)) # pyright: ignore[reportArgumentType] + rio_values = tuple(float(value) for value in rio_iter[:6]) if len(rio_values) == 6: rio_transform = ( rio_values[0], @@ -535,7 +537,7 @@ def create_measurements_encoding( for key in keep_keys: if key in var_data.encoding: - var_encoding[key] = var_data.encoding[key] # type: ignore[literal-required] + var_encoding[key] = var_data.encoding[key] if len(set(var_data.encoding.keys()) - XARRAY_ENCODING_KEYS) > 0: log.warning( @@ -677,7 +679,7 @@ def add_multiscales_metadata_to_parent( # Defensive guard retained for runtime safety even though the typed # contract (Mapping[str, xr.Dataset]) means mypy proves it unreachable. if dataset is None: - continue # type: ignore[unreachable] + continue # Get first data variable to extract dimensions first_var = next(iter(dataset.data_vars.values())) @@ -781,7 +783,7 @@ def add_multiscales_metadata_to_parent( log.info(" Could not create overview levels for {}", base_path=group.path) return - layout: list[zcm.ScaleLevel] | MISSING = MISSING # type: ignore[valid-type] + layout: list[zcm.ScaleLevel] | MISSING = MISSING layout = [] @@ -812,6 +814,7 @@ def add_multiscales_metadata_to_parent( scale_level_data["transform"] = multiscale_transform # Add spatial properties + assert "spatial_shape" in overview_level # always populated by the producer above scale_level_data["spatial:shape"] = overview_level["spatial_shape"] if "spatial_transform" in overview_level: spatial_transform = overview_level["spatial_transform"] @@ -828,7 +831,7 @@ def add_multiscales_metadata_to_parent( # (multiscales, spatial, proj). multiscales_data = cast( "MultiscalesAttrs", - MultiscaleMeta(layout=layout, resampling_method="average").model_dump(), + MultiscaleMeta(layout=tuple(layout), resampling_method="average").model_dump(), ) attrs_to_write: dict[str, Any] = {} @@ -866,7 +869,7 @@ def create_original_encoding(dataset: xr.Dataset) -> dict[str, XarrayDataArrayEn var_encoding["compressors"] = (compressor,) for key in XARRAY_ENCODING_KEYS - {"compressors", "fill_value"}: if key in var_data.encoding: - var_encoding[key] = var_data.encoding[key] # type: ignore[literal-required] + var_encoding[key] = var_data.encoding[key] # Set the zarr-level `fill_value` explicitly rather than letting xarray # decide — different xarray versions infer different defaults from the # variable's `_FillValue`. See `explicit_fill_value` for the rationale. @@ -977,9 +980,7 @@ def create_lazy_downsample_operation_from_existing( ) -> xr.DataArray: """Create lazy downsampling operation from existing data.""" - # `dask.delayed` is untyped, so the decorator would otherwise make - # `downsample_operation` untyped under strict mypy. - @delayed # type: ignore[untyped-decorator] + @delayed def downsample_operation() -> Any: var_type = determine_variable_type(str(source_data.name), source_data) return downsample_variable(source_data, target_height, target_width, var_type) @@ -1089,7 +1090,7 @@ def stream_write_dataset( try: client = distributed.Client.current() # Use client.compute to get a proper Future with status - future = client.compute(write_job) + future = cast("distributed.Future", client.compute(write_job)) log.info("Using distributed client for write job monitoring") try: diff --git a/tests/conftest.py b/tests/conftest.py index c08bade3..59746f8f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,7 +28,9 @@ def read_json(path: pathlib.Path) -> dict[str, object]: """ Read the contents of path as JSON """ - return json.loads(path.read_text()) + obj = json.loads(path.read_text()) + assert isinstance(obj, dict) + return obj def get_stem(p: pathlib.Path) -> str: @@ -40,8 +42,9 @@ def create_group_from_json(source_path: pathlib.Path, out_path: pathlib.Path) -> Create a Zarr V2 group from a JSON model """ out_dir = out_path / (source_path.stem + ".zarr") - g = GroupSpecV2(**read_json(source_path)) - g.to_zarr(out_dir, path="") + g: GroupSpecV2 = GroupSpecV2.model_validate(read_json(source_path)) + # to_zarr is annotated to take a Store but accepts a path-like at runtime. + g.to_zarr(out_dir, path="") # type: ignore[arg-type] return out_dir @@ -87,8 +90,9 @@ def s2_geozarr_group_example(request: pytest.FixtureRequest) -> zarr.Group: Return a memory-backed Zarr V3 Group based on a sentinel 2 product converted to geozarr """ source_path: pathlib.Path = request.param - store = {} - return GroupSpecV3(**read_json(source_path)).to_zarr(store, path="") + store: dict[str, bytes] = {} + # to_zarr is annotated to take a Store but accepts a dict-backed store at runtime. + return GroupSpecV3.model_validate(read_json(source_path)).to_zarr(store, path="") # type: ignore[arg-type] @pytest.fixture(params=optimized_geozarr_example_paths, ids=get_stem) @@ -97,8 +101,9 @@ def s2_optimized_geozarr_group_example(request: pytest.FixtureRequest) -> zarr.G Return a memory-backed Zarr V3 Group based on a sentinel 2 product converted to geozarr """ source_path: pathlib.Path = request.param - store = {} - return GroupSpecV3(**read_json(source_path)).to_zarr(store, path="") + store: dict[str, bytes] = {} + # to_zarr is annotated to take a Store but accepts a dict-backed store at runtime. + return GroupSpecV3.model_validate(read_json(source_path)).to_zarr(store, path="") # type: ignore[arg-type] @pytest.fixture(params=zcm_multiscales_example_paths, ids=get_stem) @@ -207,7 +212,7 @@ def _verify_geozarr_spec_compliance(output_path: pathlib.Path, group: str) -> No # Check coordinates for coord_name in ds.coords: - if coord_name not in ["spatial_ref"]: # Skip CRS coordinate + if coord_name != "spatial_ref": # Skip CRS coordinate assert "_ARRAY_DIMENSIONS" in ds[coord_name].attrs, ( f"Missing _ARRAY_DIMENSIONS for coordinate {coord_name} in {group}" ) diff --git a/tests/test_array_attrs.py b/tests/test_array_attrs.py index 53ef3e0a..a4234a11 100644 --- a/tests/test_array_attrs.py +++ b/tests/test_array_attrs.py @@ -46,7 +46,8 @@ def _is_float_array(node: dict) -> bool: @pytest.fixture(params=_SNAPSHOTS, ids=lambda p: p.stem) def snapshot(request: pytest.FixtureRequest) -> dict: - return json.loads(request.param.read_text()) + loaded: dict = json.loads(request.param.read_text()) + return loaded def test_no_eopf_attrs(snapshot: dict) -> None: @@ -163,7 +164,9 @@ def test_fill_value_masking_roundtrip(tmp_path: pathlib.Path) -> None: "NaN cells in converter output should be masked when opened with " "use_zarr_fill_value_as_mask=True" ) - assert masked.mask[0, 0], "nodata corner cell must be masked" - assert not masked.mask[-1, -1], "valid cell must not be masked" + mask = masked.mask + assert isinstance(mask, np.ndarray), "mask must be an array, not a scalar" + assert mask[0, 0], "nodata corner cell must be masked" + assert not mask[-1, -1], "valid cell must not be masked" finally: reopened.close() diff --git a/tests/test_cli_e2e.py b/tests/test_cli_e2e.py index 6b11b120..b91ce2b9 100644 --- a/tests/test_cli_e2e.py +++ b/tests/test_cli_e2e.py @@ -140,7 +140,7 @@ def test_cli_convert_real_sentinel2_data(s2_group_example: Path, tmp_path: Path) GroupSpec.from_zarr(zarr.open_group(output_path)).model_dump() ) assert expected_structure_json == observed_structure_json, view_json_diff( - expected_structure_json, observed_structure_json + dict(expected_structure_json), dict(observed_structure_json) ) @@ -206,7 +206,7 @@ def test_cli_crs_groups_option() -> None: assert "Groups that need CRS information added" in result.stdout, "Help text should be present" -def test_cli_convert_with_crs_groups(s2_group_example, tmp_path: Path) -> None: +def test_cli_convert_with_crs_groups(s2_group_example: Path, tmp_path: Path) -> None: """ Test CLI conversion with --crs-groups option using real Sentinel-2 data. diff --git a/tests/test_convention_attrs.py b/tests/test_convention_attrs.py index 0b3ab578..f3ddf576 100644 --- a/tests/test_convention_attrs.py +++ b/tests/test_convention_attrs.py @@ -2,11 +2,12 @@ from __future__ import annotations -from typing import Any +from typing import cast import pytest from pyproj import CRS from zarr_cm import geo_proj +from zarr_cm import multiscales as multiscales_cm from zarr_cm import spatial as spatial_cm from eopf_geozarr.conversion.utils import ( @@ -48,18 +49,21 @@ def test_build_convention_attrs_data_and_cmos() -> None: }, crs=CRS.from_epsg(32632), ) - assert out["spatial:dimensions"] == ["y", "x"] - assert out["spatial:bbox"] == bbox - assert out["spatial:registration"] == "pixel" - assert out["proj:code"] == "EPSG:32632" - names = [c["name"] for c in out["zarr_conventions"]] - assert names == [spatial_cm.CMO["name"], geo_proj.CMO["name"]] + data: dict[str, object] = dict(out) + assert data["spatial:dimensions"] == ["y", "x"] + assert data["spatial:bbox"] == bbox + assert data["spatial:registration"] == "pixel" + assert data["proj:code"] == "EPSG:32632" + conventions = out.get("zarr_conventions") + assert conventions is not None + names = [c.get("name") for c in conventions] + assert names == [spatial_cm.CMO.get("name"), geo_proj.CMO.get("name")] def test_build_convention_attrs_matches_handwritten() -> None: """Output is byte-equivalent to the previous hand-assembled dict.""" bbox = [300000.0, 4990200.0, 409800.0, 5100000.0] - hand: dict[str, Any] = { + hand: dict[str, object] = { "spatial:dimensions": ["y", "x"], "spatial:bbox": bbox, "spatial:registration": "pixel", @@ -81,39 +85,48 @@ def test_build_convention_attrs_validates() -> None: """Invalid spatial data is rejected by zarr-cm validation.""" with pytest.raises(ValueError, match="spatial:dimensions"): build_convention_attrs( - spatial={"spatial:registration": "pixel"}, # missing required dimensions + # missing required dimensions — exercises runtime validation + spatial=cast("spatial_cm.SpatialAttrs", {"spatial:registration": "pixel"}), crs=CRS.from_epsg(32632), ) def test_build_convention_attrs_with_multiscales() -> None: """With multiscales, CMOs are ordered [multiscales, spatial, proj].""" - from zarr_cm import multiscales as multiscales_cm - out = build_convention_attrs( - multiscales={ - "layout": [ - {"asset": "r10m", "spatial:shape": [10980, 10980]}, - { - "asset": "r20m", - "derived_from": "r10m", - "transform": {"scale": [2.0, 2.0], "translation": [0.0, 0.0]}, - "spatial:shape": [5490, 5490], - }, - ], - "resampling_method": "average", - }, + multiscales=cast( + "multiscales_cm.MultiscalesAttrs", + { + "layout": [ + {"asset": "r10m", "spatial:shape": [10980, 10980]}, + { + "asset": "r20m", + "derived_from": "r10m", + "transform": {"scale": [2.0, 2.0], "translation": [0.0, 0.0]}, + "spatial:shape": [5490, 5490], + }, + ], + "resampling_method": "average", + }, + ), spatial={"spatial:dimensions": ["y", "x"]}, crs=CRS.from_epsg(32632), ) - names = [c["name"] for c in out["zarr_conventions"]] + conventions = out.get("zarr_conventions") + assert conventions is not None + names = [c.get("name") for c in conventions] assert names == [ - multiscales_cm.CMO["name"], - spatial_cm.CMO["name"], - geo_proj.CMO["name"], + multiscales_cm.CMO.get("name"), + spatial_cm.CMO.get("name"), + geo_proj.CMO.get("name"), ] # extra layout keys (spatial:shape) survive the round-trip - assert out["multiscales"]["layout"][0]["spatial:shape"] == [10980, 10980] + multiscales = out.get("multiscales") + assert multiscales is not None + layout = multiscales["layout"] + assert isinstance(layout, list) + first_layout: dict[str, object] = dict(layout[0]) + assert first_layout["spatial:shape"] == [10980, 10980] def test_build_convention_attrs_multiscales_validation() -> None: diff --git a/tests/test_data_api/conftest.py b/tests/test_data_api/conftest.py index be567726..03537803 100644 --- a/tests/test_data_api/conftest.py +++ b/tests/test_data_api/conftest.py @@ -5,13 +5,9 @@ import re from collections.abc import Mapping from pathlib import Path -from typing import TYPE_CHECKING import pytest -if TYPE_CHECKING: - from typing import Any - def extract_json_code_blocks( markdown_content: str, @@ -53,7 +49,8 @@ def extract_json_code_blocks( end_line: int = i # Line with closing ``` raw_json: str = "\n".join(json_lines) - parsed_json: Any = json.loads(raw_json) + parsed_json: object = json.loads(raw_json) + assert isinstance(parsed_json, dict) code_blocks[(start_line + 1, end_line)] = parsed_json i += 1 diff --git a/tests/test_data_api/test_geoproj.py b/tests/test_data_api/test_geoproj.py index 5949099c..c9f734ce 100644 --- a/tests/test_data_api/test_geoproj.py +++ b/tests/test_data_api/test_geoproj.py @@ -14,7 +14,7 @@ class TestProj: def test_proj_with_epsg_code(self) -> None: """Test creation with EPSG code.""" - proj = Proj(**{"proj:code": "EPSG:4326"}) + proj = Proj.model_validate({"proj:code": "EPSG:4326"}) assert proj.code == "EPSG:4326" assert proj.wkt2 is None @@ -23,7 +23,7 @@ def test_proj_with_epsg_code(self) -> None: def test_proj_with_wkt2(self) -> None: """Test creation with WKT2 string.""" wkt2_example = 'GEOGCRS["WGS 84",DATUM["World Geodetic System 1984"]]' - proj = Proj(**{"proj:wkt2": wkt2_example}) + proj = Proj.model_validate({"proj:wkt2": wkt2_example}) assert proj.wkt2 == wkt2_example assert proj.code is None @@ -36,7 +36,7 @@ def test_proj_with_projjson(self) -> None: "type": "GeographicCRS", "name": "WGS 84", } - proj = Proj(**{"proj:projjson": projjson_data}) + proj = Proj.model_validate({"proj:projjson": projjson_data}) assert proj.projjson is not None assert proj.code is None @@ -45,7 +45,7 @@ def test_proj_with_projjson(self) -> None: def test_proj_validation_error_no_crs(self) -> None: """Test that missing all CRS fields raises ValidationError.""" with pytest.raises(ValidationError) as exc_info: - Proj() + Proj() # pyright: ignore[reportCallIssue] # no-args construction tests the validation error assert "At least one of proj:code, proj:wkt2, or proj:projjson must be provided" in str( exc_info.value @@ -54,7 +54,7 @@ def test_proj_validation_error_no_crs(self) -> None: def test_proj_multiple_crs_fields(self) -> None: """Test that multiple CRS fields can be provided.""" wkt2_example = 'GEOGCRS["WGS 84",DATUM["World Geodetic System 1984"]]' - proj = Proj(**{"proj:code": "EPSG:4326", "proj:wkt2": wkt2_example}) + proj = Proj.model_validate({"proj:code": "EPSG:4326", "proj:wkt2": wkt2_example}) assert proj.code == "EPSG:4326" assert proj.wkt2 == wkt2_example @@ -62,7 +62,7 @@ def test_proj_multiple_crs_fields(self) -> None: def test_proj_serialization_by_alias(self) -> None: """Test that serialization uses aliases (proj: prefixes).""" - proj = Proj(**{"proj:code": "EPSG:32633"}) + proj = Proj.model_validate({"proj:code": "EPSG:32633"}) result = proj.model_dump() # Should serialize with proj: prefix @@ -74,7 +74,7 @@ def test_proj_serialization_by_alias(self) -> None: def test_proj_none_fields_excluded(self) -> None: """Test that None fields are excluded from serialization.""" - proj = Proj(**{"proj:code": "EPSG:4326"}) + proj = Proj.model_validate({"proj:code": "EPSG:4326"}) result = proj.model_dump() # None fields should be excluded @@ -86,8 +86,8 @@ def test_proj_none_fields_excluded(self) -> None: def test_proj_extra_fields_allowed(self) -> None: """Test that extra fields are allowed.""" - proj = Proj( - **{ + proj = Proj.model_validate( + { "proj:code": "EPSG:4326", "custom_field": "custom_value", "proj:custom": "also_allowed", @@ -104,7 +104,7 @@ def test_proj_roundtrip_serialization(self) -> None: original_data = {"proj:code": "EPSG:32633", "proj:wkt2": 'PROJCRS["WGS 84 / UTM zone 33N"]'} # Create model, serialize, then recreate - proj1 = Proj(**original_data) + proj1 = Proj.model_validate(original_data) serialized = proj1.model_dump() proj2 = Proj(**serialized) @@ -124,8 +124,8 @@ def test_geoproj_is_proj_alias(self) -> None: def test_geoproj_functionality(self) -> None: """Test that GeoProj works exactly like Proj.""" # Create using both classes - proj_instance = Proj(**{"proj:code": "EPSG:4326"}) - geoproj_instance = GeoProj(**{"proj:code": "EPSG:4326"}) + proj_instance = Proj.model_validate({"proj:code": "EPSG:4326"}) + geoproj_instance = GeoProj.model_validate({"proj:code": "EPSG:4326"}) # Should be instances of the same class assert type(proj_instance) is type(geoproj_instance) diff --git a/tests/test_data_api/test_geozarr/test_common.py b/tests/test_data_api/test_geozarr/test_common.py index fa54ffb8..e7286726 100644 --- a/tests/test_data_api/test_geozarr/test_common.py +++ b/tests/test_data_api/test_geozarr/test_common.py @@ -1,11 +1,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from collections.abc import Mapping +from typing import TYPE_CHECKING import numpy as np import pytest from pydantic_zarr.core import tuplify_json +from pydantic_zarr.v2 import AnyGroupSpec as AnyGroupSpec_V2 from pydantic_zarr.v2 import GroupSpec as GroupSpec_V2 +from pydantic_zarr.v3 import AnyGroupSpec as AnyGroupSpec_V3 from pydantic_zarr.v3 import GroupSpec as GroupSpec_V3 from eopf_geozarr.data_api.geozarr.common import ( @@ -33,7 +36,7 @@ DataArray_V3.from_array(np.arange(10), dimension_names=("time",)), ], ) -def test_datarraylike(obj: DataArray_V2 | DataArray_V3) -> None: +def test_datarraylike(obj: object) -> None: """ Test that the DataArrayLike protocol works correctly """ @@ -41,7 +44,7 @@ def test_datarraylike(obj: DataArray_V2 | DataArray_V3) -> None: @pytest.mark.parametrize("obj", [GroupSpec_V2(attributes={}), GroupSpec_V3(attributes={})]) -def test_grouplike(obj: GroupSpec_V3[Any, Any] | GroupSpec_V2[Any, Any]) -> None: +def test_grouplike(obj: AnyGroupSpec_V3 | AnyGroupSpec_V2) -> None: """ Test that the GroupLike protocol works correctly """ @@ -80,9 +83,12 @@ def test_multiscales_round_trip(s2_optimized_geozarr_group_example: zarr.Group) """ Ensure that we can round-trip multiscale metadata through the `Multiscales` model. """ - source_untyped = GroupSpec_V3.from_zarr(s2_optimized_geozarr_group_example) + source_untyped: AnyGroupSpec_V3 = GroupSpec_V3.from_zarr(s2_optimized_geozarr_group_example) flat = source_untyped.to_flat() - meta = flat["/measurements/reflectance"].attributes["multiscales"] + attributes = flat["/measurements/reflectance"].attributes + assert isinstance(attributes, Mapping) + meta = attributes["multiscales"] + assert isinstance(meta, Mapping) # pull out the multiscales keys, ignore extra submodel = tuplify_json({k: meta[k] for k in ZCMMultiscales.model_fields if k in meta}) assert ZCMMultiscales(**submodel).model_dump() == submodel @@ -95,11 +101,11 @@ def test_projattrs_crs_required() -> None: with pytest.raises( ValueError, match=r"One of 'code', 'wkt2', or 'projjson' must be provided\." ): - ProjAttrs() + ProjAttrs() # pyright: ignore[reportCallIssue] # no-args construction tests the validation error def test_projattrs_json_examples( - proj_attrs_examples: dict[tuple[int, int], dict[str, Any]], + proj_attrs_examples: dict[tuple[int, int], dict[str, object]], ) -> None: """ Test that proj attributes in the JSON examples of the proj extension README are valid. @@ -108,19 +114,22 @@ def test_projattrs_json_examples( for json_block in proj_attrs_examples.values(): # Check if this JSON block contains geo.proj attributes - if "attributes" in json_block and isinstance(json_block["attributes"], dict): - geo: Any = json_block["attributes"].get("geo") + attributes = json_block.get("attributes") + if isinstance(attributes, dict): + geo: object = attributes.get("geo") if geo and isinstance(geo, dict) and "proj" in geo: proj_examples_found += 1 - proj_data: dict[str, Any] = geo["proj"] + proj_data_obj: object = geo["proj"] + assert isinstance(proj_data_obj, dict) + proj_data: dict[str, object] = proj_data_obj # Validate that ProjAttrs can parse this data - proj_attrs: ProjAttrs = ProjAttrs(**proj_data) + proj_attrs: ProjAttrs = ProjAttrs.model_validate(proj_data) # Verify that all fields from the original data are present in the model for key, value in proj_data.items(): if value is not None: - model_value: Any = getattr(proj_attrs, key) + model_value: object = getattr(proj_attrs, key) # Handle tuple/list comparison for transform and bbox fields if isinstance(value, list) and isinstance(model_value, tuple): assert tuple(value) == model_value, f"Field {key} mismatch" diff --git a/tests/test_data_api/test_geozarr/test_multiscales/test_geozarr.py b/tests/test_data_api/test_geozarr/test_multiscales/test_geozarr.py index a9c9418c..3391d796 100644 --- a/tests/test_data_api/test_geozarr/test_multiscales/test_geozarr.py +++ b/tests/test_data_api/test_geozarr/test_multiscales/test_geozarr.py @@ -1,7 +1,8 @@ -from typing import Any, Literal +from typing import Literal import pytest from pydantic.experimental.missing_sentinel import MISSING +from zarr_cm import ConventionMetadataObject from zarr_cm import multiscales as multiscales_cm from eopf_geozarr.data_api.geozarr.multiscales import tms, zcm @@ -18,13 +19,13 @@ def test_multiscale_group_attrs(multiscale_flavor: set[Literal["zcm", "tms"]]) - """ zcm_meta: dict[str, object] = {} tms_meta: dict[str, object] = {} - zarr_conventions_meta: MISSING | tuple[Any, ...] = MISSING + zarr_conventions_meta: tuple[ConventionMetadataObject, ...] | MISSING = MISSING if "zcm" in multiscale_flavor: layout = ( zcm.ScaleLevel( asset="level_0", - transform={"scale": (1.0, 1.0), "translation": (0.0, 0.0)}, + transform=zcm.Transform(scale=(1.0, 1.0), translation=(0.0, 0.0)), ), ) zcm_meta = zcm.Multiscales(layout=layout, resampling_method="nearest").model_dump() @@ -58,13 +59,17 @@ def test_multiscale_group_attrs(multiscale_flavor: set[Literal["zcm", "tms"]]) - ) }, ).model_dump() - multiscale_meta = MultiscaleMeta(**{**zcm_meta, **tms_meta}) + multiscale_meta = MultiscaleMeta.model_validate({**zcm_meta, **tms_meta}) multiscale_group_attrs = MultiscaleGroupAttrs( zarr_conventions=zarr_conventions_meta, multiscales=multiscale_meta ) if "zcm" in multiscale_flavor: assert "zcm" in multiscale_group_attrs.multiscale_meta - assert multiscale_group_attrs.multiscale_meta["zcm"] == zcm.Multiscales(**zcm_meta) + assert multiscale_group_attrs.multiscale_meta["zcm"] == zcm.Multiscales.model_validate( + zcm_meta + ) if "tms" in multiscale_flavor: assert "tms" in multiscale_group_attrs.multiscale_meta - assert multiscale_group_attrs.multiscale_meta["tms"] == tms.Multiscales(**tms_meta) + assert multiscale_group_attrs.multiscale_meta["tms"] == tms.Multiscales.model_validate( + tms_meta + ) diff --git a/tests/test_data_api/test_geozarr/test_multiscales/test_zcm.py b/tests/test_data_api/test_geozarr/test_multiscales/test_zcm.py index 32d3dc4c..52be4251 100644 --- a/tests/test_data_api/test_geozarr/test_multiscales/test_zcm.py +++ b/tests/test_data_api/test_geozarr/test_multiscales/test_zcm.py @@ -30,7 +30,7 @@ def test_scale_level_from_group() -> None: """ meta = {"group": "1", "from_group": "0"} with pytest.raises(ValidationError): - ScaleLevel(**meta) + ScaleLevel.model_validate(meta) def test_scalelevel_json() -> None: @@ -46,4 +46,4 @@ def test_scalelevel_json() -> None: }, "resampling_method": "nearest", } - assert ScaleLevel(**x).model_dump() == x + assert ScaleLevel.model_validate(x).model_dump() == x diff --git a/tests/test_data_api/test_projjson.py b/tests/test_data_api/test_projjson.py index ada54398..ae2214f8 100644 --- a/tests/test_data_api/test_projjson.py +++ b/tests/test_data_api/test_projjson.py @@ -7,8 +7,6 @@ from __future__ import annotations -from typing import Any - import pytest from pydantic import ValidationError @@ -39,51 +37,52 @@ class TestBasicModels: def test_id_model(self) -> None: """Test Id model validation""" # Valid ID with required fields - id_data: dict[str, Any] = {"authority": "EPSG", "code": 4326} - id_obj: Id = Id(**id_data) + id_data: dict[str, object] = {"authority": "EPSG", "code": 4326} + id_obj: Id = Id.model_validate(id_data) assert id_obj.authority == "EPSG" assert id_obj.code == 4326 # ID with string code - id_data_str: dict[str, Any] = {"authority": "EPSG", "code": "4326"} - id_obj_str: Id = Id(**id_data_str) + id_data_str: dict[str, object] = {"authority": "EPSG", "code": "4326"} + id_obj_str: Id = Id.model_validate(id_data_str) assert id_obj_str.code == "4326" # ID with optional fields - id_full: dict[str, Any] = { + id_full: dict[str, object] = { "authority": "EPSG", "code": 4326, "version": "10.095", "authority_citation": "EPSG Geodetic Parameter Dataset", "uri": "urn:ogc:def:crs:EPSG::4326", } - id_obj_full: Id = Id(**id_full) + id_obj_full: Id = Id.model_validate(id_full) assert id_obj_full.version == "10.095" assert id_obj_full.uri == "urn:ogc:def:crs:EPSG::4326" # Missing required field should raise ValidationError with pytest.raises(ValidationError): - Id(authority="EPSG") # missing code + Id(authority="EPSG") # type: ignore[call-arg] # missing code (intentional) def test_unit_model(self) -> None: """Test Unit model validation""" - unit_data: dict[str, Any] = { + unit_data: dict[str, object] = { "type": "Unit", "name": "metre", "conversion_factor": 1.0, } - unit: Unit = Unit(**unit_data) + unit: Unit = Unit.model_validate(unit_data) assert unit.name == "metre" assert unit.conversion_factor == 1.0 # With ID - unit_with_id: dict[str, Any] = { + unit_with_id: dict[str, object] = { "type": "Unit", "name": "degree", "conversion_factor": 0.017453292519943295, "id": {"authority": "EPSG", "code": 9122}, } - unit = Unit(**unit_with_id) + unit = Unit.model_validate(unit_with_id) + assert unit.id is not None assert unit.id.authority == "EPSG" assert unit.id.code == 9122 @@ -101,17 +100,18 @@ def test_bbox_model(self) -> None: # Missing required field with pytest.raises(ValidationError): - BBox(east_longitude=180.0, west_longitude=-180.0) # missing latitude fields + # missing latitude fields (intentional) + BBox(east_longitude=180.0, west_longitude=-180.0) # type: ignore[call-arg] def test_axis_model(self) -> None: """Test Axis model validation""" - axis_data: dict[str, str] = { + axis_data: dict[str, object] = { "type": "Axis", "name": "Geodetic latitude", "abbreviation": "Lat", "direction": "north", } - axis: Axis = Axis(**axis_data) + axis: Axis = Axis.model_validate(axis_data) assert axis.name == "Geodetic latitude" assert axis.direction == "north" @@ -121,7 +121,7 @@ def test_axis_model(self) -> None: type="Axis", name="Invalid", abbreviation="Inv", - direction="invalid_direction", + direction="invalid_direction", # type: ignore[arg-type] # invalid direction (intentional) ) @@ -130,35 +130,35 @@ class TestEllipsoidModel: def test_ellipsoid_with_semi_axes(self) -> None: """Test ellipsoid with semi-major and semi-minor axes""" - ellipsoid_data: dict[str, Any] = { + ellipsoid_data: dict[str, object] = { "type": "Ellipsoid", "name": "WGS 84", "semi_major_axis": 6378137.0, "semi_minor_axis": 6356752.314245179, } - ellipsoid: Ellipsoid = Ellipsoid(**ellipsoid_data) + ellipsoid: Ellipsoid = Ellipsoid.model_validate(ellipsoid_data) assert ellipsoid.name == "WGS 84" assert ellipsoid.semi_major_axis == 6378137.0 def test_ellipsoid_with_inverse_flattening(self) -> None: """Test ellipsoid with inverse flattening""" - ellipsoid_data: dict[str, Any] = { + ellipsoid_data: dict[str, object] = { "type": "Ellipsoid", "name": "WGS 84", "semi_major_axis": 6378137.0, "inverse_flattening": 298.257223563, } - ellipsoid: Ellipsoid = Ellipsoid(**ellipsoid_data) + ellipsoid: Ellipsoid = Ellipsoid.model_validate(ellipsoid_data) assert ellipsoid.inverse_flattening == 298.257223563 def test_ellipsoid_sphere(self) -> None: """Test spherical ellipsoid (equal radii)""" - ellipsoid_data: dict[str, Any] = { + ellipsoid_data: dict[str, object] = { "type": "Ellipsoid", "name": "Sphere", "radius": 6371000.0, } - ellipsoid: Ellipsoid = Ellipsoid(**ellipsoid_data) + ellipsoid: Ellipsoid = Ellipsoid.model_validate(ellipsoid_data) assert ellipsoid.radius == 6371000.0 @@ -167,7 +167,7 @@ class TestCoordinateSystemModel: def test_ellipsoidal_coordinate_system(self) -> None: """Test ellipsoidal coordinate system""" - cs_data: dict[str, Any] = { + cs_data: dict[str, object] = { "type": "CoordinateSystem", "subtype": "ellipsoidal", "axis": [ @@ -195,14 +195,14 @@ def test_ellipsoidal_coordinate_system(self) -> None: }, ], } - cs: CoordinateSystem = CoordinateSystem(**cs_data) + cs: CoordinateSystem = CoordinateSystem.model_validate(cs_data) assert cs.subtype == "ellipsoidal" assert len(cs.axis) == 2 assert cs.axis[0].name == "Geodetic latitude" def test_cartesian_coordinate_system(self) -> None: """Test Cartesian coordinate system""" - cs_data: dict[str, Any] = { + cs_data: dict[str, object] = { "type": "CoordinateSystem", "subtype": "Cartesian", "axis": [ @@ -222,7 +222,7 @@ def test_cartesian_coordinate_system(self) -> None: }, ], } - cs: CoordinateSystem = CoordinateSystem(**cs_data) + cs: CoordinateSystem = CoordinateSystem.model_validate(cs_data) assert cs.subtype == "Cartesian" assert cs.axis[0].direction == "east" assert cs.axis[1].direction == "north" @@ -233,7 +233,7 @@ class TestCRSModels: def test_geodetic_crs_wgs84(self) -> None: """Test WGS 84 geodetic CRS""" - wgs84_data: dict[str, Any] = { + wgs84_data: dict[str, object] = { "type": "GeographicCRS", "name": "WGS 84", "datum": { @@ -276,14 +276,16 @@ def test_geodetic_crs_wgs84(self) -> None: }, "id": {"authority": "EPSG", "code": 4326}, } - crs: GeodeticCRS = GeodeticCRS(**wgs84_data) + crs: GeodeticCRS = GeodeticCRS.model_validate(wgs84_data) assert crs.name == "WGS 84" + assert crs.datum is not None assert crs.datum.name == "World Geodetic System 1984" + assert crs.id is not None assert crs.id.code == 4326 def test_projected_crs_utm(self) -> None: """Test UTM projected CRS""" - utm_data: dict[str, Any] = { + utm_data: dict[str, object] = { "type": "ProjectedCRS", "name": "WGS 84 / UTM zone 33N", "base_crs": { @@ -356,14 +358,14 @@ def test_projected_crs_utm(self) -> None: ], }, } - crs: ProjectedCRS = ProjectedCRS(**utm_data) + crs: ProjectedCRS = ProjectedCRS.model_validate(utm_data) assert crs.name == "WGS 84 / UTM zone 33N" assert crs.base_crs.name == "WGS 84" assert crs.conversion.name == "UTM zone 33N" def test_compound_crs(self) -> None: """Test compound CRS with horizontal and vertical components""" - compound_data: dict[str, Any] = { + compound_data: dict[str, object] = { "type": "CompoundCRS", "name": "WGS 84 + EGM96 height", "components": [ @@ -388,11 +390,15 @@ def test_compound_crs(self) -> None: }, ], } - crs: CompoundCRS = CompoundCRS(**compound_data) + crs: CompoundCRS = CompoundCRS.model_validate(compound_data) assert crs.name == "WGS 84 + EGM96 height" assert len(crs.components) == 2 - assert crs.components[0].name == "WGS 84" - assert crs.components[1].name == "EGM96 height" + component_0 = crs.components[0] + component_1 = crs.components[1] + assert isinstance(component_0, GeodeticCRS) + assert isinstance(component_1, VerticalCRS) + assert component_0.name == "WGS 84" + assert component_1.name == "EGM96 height" class TestDatumEnsemble: @@ -400,7 +406,7 @@ class TestDatumEnsemble: def test_datum_ensemble_creation(self) -> None: """Test creation of datum ensemble""" - ensemble_data: dict[str, Any] = { + ensemble_data: dict[str, object] = { "type": "DatumEnsemble", "name": "World Geodetic System 1984 ensemble", "members": [ @@ -416,7 +422,7 @@ def test_datum_ensemble_creation(self) -> None: }, "accuracy": "2.0", } - ensemble: DatumEnsemble = DatumEnsemble(**ensemble_data) + ensemble: DatumEnsemble = DatumEnsemble.model_validate(ensemble_data) assert ensemble.name == "World Geodetic System 1984 ensemble" assert len(ensemble.members) == 3 assert ensemble.accuracy == "2.0" @@ -427,7 +433,7 @@ class TestOperations: def test_coordinate_metadata(self) -> None: """Test coordinate metadata""" - metadata_data: dict[str, Any] = { + metadata_data: dict[str, object] = { "type": "CoordinateMetadata", "crs": { "type": "GeographicCRS", @@ -445,13 +451,14 @@ def test_coordinate_metadata(self) -> None: }, "coordinateEpoch": 2020.0, } - metadata: CoordinateMetadata = CoordinateMetadata(**metadata_data) + metadata: CoordinateMetadata = CoordinateMetadata.model_validate(metadata_data) assert metadata.coordinateEpoch == 2020.0 + assert isinstance(metadata.crs, GeodeticCRS) assert metadata.crs.name == "WGS 84" def test_single_operation(self) -> None: """Test single operation (transformation)""" - operation_data: dict[str, Any] = { + operation_data: dict[str, object] = { "type": "Transformation", "name": "NAD27 to NAD83 (1)", "method": {"type": "OperationMethod", "name": "NADCON"}, @@ -469,9 +476,10 @@ def test_single_operation(self) -> None: ], "accuracy": "0.15", } - operation: SingleOperation = SingleOperation(**operation_data) + operation: SingleOperation = SingleOperation.model_validate(operation_data) assert operation.name == "NAD27 to NAD83 (1)" assert operation.accuracy == "0.15" + assert operation.parameters is not None assert len(operation.parameters) == 2 @@ -480,27 +488,28 @@ class TestValidationEdgeCases: def test_invalid_crs_type(self) -> None: """Test invalid CRS type raises ValidationError""" - invalid_data: dict[str, Any] = { + invalid_data: dict[str, object] = { "type": "InvalidCRS", # Invalid type "name": "Invalid CRS", } with pytest.raises(ValidationError): - GeodeticCRS(**invalid_data) + GeodeticCRS(**invalid_data) # type: ignore[arg-type] # building model from dict[str, object] (intentional) def test_missing_required_fields(self) -> None: """Test missing required fields raise ValidationError""" # Missing name for CRS with pytest.raises(ValidationError): - GeodeticCRS(type="GeographicCRS") + GeodeticCRS(type="GeographicCRS") # type: ignore[call-arg] # missing name (intentional) # Missing ellipsoid for geodetic reference frame with pytest.raises(ValidationError): - GeodeticReferenceFrame(type="GeodeticReferenceFrame", name="Test Datum") + # missing ellipsoid (intentional) + GeodeticReferenceFrame(type="GeodeticReferenceFrame", name="Test Datum") # type: ignore[call-arg] def test_mutually_exclusive_fields(self) -> None: """Test that mutually exclusive fields are properly validated""" # Cannot have both id and ids - invalid_data: dict[str, Any] = { + invalid_data: dict[str, object] = { "type": "Unit", "name": "metre", "conversion_factor": 1.0, @@ -511,27 +520,27 @@ def test_mutually_exclusive_fields(self) -> None: # For now, we'll just ensure the model can be created with either field with pytest.raises(ValidationError): - Unit(**invalid_data) + Unit(**invalid_data) # type: ignore[arg-type] # building model from dict[str, object] (intentional) # Valid with id only - valid_with_id: dict[str, Any] = { + valid_with_id: dict[str, object] = { "type": "Unit", "name": "metre", "conversion_factor": 1.0, "id": {"authority": "EPSG", "code": 9001}, } - unit: Unit = Unit(**valid_with_id) + unit: Unit = Unit.model_validate(valid_with_id) assert unit.id is not None assert unit.ids is None # Valid with ids only - valid_with_ids: dict[str, Any] = { + valid_with_ids: dict[str, object] = { "type": "Unit", "name": "metre", "conversion_factor": 1.0, "ids": [{"authority": "EPSG", "code": 9001}], } - unit = Unit(**valid_with_ids) + unit = Unit.model_validate(valid_with_ids) assert unit.ids is not None assert unit.id is None @@ -542,7 +551,7 @@ class TestSerializationDeserialization: def test_round_trip_serialization(self) -> None: """Test that models can be serialized to JSON and back""" # Create a simple CRS - crs_data: dict[str, Any] = { + crs_data: dict[str, object] = { "type": "GeographicCRS", "name": "WGS 84", "datum": { @@ -558,14 +567,16 @@ def test_round_trip_serialization(self) -> None: } # Create model instance - original_crs: GeodeticCRS = GeodeticCRS(**crs_data) + original_crs: GeodeticCRS = GeodeticCRS.model_validate(crs_data) # Deserialize back to model - json_data: dict[str, Any] = original_crs.model_dump() - reconstructed_crs: GeodeticCRS = GeodeticCRS(**json_data) + json_data: dict[str, object] = original_crs.model_dump() + reconstructed_crs: GeodeticCRS = GeodeticCRS.model_validate(json_data) # Verify they're equivalent assert reconstructed_crs.name == original_crs.name + assert reconstructed_crs.datum is not None + assert original_crs.datum is not None assert reconstructed_crs.datum.name == original_crs.datum.name assert reconstructed_crs.datum.ellipsoid.name == original_crs.datum.ellipsoid.name @@ -574,17 +585,17 @@ def test_projjson_union_type(self) -> None: # Test with different types that should all be valid ProjJSON # Ellipsoid - ellipsoid_data: dict[str, Any] = { + ellipsoid_data: dict[str, object] = { "type": "Ellipsoid", "name": "WGS 84", "semi_major_axis": 6378137.0, "inverse_flattening": 298.257223563, } - ellipsoid: Ellipsoid = Ellipsoid(**ellipsoid_data) + ellipsoid: Ellipsoid = Ellipsoid.model_validate(ellipsoid_data) assert ellipsoid.name == "WGS 84" # CRS - crs_data: dict[str, Any] = { + crs_data: dict[str, object] = { "type": "GeographicCRS", "name": "WGS 84", "datum": { @@ -593,26 +604,26 @@ def test_projjson_union_type(self) -> None: "ellipsoid": ellipsoid_data, }, } - crs: GeodeticCRS = GeodeticCRS(**crs_data) + crs: GeodeticCRS = GeodeticCRS.model_validate(crs_data) assert crs.name == "WGS 84" class TestRoundTripSerialization: """Test round-trip serialization with real PROJ JSON examples.""" - def test_projected_crs_round_trip(self, projected_crs_json: dict[str, Any]) -> None: + def test_projected_crs_round_trip(self, projected_crs_json: dict[str, object]) -> None: """Test round-trip serialization of projected CRS example.""" # Parse JSON to Pydantic model from eopf_geozarr.data_api.geozarr.projjson import ProjectedCRS # Create model from JSON - original_crs: ProjectedCRS = ProjectedCRS(**projected_crs_json) + original_crs: ProjectedCRS = ProjectedCRS.model_validate(projected_crs_json) # Serialize back to dict - serialized: dict[str, Any] = original_crs.model_dump(exclude_none=True) + serialized: dict[str, object] = original_crs.model_dump(exclude_none=True) # Create model from serialized data - round_trip_crs: ProjectedCRS = ProjectedCRS(**serialized) + round_trip_crs: ProjectedCRS = ProjectedCRS.model_validate(serialized) # Verify key properties are preserved assert round_trip_crs.name == original_crs.name @@ -620,88 +631,100 @@ def test_projected_crs_round_trip(self, projected_crs_json: dict[str, Any]) -> N assert round_trip_crs.base_crs.name == original_crs.base_crs.name assert round_trip_crs.conversion.name == original_crs.conversion.name if original_crs.id: + assert round_trip_crs.id is not None assert round_trip_crs.id.authority == original_crs.id.authority assert round_trip_crs.id.code == original_crs.id.code - def test_bound_crs_round_trip(self, bound_crs_json: dict[str, Any]) -> None: + def test_bound_crs_round_trip(self, bound_crs_json: dict[str, object]) -> None: """Test round-trip serialization of bound CRS example.""" from eopf_geozarr.data_api.geozarr.projjson import BoundCRS # Create model from JSON - original_crs: BoundCRS = BoundCRS(**bound_crs_json) + original_crs: BoundCRS = BoundCRS.model_validate(bound_crs_json) # Serialize back to dict - serialized: dict[str, Any] = original_crs.model_dump(exclude_none=True) + serialized: dict[str, object] = original_crs.model_dump(exclude_none=True) # Create model from serialized data - round_trip_crs: BoundCRS = BoundCRS(**serialized) + round_trip_crs: BoundCRS = BoundCRS.model_validate(serialized) # Verify key properties are preserved assert round_trip_crs.type == original_crs.type + # A nested BoundCRS has no "name"; this example's source/target are named CRSs. + assert not isinstance(round_trip_crs.source_crs, BoundCRS) + assert not isinstance(round_trip_crs.target_crs, BoundCRS) + assert not isinstance(original_crs.source_crs, BoundCRS) + assert not isinstance(original_crs.target_crs, BoundCRS) assert round_trip_crs.source_crs.name == original_crs.source_crs.name assert round_trip_crs.target_crs.name == original_crs.target_crs.name assert round_trip_crs.transformation.name == original_crs.transformation.name - def test_compound_crs_round_trip(self, compound_crs_json: dict[str, Any]) -> None: + def test_compound_crs_round_trip(self, compound_crs_json: dict[str, object]) -> None: """Test round-trip serialization of compound CRS example.""" from eopf_geozarr.data_api.geozarr.projjson import CompoundCRS # Create model from JSON - original_crs: CompoundCRS = CompoundCRS(**compound_crs_json) + original_crs: CompoundCRS = CompoundCRS.model_validate(compound_crs_json) # Serialize back to dict - serialized: dict[str, Any] = original_crs.model_dump(exclude_none=True) + serialized: dict[str, object] = original_crs.model_dump(exclude_none=True) # Create model from serialized data - round_trip_crs: CompoundCRS = CompoundCRS(**serialized) + round_trip_crs: CompoundCRS = CompoundCRS.model_validate(serialized) # Verify key properties are preserved assert round_trip_crs.name == original_crs.name assert round_trip_crs.type == original_crs.type assert len(round_trip_crs.components) == len(original_crs.components) for i, component in enumerate(round_trip_crs.components): - assert component.name == original_crs.components[i].name + # Compound components are named CRSs, never a nested BoundCRS. + assert not isinstance(component, BoundCRS) + original_component = original_crs.components[i] + assert not isinstance(original_component, BoundCRS) + assert component.name == original_component.name - def test_datum_ensemble_round_trip(self, datum_ensemble_json: dict[str, Any]) -> None: + def test_datum_ensemble_round_trip(self, datum_ensemble_json: dict[str, object]) -> None: """Test round-trip serialization of datum ensemble example.""" from eopf_geozarr.data_api.geozarr.projjson import GeodeticCRS # Create model from JSON - original_crs: GeodeticCRS = GeodeticCRS(**datum_ensemble_json) + original_crs: GeodeticCRS = GeodeticCRS.model_validate(datum_ensemble_json) # Serialize back to dict - serialized: dict[str, Any] = original_crs.model_dump(exclude_none=True) + serialized: dict[str, object] = original_crs.model_dump(exclude_none=True) # Create model from serialized data - round_trip_crs: GeodeticCRS = GeodeticCRS(**serialized) + round_trip_crs: GeodeticCRS = GeodeticCRS.model_validate(serialized) # Verify key properties are preserved assert round_trip_crs.name == original_crs.name assert round_trip_crs.type == original_crs.type if original_crs.datum_ensemble: + assert round_trip_crs.datum_ensemble is not None assert round_trip_crs.datum_ensemble.name == original_crs.datum_ensemble.name assert len(round_trip_crs.datum_ensemble.members) == len( original_crs.datum_ensemble.members ) - def test_transformation_round_trip(self, transformation_json: dict[str, Any]) -> None: + def test_transformation_round_trip(self, transformation_json: dict[str, object]) -> None: """Test round-trip serialization of transformation example.""" from eopf_geozarr.data_api.geozarr.projjson import SingleOperation # Create model from JSON - original_op: SingleOperation = SingleOperation(**transformation_json) + original_op: SingleOperation = SingleOperation.model_validate(transformation_json) # Serialize back to dict - serialized: dict[str, Any] = original_op.model_dump(exclude_none=True) + serialized: dict[str, object] = original_op.model_dump(exclude_none=True) # Create model from serialized data - round_trip_op: SingleOperation = SingleOperation(**serialized) + round_trip_op: SingleOperation = SingleOperation.model_validate(serialized) # Verify key properties are preserved assert round_trip_op.name == original_op.name assert round_trip_op.type == original_op.type assert round_trip_op.method.name == original_op.method.name if original_op.parameters: + assert round_trip_op.parameters is not None assert len(round_trip_op.parameters) == len(original_op.parameters) def test_all_examples_round_trip(self, projjson_example: dict[str, object]) -> None: @@ -726,6 +749,7 @@ def test_all_examples_round_trip(self, projjson_example: dict[str, object]) -> N # Get the model class based on type obj_type = projjson_example.get("type") + assert isinstance(obj_type, str) model_class = type_mapping[obj_type] diff --git a/tests/test_data_api/test_s1.py b/tests/test_data_api/test_s1.py index 98d9917e..5e7ce77d 100644 --- a/tests/test_data_api/test_s1.py +++ b/tests/test_data_api/test_s1.py @@ -15,7 +15,7 @@ def test_sentinel1_roundtrip(s1_json_example: dict[str, object]) -> None: """Test that we can round-trip JSON data without loss""" - model1 = Sentinel1Root(**s1_json_example) + model1 = Sentinel1Root.model_validate(s1_json_example) dumped = model1.model_dump() - model2 = Sentinel1Root(**dumped) + model2 = Sentinel1Root.model_validate(dumped) assert model1.model_dump() == model2.model_dump() diff --git a/tests/test_data_api/test_s2.py b/tests/test_data_api/test_s2.py index a17fa138..ad7c45c7 100644 --- a/tests/test_data_api/test_s2.py +++ b/tests/test_data_api/test_s2.py @@ -15,7 +15,7 @@ def test_sentinel2_roundtrip(s2_json_example: dict[str, object]) -> None: """Test that we can round-trip JSON data without loss""" - model1 = Sentinel2Root(**s2_json_example) + model1 = Sentinel2Root.model_validate(s2_json_example) dumped = model1.model_dump() - model2 = Sentinel2Root(**dumped) + model2 = Sentinel2Root.model_validate(dumped) assert model1.model_dump() == model2.model_dump() diff --git a/tests/test_data_api/test_spatial.py b/tests/test_data_api/test_spatial.py index aa757644..0ee7464f 100644 --- a/tests/test_data_api/test_spatial.py +++ b/tests/test_data_api/test_spatial.py @@ -13,7 +13,8 @@ class TestSpatial: def test_minimal_required_fields(self) -> None: """Test creation with only required fields.""" - spatial = Spatial(**{"spatial:dimensions": ["y", "x"]}) + data: dict[str, object] = {"spatial:dimensions": ["y", "x"]} + spatial = Spatial.model_validate(data) assert spatial.dimensions == ["y", "x"] assert spatial.bbox is None @@ -25,13 +26,13 @@ def test_minimal_required_fields(self) -> None: def test_missing_required_dimensions(self) -> None: """Test that missing dimensions field raises ValidationError.""" with pytest.raises(ValidationError) as exc_info: - Spatial() + Spatial() # type: ignore[call-arg] # intentionally missing required field assert "spatial:dimensions" in str(exc_info.value) def test_full_spatial_metadata(self) -> None: """Test creation with all fields populated.""" - data = { + data: dict[str, object] = { "spatial:dimensions": ["y", "x"], "spatial:bbox": [500000.0, 4900000.0, 600000.0, 5000000.0], "spatial:transform_type": "affine", @@ -40,7 +41,7 @@ def test_full_spatial_metadata(self) -> None: "spatial:registration": "pixel", } - spatial = Spatial(**data) + spatial = Spatial.model_validate(data) assert spatial.dimensions == ["y", "x"] assert spatial.bbox == [500000.0, 4900000.0, 600000.0, 5000000.0] @@ -51,13 +52,13 @@ def test_full_spatial_metadata(self) -> None: def test_3d_spatial_data(self) -> None: """Test spatial model with 3D data.""" - data = { + data: dict[str, object] = { "spatial:dimensions": ["z", "y", "x"], "spatial:bbox": [500000.0, 4900000.0, 0.0, 600000.0, 5000000.0, 100.0], "spatial:shape": [10, 1000, 1000], } - spatial = Spatial(**data) + spatial = Spatial.model_validate(data) assert spatial.dimensions == ["z", "y", "x"] assert spatial.bbox == [500000.0, 4900000.0, 0.0, 600000.0, 5000000.0, 100.0] @@ -65,14 +66,14 @@ def test_3d_spatial_data(self) -> None: def test_serialization_by_alias(self) -> None: """Test that serialization uses aliases (spatial: prefixes).""" - data = { + data: dict[str, object] = { "spatial:dimensions": ["y", "x"], "spatial:bbox": [0.0, 0.0, 100.0, 100.0], "spatial:transform": [1.0, 0.0, 0.0, 0.0, -1.0, 100.0], "spatial:shape": [100, 100], } - spatial = Spatial(**data) + spatial = Spatial.model_validate(data) result = spatial.model_dump() # Should serialize with spatial: prefixes @@ -91,7 +92,8 @@ def test_serialization_by_alias(self) -> None: def test_none_fields_excluded(self) -> None: """Test that None fields are excluded from serialization.""" - spatial = Spatial(**{"spatial:dimensions": ["y", "x"]}) + data: dict[str, object] = {"spatial:dimensions": ["y", "x"]} + spatial = Spatial.model_validate(data) result = spatial.model_dump() # None fields should be excluded @@ -105,27 +107,30 @@ def test_none_fields_excluded(self) -> None: def test_node_registration(self) -> None: """Test node registration type.""" - data = {"spatial:dimensions": ["y", "x"], "spatial:registration": "node"} + data: dict[str, object] = {"spatial:dimensions": ["y", "x"], "spatial:registration": "node"} - spatial = Spatial(**data) + spatial = Spatial.model_validate(data) assert spatial.registration == "node" def test_non_affine_transform_type(self) -> None: """Test non-affine transform types.""" - data = {"spatial:dimensions": ["y", "x"], "spatial:transform_type": "rpc"} + data: dict[str, object] = { + "spatial:dimensions": ["y", "x"], + "spatial:transform_type": "rpc", + } - spatial = Spatial(**data) + spatial = Spatial.model_validate(data) assert spatial.transform_type == "rpc" def test_extra_fields_allowed(self) -> None: """Test that extra fields are allowed.""" - data = { + data: dict[str, object] = { "spatial:dimensions": ["y", "x"], "custom_field": "custom_value", "spatial:custom": "also_allowed", } - spatial = Spatial(**data) + spatial = Spatial.model_validate(data) result = spatial.model_dump() assert result["custom_field"] == "custom_value" @@ -133,7 +138,7 @@ def test_extra_fields_allowed(self) -> None: def test_roundtrip_serialization(self) -> None: """Test that serialization and deserialization preserves data.""" - original_data = { + original_data: dict[str, object] = { "spatial:dimensions": ["y", "x"], "spatial:bbox": [500000.0, 4900000.0, 600000.0, 5000000.0], "spatial:transform": [10.0, 0.0, 500000.0, 0.0, -10.0, 5000000.0], @@ -143,7 +148,7 @@ def test_roundtrip_serialization(self) -> None: } # Create model, serialize, then recreate - spatial1 = Spatial(**original_data) + spatial1 = Spatial.model_validate(original_data) serialized = spatial1.model_dump() spatial2 = Spatial(**serialized) @@ -157,23 +162,26 @@ def test_roundtrip_serialization(self) -> None: def test_invalid_dimensions_none(self) -> None: """Test that None dimensions raise ValidationError.""" + data: dict[str, object] = {"spatial:dimensions": None} # intentionally invalid value with pytest.raises(ValidationError): - Spatial(**{"spatial:dimensions": None}) + Spatial.model_validate(data) def test_empty_dimensions_not_allowed(self) -> None: """Test that empty dimensions raise ValidationError.""" + empty_data: dict[str, object] = {"spatial:dimensions": []} with pytest.raises(ValidationError) as exc_info: - Spatial(**{"spatial:dimensions": []}) + Spatial.model_validate(empty_data) assert "spatial:dimensions must contain at least one dimension" in str(exc_info.value) - data = { + data: dict[str, object] = { "spatial:dimensions": ["y", "x"], "spatial:transform_type": "affine", "spatial:transform": [10.0, 0.0, 500000.0, 0.0, -10.0], # Only 5 elements } # Currently this will pass, but in the future we might want validation - spatial = Spatial(**data) + spatial = Spatial.model_validate(data) + assert spatial.transform is not None assert len(spatial.transform) == 5 # Current behavior # Future: might want to validate for exactly 6 elements for affine diff --git a/tests/test_data_api/test_v2.py b/tests/test_data_api/test_v2.py index a3386033..78bdc161 100644 --- a/tests/test_data_api/test_v2.py +++ b/tests/test_data_api/test_v2.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any +from collections.abc import Mapping +from typing import TYPE_CHECKING, cast import numpy as np import pytest @@ -12,6 +13,9 @@ check_valid_coordinates, ) +if TYPE_CHECKING: + from eopf_geozarr.data_api.geozarr.common import GroupLike + def test_invalid_dimension_names() -> None: msg = r"The _ARRAY_DIMENSIONS attribute has length 3, which does not match the number of dimensions for this array \(got 2\)" @@ -35,10 +39,13 @@ def test_valid(data_shape: tuple[int, ...]) -> None: f"dim_{idx}": DataArray.from_array(np.arange(s), dimension_names=(f"dim_{idx}",)) for idx, s in enumerate(data_shape) } - group = GroupSpec[Any, DataArray]( + group = GroupSpec[Mapping[str, object], DataArray]( attributes={}, members={"base": base_array, **coords_arrays} ) - assert check_valid_coordinates(group) == group + # ``group`` structurally satisfies ``GroupLike``, but mypy cannot bind the + # invariant ``Mapping`` value type, mirroring the cast used in the source. + group_like = cast("GroupLike", group) + assert check_valid_coordinates(group_like) == group_like @staticmethod @pytest.mark.parametrize("data_shape", [(10,), (10, 12)]) @@ -59,9 +66,9 @@ def test_invalid_coordinates( f"dim_{idx}": DataArray.from_array(np.arange(s + 1), dimension_names=(f"dim_{idx}",)) for idx, s in enumerate(data_shape) } - group = GroupSpec[Any, DataArray]( + group = GroupSpec[Mapping[str, object], DataArray]( attributes={}, members={"base": base_array, **coords_arrays} ) msg = "Dimension .* for array 'base' has a shape mismatch:" with pytest.raises(ValueError, match=msg): - check_valid_coordinates(group) + check_valid_coordinates(cast("GroupLike", group)) diff --git a/tests/test_data_api/test_v3.py b/tests/test_data_api/test_v3.py index 09771b01..c2a16fa4 100644 --- a/tests/test_data_api/test_v3.py +++ b/tests/test_data_api/test_v3.py @@ -1,4 +1,5 @@ -from typing import Any +from collections.abc import Mapping +from typing import TYPE_CHECKING, cast import numpy as np import pytest @@ -12,6 +13,9 @@ check_valid_coordinates, ) +if TYPE_CHECKING: + from eopf_geozarr.data_api.geozarr.common import GroupLike + class TestCheckValidCoordinates: @staticmethod @@ -29,10 +33,13 @@ def test_valid(data_shape: tuple[int, ...]) -> None: f"dim_{idx}": DataArray.from_array(np.arange(s), dimension_names=(f"dim_{idx}",)) for idx, s in enumerate(data_shape) } - group = GroupSpec[Any, DataArray]( + group = GroupSpec[Mapping[str, object], DataArray]( attributes={}, members={"base": base_array, **coords_arrays} ) - assert check_valid_coordinates(group) == group + # ``group`` structurally satisfies ``GroupLike``, but mypy cannot bind the + # invariant ``Mapping`` value type, mirroring the cast used in the source. + group_like = cast("GroupLike", group) + assert check_valid_coordinates(group_like) == group_like @staticmethod @pytest.mark.parametrize("data_shape", [(10,), (10, 12)]) @@ -53,19 +60,19 @@ def test_invalid_coordinates( f"dim_{idx}": DataArray.from_array(np.arange(s + 1), dimension_names=(f"dim_{idx}",)) for idx, s in enumerate(data_shape) } - group = GroupSpec[Any, DataArray]( + group = GroupSpec[Mapping[str, object], DataArray]( attributes={}, members={"base": base_array, **coords_arrays} ) msg = "Dimension .* for array 'base' has a shape mismatch:" with pytest.raises(ValueError, match=msg): - check_valid_coordinates(group) + check_valid_coordinates(cast("GroupLike", group)) -def test_dataarray_round_trip(s2_geozarr_group_example: Any) -> None: +def test_dataarray_round_trip(s2_geozarr_group_example: zarr.Group) -> None: """ Ensure that we can round-trip dataarray attributes through the `Multiscales` model. """ - source_untyped = GroupSpec.from_zarr(s2_geozarr_group_example) + source_untyped: GroupSpec = GroupSpec.from_zarr(s2_geozarr_group_example) flat = source_untyped.to_flat() for val in flat.values(): if isinstance(val, ArraySpec) and val.dimension_names is not None: @@ -73,7 +80,7 @@ def test_dataarray_round_trip(s2_geozarr_group_example: Any) -> None: assert DataArray(**model_json).model_dump() == model_json -def test_multiscale_attrs_round_trip(s2_geozarr_group_example: Any) -> None: +def test_multiscale_attrs_round_trip(s2_geozarr_group_example: zarr.Group) -> None: """ Test that multiscale datasets round-trip through the `Multiscales` model """ diff --git a/tests/test_docs.py b/tests/test_docs.py index 82763904..b9422d38 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -10,6 +10,7 @@ #> hello """ +import uuid from io import StringIO from pathlib import Path @@ -92,7 +93,7 @@ def _validate_output(example: CodeExample, captured_output: str) -> None: # Get all examples and group them by group ID _all_examples = list(find_examples("docs")) -_examples_by_group = {} +_examples_by_group: dict[uuid.UUID | None, list[CodeExample]] = {} for ex in _all_examples: if ex.prefix == "python": if ex.group not in _examples_by_group: @@ -120,7 +121,7 @@ def test_doc_example_group(group_examples: list[CodeExample]) -> None: #> hello """ # Execute all examples in the group sequentially to share state - namespace = {} + namespace: dict[str, object] = {} any_skipped = False for example in group_examples: diff --git a/tests/test_fs_utils.py b/tests/test_fs_utils.py index 872f93a9..fec0cee7 100644 --- a/tests/test_fs_utils.py +++ b/tests/test_fs_utils.py @@ -66,7 +66,7 @@ def test_get_s3_credentials_info() -> None: @patch("eopf_geozarr.conversion.fs_utils.s3fs.S3FileSystem") -def test_validate_s3_access_success(mock_s3fs) -> None: +def test_validate_s3_access_success(mock_s3fs: Mock) -> None: """Test successful S3 access validation.""" mock_fs = Mock() mock_fs.ls.return_value = ["file1", "file2"] @@ -79,7 +79,7 @@ def test_validate_s3_access_success(mock_s3fs) -> None: @patch("eopf_geozarr.conversion.fs_utils.s3fs.S3FileSystem") -def test_validate_s3_access_failure(mock_s3fs) -> None: +def test_validate_s3_access_failure(mock_s3fs: Mock) -> None: """Test failed S3 access validation.""" mock_fs = Mock() mock_fs.ls.side_effect = Exception("Access denied") @@ -87,6 +87,7 @@ def test_validate_s3_access_failure(mock_s3fs) -> None: success, error = validate_s3_access("s3://test-bucket/path") assert success is False + assert error is not None assert "Access denied" in error @@ -101,11 +102,13 @@ def test_get_s3_storage_options() -> None: ): options = get_s3_storage_options("s3://test-bucket/path") - assert options["anon"] is False - assert options["use_ssl"] is True - assert options["client_kwargs"]["region_name"] == "us-west-2" - assert options["endpoint_url"] == "https://s3.example.com" - assert options["client_kwargs"]["endpoint_url"] == "https://s3.example.com" + assert options.get("anon") is False + assert options.get("use_ssl") is True + client_kwargs = options.get("client_kwargs") + assert client_kwargs is not None + assert client_kwargs.get("region_name") == "us-west-2" + assert options.get("endpoint_url") == "https://s3.example.com" + assert client_kwargs.get("endpoint_url") == "https://s3.example.com" def test_get_storage_options() -> None: @@ -114,9 +117,11 @@ def test_get_storage_options() -> None: with patch.dict("os.environ", {"AWS_DEFAULT_REGION": "us-west-2"}): options = get_storage_options("s3://test-bucket/path") assert options is not None - assert options["anon"] is False - assert options["use_ssl"] is True - assert options["client_kwargs"]["region_name"] == "us-west-2" + assert options.get("anon") is False + assert options.get("use_ssl") is True + client_kwargs = options.get("client_kwargs") + assert client_kwargs is not None + assert client_kwargs.get("region_name") == "us-west-2" # Test local path options = get_storage_options("/local/path") @@ -142,7 +147,7 @@ def test_normalize_path() -> None: @patch("eopf_geozarr.conversion.fs_utils.get_filesystem") -def test_path_exists(mock_get_filesystem) -> None: +def test_path_exists(mock_get_filesystem: Mock) -> None: """Test unified path existence check.""" mock_fs = Mock() mock_fs.exists.return_value = True @@ -160,7 +165,7 @@ def test_path_exists(mock_get_filesystem) -> None: @patch("eopf_geozarr.conversion.fs_utils.get_filesystem") -def test_write_json_metadata(mock_get_filesystem) -> None: +def test_write_json_metadata(mock_get_filesystem: Mock) -> None: """Test unified JSON metadata writing.""" from unittest.mock import MagicMock, mock_open @@ -183,7 +188,7 @@ def test_write_json_metadata(mock_get_filesystem) -> None: @patch("eopf_geozarr.conversion.fs_utils.get_filesystem") -def test_read_json_metadata(mock_get_filesystem) -> None: +def test_read_json_metadata(mock_get_filesystem: Mock) -> None: """Test unified JSON metadata reading.""" from unittest.mock import MagicMock, mock_open diff --git a/tests/test_integration_sentinel1.py b/tests/test_integration_sentinel1.py index 137cd53b..13f20925 100644 --- a/tests/test_integration_sentinel1.py +++ b/tests/test_integration_sentinel1.py @@ -21,7 +21,7 @@ class MockSentinel1L1GRDBuilder: """Builder class to generate a sample EOPF Sentinel-1 Level 1 GRD data product for testing purpose.""" - def __init__(self, product_id) -> None: + def __init__(self, product_id: str) -> None: self.product_title = "S01SIWGRD" self.product_id = product_id @@ -32,7 +32,7 @@ def __init__(self, product_id) -> None: self.nlines = 552 self.npixels = 1131 - def create_coordinates(self, az_dim_size, gr_dim_size) -> xr.Coordinates: + def create_coordinates(self, az_dim_size: int, gr_dim_size: int) -> xr.Coordinates: coords = { self.az_dim: pd.date_range( start="2017-05-08T16:48:30", @@ -182,7 +182,7 @@ def temp_output_dir() -> Generator[str, None, None]: shutil.rmtree(temp_dir) -def test_no_gcp_group(temp_output_dir, sample_sentinel1_datatree) -> None: +def test_no_gcp_group(temp_output_dir: str, sample_sentinel1_datatree: xr.DataTree) -> None: output_path = Path(temp_output_dir) / "temp.zarr" with pytest.raises(ValueError, match=r"Detected Sentinel-1.*GCP group not provided"): @@ -193,7 +193,9 @@ def test_no_gcp_group(temp_output_dir, sample_sentinel1_datatree) -> None: ) -def test_invalid_gcp_group_raises_error(temp_output_dir, sample_sentinel1_datatree) -> None: +def test_invalid_gcp_group_raises_error( + temp_output_dir: str, sample_sentinel1_datatree: xr.DataTree +) -> None: """Test that specifying a non-existent GCP group raises an error.""" output_path = Path(temp_output_dir) / "test_s1_invalid_gcp.zarr" groups = ["measurements"] @@ -216,7 +218,7 @@ def test_invalid_gcp_group_raises_error(temp_output_dir, sample_sentinel1_datatr ], ) def test_sentinel1_gcp_conversion( - temp_output_dir, sample_sentinel1_datatree, polarization_group + temp_output_dir: str, sample_sentinel1_datatree: xr.DataTree, polarization_group: str ) -> None: """Test conversion of Sentinel-1 data with GCPs.""" # Prepare test diff --git a/tests/test_integration_sentinel2.py b/tests/test_integration_sentinel2.py index bae02360..91b6b7bc 100644 --- a/tests/test_integration_sentinel2.py +++ b/tests/test_integration_sentinel2.py @@ -230,7 +230,7 @@ def temp_output_dir() -> Generator[str, None, None]: def test_complete_sentinel2_conversion_notebook_workflow( - sample_sentinel2_datatree, temp_output_dir + sample_sentinel2_datatree: xr.DataTree, temp_output_dir: str ) -> None: """ Test complete conversion following the notebook workflow. @@ -290,7 +290,9 @@ def test_complete_sentinel2_conversion_notebook_workflow( @pytest.mark.slow -def test_performance_characteristics(sample_sentinel2_datatree, temp_output_dir) -> None: +def test_performance_characteristics( + sample_sentinel2_datatree: xr.DataTree, temp_output_dir: str +) -> None: """ Test performance characteristics following notebook analysis. diff --git a/tests/test_reprojection_validation.py b/tests/test_reprojection_validation.py index aa5bbab1..abb944ce 100755 --- a/tests/test_reprojection_validation.py +++ b/tests/test_reprojection_validation.py @@ -16,7 +16,7 @@ class MockSentinel1L1GRDBuilder: """Builder class to generate a sample EOPF Sentinel-1 Level 1 GRD data product for testing purpose.""" - def __init__(self, product_id) -> None: + def __init__(self, product_id: str) -> None: self.product_title = "S01SIWGRD" self.product_id = product_id self.az_dim = "azimuth_time" @@ -25,7 +25,7 @@ def __init__(self, product_id) -> None: self.nlines = 552 self.npixels = 1131 - def create_coordinates(self, az_dim_size, gr_dim_size) -> xr.Coordinates: + def create_coordinates(self, az_dim_size: int, gr_dim_size: int) -> xr.Coordinates: coords = { self.az_dim: pd.date_range( start="2017-05-08T16:48:30", diff --git a/tests/test_s2_converter_simplified.py b/tests/test_s2_converter_simplified.py index 69cff728..0bbf746b 100644 --- a/tests/test_s2_converter_simplified.py +++ b/tests/test_s2_converter_simplified.py @@ -254,8 +254,10 @@ def test_write_store_root_bbox_reprojects_utm_to_wgs84(tmp_path: Path) -> None: root_attrs = dict(zarr.open_group(store_path, mode="r").attrs) bbox = root_attrs["spatial:bbox"] + assert isinstance(bbox, list) + assert all(isinstance(coord, float) for coord in bbox) assert len(bbox) == 4 - xmin, ymin, xmax, ymax = bbox + xmin, ymin, xmax, ymax = (coord for coord in bbox if isinstance(coord, float)) # Roughly 7.0-8.0E, 44.1-45.1N after reprojection assert 6.0 < xmin < 8.0, bbox assert 43.0 < ymin < 45.0, bbox diff --git a/tests/test_s2_data_consolidator.py b/tests/test_s2_data_consolidator.py index 6102a25d..be82a85e 100644 --- a/tests/test_s2_data_consolidator.py +++ b/tests/test_s2_data_consolidator.py @@ -187,7 +187,7 @@ def sample_s2_datatree(self) -> MagicMock: } # Mock the dataset access - def mock_getitem(self, path: str) -> MagicMock: + def mock_getitem(self: object, path: str) -> MagicMock: mock_node = MagicMock() if "r10m" in path: if "reflectance" in path: @@ -220,7 +220,7 @@ def mock_getitem(self, path: str) -> MagicMock: mock_dt.__getitem__ = mock_getitem return mock_dt - def test_init(self, sample_s2_datatree) -> None: + def test_init(self, sample_s2_datatree: MagicMock) -> None: """Test consolidator initialization.""" consolidator = S2DataConsolidator(sample_s2_datatree) @@ -229,7 +229,7 @@ def test_init(self, sample_s2_datatree) -> None: assert consolidator.geometry_data == {} assert consolidator.meteorology_data == {} - def test_consolidate_all_data(self, sample_s2_datatree) -> None: + def test_consolidate_all_data(self, sample_s2_datatree: MagicMock) -> None: """Test complete data consolidation.""" consolidator = S2DataConsolidator(sample_s2_datatree) measurements, geometry, meteorology = consolidator.consolidate_all_data() @@ -253,7 +253,7 @@ def test_consolidate_all_data(self, sample_s2_datatree) -> None: assert "atmosphere" in measurements[resolution] assert "probability" in measurements[resolution] - def test_extract_reflectance_bands(self, sample_s2_datatree) -> None: + def test_extract_reflectance_bands(self, sample_s2_datatree: MagicMock) -> None: """Test reflectance band extraction.""" consolidator = S2DataConsolidator(sample_s2_datatree) consolidator._extract_measurements_data() @@ -274,7 +274,7 @@ def test_extract_reflectance_bands(self, sample_s2_datatree) -> None: assert "b01" in consolidator.measurements_data[60]["bands"] assert "b09" in consolidator.measurements_data[60]["bands"] - def test_extract_quality_data(self, sample_s2_datatree) -> None: + def test_extract_quality_data(self, sample_s2_datatree: MagicMock) -> None: """Test quality data extraction.""" consolidator = S2DataConsolidator(sample_s2_datatree) consolidator._extract_measurements_data() @@ -283,7 +283,7 @@ def test_extract_quality_data(self, sample_s2_datatree) -> None: assert "quality_b02" in consolidator.measurements_data[10]["quality"] assert "quality_b03" in consolidator.measurements_data[10]["quality"] - def test_extract_detector_footprints(self, sample_s2_datatree) -> None: + def test_extract_detector_footprints(self, sample_s2_datatree: MagicMock) -> None: """Test detector footprint extraction.""" consolidator = S2DataConsolidator(sample_s2_datatree) consolidator._extract_measurements_data() @@ -292,7 +292,7 @@ def test_extract_detector_footprints(self, sample_s2_datatree) -> None: assert "detector_footprint_b02" in consolidator.measurements_data[10]["detector_footprints"] assert "detector_footprint_b03" in consolidator.measurements_data[10]["detector_footprints"] - def test_extract_atmosphere_data(self, sample_s2_datatree) -> None: + def test_extract_atmosphere_data(self, sample_s2_datatree: MagicMock) -> None: """Test atmosphere data extraction.""" consolidator = S2DataConsolidator(sample_s2_datatree) consolidator._extract_measurements_data() @@ -301,7 +301,7 @@ def test_extract_atmosphere_data(self, sample_s2_datatree) -> None: assert "aot" in consolidator.measurements_data[20]["atmosphere"] assert "wvp" in consolidator.measurements_data[20]["atmosphere"] - def test_extract_classification_data(self, sample_s2_datatree) -> None: + def test_extract_classification_data(self, sample_s2_datatree: MagicMock) -> None: """Test classification data extraction.""" consolidator = S2DataConsolidator(sample_s2_datatree) consolidator._extract_measurements_data() @@ -309,7 +309,7 @@ def test_extract_classification_data(self, sample_s2_datatree) -> None: # Classification should be at 20m resolution assert "scl" in consolidator.measurements_data[20]["classification"] - def test_extract_probability_data(self, sample_s2_datatree) -> None: + def test_extract_probability_data(self, sample_s2_datatree: MagicMock) -> None: """Test probability data extraction.""" consolidator = S2DataConsolidator(sample_s2_datatree) consolidator._extract_measurements_data() @@ -318,7 +318,7 @@ def test_extract_probability_data(self, sample_s2_datatree) -> None: assert "cld" in consolidator.measurements_data[20]["probability"] assert "snw" in consolidator.measurements_data[20]["probability"] - def test_extract_geometry_data(self, sample_s2_datatree) -> None: + def test_extract_geometry_data(self, sample_s2_datatree: MagicMock) -> None: """Test geometry data extraction.""" consolidator = S2DataConsolidator(sample_s2_datatree) consolidator._extract_geometry_data() @@ -329,7 +329,7 @@ def test_extract_geometry_data(self, sample_s2_datatree) -> None: assert "view_zenith_angle" in consolidator.geometry_data assert "view_azimuth_angle" in consolidator.geometry_data - def test_extract_meteorology_data(self, sample_s2_datatree) -> None: + def test_extract_meteorology_data(self, sample_s2_datatree: MagicMock) -> None: """Test meteorology data extraction.""" consolidator = S2DataConsolidator(sample_s2_datatree) consolidator._extract_meteorology_data() @@ -412,7 +412,9 @@ def sample_data_dict(self) -> dict[str, dict[str, xr.DataArray]]: }, } - def test_create_consolidated_dataset_success(self, sample_data_dict) -> None: + def test_create_consolidated_dataset_success( + self, sample_data_dict: dict[str, dict[str, xr.DataArray]] + ) -> None: """Test successful dataset creation.""" ds = create_consolidated_dataset(sample_data_dict, resolution=10) @@ -434,14 +436,20 @@ def test_create_consolidated_dataset_success(self, sample_data_dict) -> None: def test_create_consolidated_dataset_empty_data(self) -> None: """Test dataset creation with empty data.""" - empty_data_dict = {"bands": {}, "quality": {}, "atmosphere": {}} + empty_data_dict: dict[str, dict[str, xr.DataArray]] = { + "bands": {}, + "quality": {}, + "atmosphere": {}, + } ds = create_consolidated_dataset(empty_data_dict, resolution=20) # Should return empty dataset assert isinstance(ds, xr.Dataset) assert len(ds.data_vars) == 0 - def test_create_consolidated_dataset_with_crs(self, sample_data_dict) -> None: + def test_create_consolidated_dataset_with_crs( + self, sample_data_dict: dict[str, dict[str, xr.DataArray]] + ) -> None: """Test dataset creation with CRS information.""" # Add CRS to one of the data arrays sample_data_dict["bands"]["b02"] = sample_data_dict["bands"]["b02"].rio.write_crs( @@ -513,7 +521,7 @@ def complete_s2_datatree(self) -> MagicMock: coords={"time": time, "x": x_20m, "y": y_20m}, ) - def mock_getitem(self, path: str) -> MagicMock: + def mock_getitem(self: object, path: str) -> MagicMock: mock_node = MagicMock() if "/measurements/reflectance/r10m" in path: mock_node.to_dataset.return_value = reflectance_10m @@ -526,7 +534,7 @@ def mock_getitem(self, path: str) -> MagicMock: mock_dt.__getitem__ = mock_getitem return mock_dt - def test_end_to_end_consolidation(self, complete_s2_datatree) -> None: + def test_end_to_end_consolidation(self, complete_s2_datatree: MagicMock) -> None: """Test complete end-to-end consolidation and dataset creation.""" # Step 1: Consolidate data consolidator = S2DataConsolidator(complete_s2_datatree) diff --git a/tests/test_s2_multiscale.py b/tests/test_s2_multiscale.py index a0ad955f..29c57117 100644 --- a/tests/test_s2_multiscale.py +++ b/tests/test_s2_multiscale.py @@ -4,6 +4,7 @@ import json import pathlib +from collections.abc import Mapping, Sequence from itertools import pairwise from pathlib import Path from unittest.mock import patch @@ -17,6 +18,7 @@ from structlog.testing import capture_logs from zarr.codecs import BloscCodec, CastValue, ScaleOffset from zarr.core.dtype import Int16 +from zarr.core.metadata import ArrayV3Metadata from eopf_geozarr.s2_optimization.s2_multiscale import ( _coarsen_variable, @@ -70,7 +72,9 @@ def _dataset(resolution: int, size: int, x0: float, y0: float) -> xr.Dataset: {"band": (["y", "x"], np.ones((size, size), dtype=np.uint16))}, coords={"x": x, "y": y}, ) - return ds.rio.write_crs("EPSG:32631") + crs_ds = ds.rio.write_crs("EPSG:32631") + assert isinstance(crs_ds, xr.Dataset) + return crs_ds r10m = _dataset(10, 12, 600000.0, 4900020.0) r120m = _dataset(120, 3, 600030.0, 4899990.0) @@ -86,9 +90,17 @@ def stale_transform() -> tuple[float, float, float, float, float, float]: {"r10m": r10m, "r120m": r120m}, ) - layout = parent_group.attrs["multiscales"]["layout"] - derived_level = next(level for level in layout if level["asset"] == "r120m") - assert tuple(derived_level["spatial:transform"]) == ( + multiscales = parent_group.attrs["multiscales"] + assert isinstance(multiscales, Mapping) + layout = multiscales["layout"] + assert isinstance(layout, Sequence) + derived_level = next( + level for level in layout if isinstance(level, Mapping) and level["asset"] == "r120m" + ) + assert isinstance(derived_level, Mapping) + transform = derived_level["spatial:transform"] + assert isinstance(transform, Sequence) + assert tuple(transform) == ( 120.0, 0.0, 600030.0, @@ -101,8 +113,8 @@ def stale_transform() -> tuple[float, float, float, float, float, float]: def test_calculate_simple_shard_dimensions() -> None: """Test simplified shard dimensions calculation.""" # Test 3D data (time, y, x) - shards are multiples of chunks - data_shape = (5, 1024, 1024) - chunks = (1, 256, 256) + data_shape: tuple[int, ...] = (5, 1024, 1024) + chunks: tuple[int, ...] = (1, 256, 256) shard_dims = calculate_simple_shard_dimensions(data_shape, chunks) @@ -181,8 +193,8 @@ def test_create_measurements_encoding(keep_scale_offset: bool, sample_dataset: x # Check that encoding is created for all variables for var_name in sample_dataset.data_vars: - assert var_name in encoding - var_encoding = encoding[var_name] + assert str(var_name) in encoding + var_encoding = encoding[str(var_name)] # Check basic encoding structure assert "chunks" in var_encoding @@ -194,11 +206,11 @@ def test_create_measurements_encoding(keep_scale_offset: bool, sample_dataset: x # Check coordinate encoding for coord_name in sample_dataset.coords: - if coord_name in encoding: + if str(coord_name) in encoding: # Coordinates may have either compressor or compressors set to None assert ( - encoding[coord_name].get("compressor") is None - or encoding[coord_name].get("compressors") is None + encoding[str(coord_name)].get("compressor") is None + or encoding[str(coord_name)].get("compressors") is None ) # Store data and check that we are conditionally applying the scale-offset transformation # based on the request passed to the encoding @@ -220,7 +232,8 @@ def test_create_measurements_encoding_time_chunking(sample_dataset: xr.Dataset) for var_name in sample_dataset.data_vars: if sample_dataset[var_name].ndim == 3: # 3D variable with time - chunks = encoding[var_name]["chunks"] + chunks = encoding[str(var_name)].get("chunks") + assert chunks is not None assert chunks[0] == 1 # Time dimension should be chunked to 1 @@ -240,7 +253,7 @@ def test_calculate_aligned_chunk_size() -> None: @pytest.mark.filterwarnings("ignore:.*:FutureWarning") @pytest.mark.filterwarnings("ignore:.*:UserWarning") def test_create_multiscale_from_datatree( - s2_group_example: zarr.Group, + s2_group_example: pathlib.Path, tmp_path: pathlib.Path, ) -> None: """Snapshot test: a single canonical parametrization (keep_scale_offset=False, @@ -253,7 +266,13 @@ def test_create_multiscale_from_datatree( output_path = str(tmp_path / "output.zarr") input_group = zarr.open_group(s2_group_example) output_group = zarr.create_group(output_path) - dt_input = xr.open_datatree(input_group.store, engine="zarr", chunks="auto") + # xarray's open_datatree accepts a zarr store at runtime, but its stub does + # not list Store among the accepted input types. + dt_input = xr.open_datatree( + input_group.store, # pyright: ignore[reportArgumentType] + engine="zarr", + chunks="auto", + ) # Capture log output using structlog's testing context manager with capture_logs(): @@ -291,7 +310,9 @@ def test_create_multiscale_from_datatree( # check that all multiscale levels have the same data type # this check is redundant with the later check, but it's expedient to check this here. # eventually this check should be spun out into its own test - _, res_groups = zip(*observed_group["measurements/reflectance"].groups(), strict=False) + reflectance_group = observed_group["measurements/reflectance"] + assert isinstance(reflectance_group, zarr.Group) + _, res_groups = zip(*reflectance_group.groups(), strict=False) dtype_mismatch: set[object] = set() for group_a, group_b in pairwise(res_groups): @@ -421,6 +442,7 @@ def test_create_multiscale_from_datatree_behavior( # ------------------------------------------------------------------ for group_path, var_name in _ORIGINAL_GROUPS.items(): arr = zarr.open_array(output_path, path=f"{group_path}/{var_name}") + assert isinstance(arr.metadata, ArrayV3Metadata) codec_names = [type(c).__name__ for c in arr.metadata.codecs] if keep_scale_offset: @@ -461,6 +483,7 @@ def test_create_multiscale_from_datatree_behavior( assert ds.data_vars, f"{group_path} has no variables" for name in ds.data_vars: arr = zarr.open_array(output_path, path=f"{group_path}/{name}") + assert isinstance(arr.metadata, ArrayV3Metadata) codec_names = [type(c).__name__ for c in arr.metadata.codecs] if keep_scale_offset: diff --git a/tests/test_scale_offset.py b/tests/test_scale_offset.py index 18826e0f..913c1c84 100644 --- a/tests/test_scale_offset.py +++ b/tests/test_scale_offset.py @@ -37,4 +37,4 @@ def test_scale_offset_from_cf_round_trip() -> None: ) arr[:] = unpacked_values - np.testing.assert_array_almost_equal(arr[:], unpacked_values) + np.testing.assert_array_almost_equal(np.asarray(arr[:]), unpacked_values) diff --git a/tests/test_titiler_integration.py b/tests/test_titiler_integration.py index 3d761c76..925e019e 100644 --- a/tests/test_titiler_integration.py +++ b/tests/test_titiler_integration.py @@ -9,6 +9,10 @@ - titiler-xarray, httpx installed """ +# titiler/fastapi/starlette are optional (the `downstream-titiler` group); this +# module is skipped at runtime when they're absent, so don't flag their imports. +# pyright: reportMissingImports=false + from __future__ import annotations import pathlib @@ -89,7 +93,7 @@ def _open_group(path: pathlib.Path, group: str) -> xr.Dataset: def _band_vars(ds: xr.Dataset) -> list[str]: """Get band variable names, excluding spatial_ref.""" - return [v for v in ds.data_vars if v != "spatial_ref"] + return [str(v) for v in ds.data_vars if v != "spatial_ref"] class TestTitilerInfo: diff --git a/uv.lock b/uv.lock index 09d98ad5..88d9e183 100644 --- a/uv.lock +++ b/uv.lock @@ -214,46 +214,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] -[[package]] -name = "ast-serialize" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, - { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, - { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, - { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, - { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, - { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, - { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, - { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, - { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, - { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, - { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, - { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, - { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, - { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, - { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, - { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, - { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, - { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, - { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, - { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, - { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, - { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, - { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, - { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, - { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, - { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, -] - [[package]] name = "attrs" version = "26.1.0" @@ -854,8 +814,8 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "bandit" }, - { name = "mypy" }, { name = "pre-commit" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, ] @@ -894,14 +854,14 @@ requires-dist = [ { name = "typing-extensions", specifier = ">=4.15.0" }, { name = "xarray", specifier = ">=2025.7.1" }, { name = "zarr", extras = ["cast-value-rs"], specifier = ">=3.2.0" }, - { name = "zarr-cm", specifier = ">=0.3.0" }, + { name = "zarr-cm", git = "https://github.com/zarr-conventions/zarr-cm.git?rev=main" }, ] [package.metadata.requires-dev] dev = [ { name = "bandit", extras = ["toml"], specifier = ">=1.7.0" }, - { name = "mypy", specifier = ">=1.0.0" }, { name = "pre-commit", specifier = ">=3.0.0" }, + { name = "pyright", specifier = ">=1.1.390" }, { name = "pytest", specifier = ">=7.0.0" }, { name = "pytest-cov", specifier = ">=4.0.0" }, ] @@ -1201,66 +1161,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/94/a8066f84d62ab666d61ef97deba1a33126e3e5c0c0da2c458ada17053ed6/jsondiff-2.2.1-py3-none-any.whl", hash = "sha256:b1f0f7e2421881848b1d556d541ac01a91680cfcc14f51a9b62cdf4da0e56722", size = 13440, upload-time = "2024-08-29T04:09:04.955Z" }, ] -[[package]] -name = "librt" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, - { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, - { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, - { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, - { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, - { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, - { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, - { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, - { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, - { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, - { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, - { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, - { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, - { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, - { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, - { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, - { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, - { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, - { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, - { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, - { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, - { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, - { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, - { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, - { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, - { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, - { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, - { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, - { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, -] - [[package]] name = "locket" version = "1.0.0" @@ -1616,50 +1516,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] -[[package]] -name = "mypy" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ast-serialize" }, - { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, - { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, - { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, - { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, - { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, - { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, - { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, - { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, - { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, - { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, - { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, - { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, - { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, - { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, - { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, -] - [[package]] name = "mypy-extensions" version = "1.1.0" @@ -2310,6 +2166,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" }, ] +[[package]] +name = "pyright" +version = "1.1.410" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/53/e4d8ea1391bd4355231be6f91bf239479aa0014260ed3fb5526eeb12a1f2/pyright-1.1.410.tar.gz", hash = "sha256:07a073b8ba6749826773c1269773efa11b93440d9a6aa60419d9a3172d6dc488", size = 4062013, upload-time = "2026-06-01T17:35:48.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/33/288b5868fa00846dacf249633719d747893e54aebd196b9968ac1878a5d3/pyright-1.1.410-py3-none-any.whl", hash = "sha256:5e961bed37cacf96b3f7cd7b1da39b350a9239aa2e69138d0e88f728cfaf296c", size = 6082448, upload-time = "2026-06-01T17:35:46.387Z" }, +] + [[package]] name = "pystac" version = "1.14.3" @@ -3157,11 +3026,10 @@ cast-value-rs = [ [[package]] name = "zarr-cm" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/b6/d920b941f5862dca32b1689316e15824e12393fa9b25598a58ba1823dc55/zarr_cm-0.3.0.tar.gz", hash = "sha256:6fbee90a196539d61ecc51fe8fdf7fee1f70d5733289fb03df639be229465c79", size = 43995, upload-time = "2026-06-17T13:06:29.448Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/1e/d308d5ee6cd728013e534de3522e726ea5a9ba5843e5a149a80b1e40c75f/zarr_cm-0.3.0-py3-none-any.whl", hash = "sha256:489b34666823d1ff0dfb9fe975f6cccbe94660a39fe0b4688970b2baf39b43fc", size = 30752, upload-time = "2026-06-17T13:06:28.202Z" }, +version = "0.4.1.dev13+ge94a3ee8d" +source = { git = "https://github.com/zarr-conventions/zarr-cm.git?rev=main#e94a3ee8db52689883753f5b928f8331c3d01b15" } +dependencies = [ + { name = "typing-extensions" }, ] [[package]] From d75189967c4784746063cc3250cdcc80ccee443a Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Sun, 21 Jun 2026 21:14:05 +0200 Subject: [PATCH 8/9] refactor: replace unverified casts with runtime checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three casts asserted a type derived from Any or a union without verifying it. Replace each with an isinstance guard that raises TypeError on violation, so the assumption is enforced at runtime instead of only asserted to the checker: - sentinel1_reprojection: rio.write_crs() returns Any -> verify xr.Dataset. - s2_multiscale: output_group[base_path] is Array | Group -> verify zarr.Group. - s2_multiscale: client.compute() returns Any -> verify distributed.Future. The remaining casts bridge types on data we already validated (model_dump / create_many output), satisfy protocol/TypeVar binding on `self`, or widen a TypedDict for a third-party API — a runtime check there adds no safety. Co-Authored-By: Claude Opus 4.8 --- .../conversion/sentinel1_reprojection.py | 11 ++++++++--- .../s2_optimization/s2_multiscale.py | 19 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/eopf_geozarr/conversion/sentinel1_reprojection.py b/src/eopf_geozarr/conversion/sentinel1_reprojection.py index e6c0f69b..7681e931 100644 --- a/src/eopf_geozarr/conversion/sentinel1_reprojection.py +++ b/src/eopf_geozarr/conversion/sentinel1_reprojection.py @@ -5,7 +5,7 @@ to geographic coordinates (lat/lon) using Ground Control Points (GCPs). """ -from typing import Any, cast +from typing import Any import numpy as np import rasterio @@ -107,11 +107,16 @@ def reproject_sentinel1_with_gcps( data_vars=reprojected_data_vars, coords=target_coords, attrs=ds.attrs.copy() ) - # Set CRS information + # Set CRS information. `rio.write_crs` is untyped (returns Any), so verify + # the result is a Dataset rather than asserting it with a cast. reprojected_ds = reprojected_ds.rio.write_crs(target_crs) + if not isinstance(reprojected_ds, xr.Dataset): + raise TypeError( + f"expected an xarray.Dataset after write_crs, got {type(reprojected_ds).__name__}" + ) log.info("✅ Successfully reprojected Sentinel-1 data", target_crs=target_crs) - return cast("xr.Dataset", reprojected_ds) + return reprojected_ds def _create_gcps_from_dataset( diff --git a/src/eopf_geozarr/s2_optimization/s2_multiscale.py b/src/eopf_geozarr/s2_optimization/s2_multiscale.py index 46e59aa1..35a06520 100644 --- a/src/eopf_geozarr/s2_optimization/s2_multiscale.py +++ b/src/eopf_geozarr/s2_optimization/s2_multiscale.py @@ -11,6 +11,7 @@ import numpy as np import structlog import xarray as xr +import zarr from dask.array import from_delayed from dask.delayed import delayed from pydantic.experimental.missing_sentinel import MISSING @@ -36,7 +37,6 @@ if TYPE_CHECKING: from collections.abc import Hashable, Mapping - import zarr from zarr_cm import MultiscalesAttrs from zarr_cm import spatial as spatial_cm @@ -412,9 +412,13 @@ def create_multiscale_from_datatree( log.info("Adding multiscales metadata to parent groups") # Get the parent group (it was created when writing the resolution groups). - # `base_path` always addresses a group (the reflectance parent), never an - # array, so narrow the `Array | Group` result to `Group` for the call below. - parent_group = cast("zarr.Group", output_group[base_path]) + # `output_group[base_path]` is typed `Array | Group`; `base_path` always + # addresses a group (the reflectance parent), so verify that at runtime. + parent_group = output_group[base_path] + if not isinstance(parent_group, zarr.Group): + raise TypeError( + f"expected a zarr.Group at {base_path!r}, got {type(parent_group).__name__}" + ) add_multiscales_metadata_to_parent( parent_group, @@ -1089,8 +1093,11 @@ def stream_write_dataset( # Try to get current client for better status monitoring try: client = distributed.Client.current() - # Use client.compute to get a proper Future with status - future = cast("distributed.Future", client.compute(write_job)) + # client.compute is untyped (returns Any); verify we got a + # Future rather than asserting it with a cast. + future = client.compute(write_job) + if not isinstance(future, distributed.Future): + raise TypeError(f"expected a distributed.Future, got {type(future).__name__}") log.info("Using distributed client for write job monitoring") try: From e75b3b595ec96e369b2d051d5781849a5914ac13 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Sun, 21 Jun 2026 21:21:48 +0200 Subject: [PATCH 9/9] chore: use latest version of zarr-cm --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9e8eedad..bd22dce3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "rioxarray>=0.13.0", "cf-xarray>=0.8.0", "typing-extensions>=4.15.0", - "zarr-cm @ git+https://github.com/zarr-conventions/zarr-cm.git@main", + "zarr-cm>=0.4.1", "aiohttp>=3.14.0", "s3fs>=2024.6.0", "boto3>=1.34.0",