From 55018480e346c1d33ecd5ccebb840bc31b644953 Mon Sep 17 00:00:00 2001 From: Caio Arantes Date: Sun, 5 Jul 2026 13:58:29 -0300 Subject: [PATCH] fix: support SingleImageSatellite in the images download path POST /images failed for every SingleImageSatellite (all DEMs, soil products) with "ImageCollection.__init__() missing 1 required positional argument: 'args'": download_multiple_images_async unconditionally calls satellite.imageCollection(ee_feature), which single-image classes do not implement, so the base-class stub returns ee.ImageCollection() with no arguments. Static datasets have no time dimension, so the collection-resolution machinery does not apply. Branch before it and resolve a single getDownloadURL ZIP saved as .zip in the existing cache dir, reusing the retry/timeout policy of the collection path. The download passes scale=satellite.pixelSize explicitly: composited images (e.g. USGS3DEP1m's mosaic()) lose their source projection and would otherwise export at the 1-degree default scale. Verified against a live deployment: a ~415x445 m AOI returned a 667-byte raster without the scale argument and a 558x446 px GeoTIFF at 1 m with it. PolarisSoilTexture returned all four bands (clay/sand/silt/ usda_soil_class) at native 30 m. Also fix PolarisSoilTexture.__init__ calling super().__init__ without parentheses (a no-op attribute access). Co-Authored-By: Claude Fable 5 --- agrigee_lite/get/image.py | 67 +++++++++++++++++++++++++++++++++++++++ agrigee_lite/sat/soil.py | 2 +- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/agrigee_lite/get/image.py b/agrigee_lite/get/image.py index 576bb9e..835240b 100644 --- a/agrigee_lite/get/image.py +++ b/agrigee_lite/get/image.py @@ -250,6 +250,52 @@ async def _fetch_and_download_image( return chunk_index, False + +async def _download_single_image_zip_async( + satellite: SingleImageSatellite, + ee_feature: ee.Feature, + ee_geometry: ee.Geometry, + output_path: pathlib.Path, + force_redownload: bool, + max_retries_per_chunk: int, +) -> list[str]: + """Download a static (time-independent) dataset as a single ZIP. + + Static datasets have no time dimension, so the collection-resolution + machinery does not apply: resolve one download URL for the satellite's + single image and save it as ``.zip`` in the cache dir. + """ + image_name = satellite.shortName + file_path = output_path / f"{image_name}.zip" + + if file_path.exists(): + if not force_redownload: + return [image_name] + file_path.unlink() + + output_path.mkdir(parents=True, exist_ok=True) + img = satellite.image(ee_feature).clip(ee_geometry) + + async with aiohttp.ClientSession() as session: + async for attempt in AsyncRetrying( + stop=stop_after_attempt(max_retries_per_chunk), + wait=wait_exponential(multiplier=1, min=1, max=30), + ): + with attempt: + url = await asyncio.wait_for( + asyncio.to_thread( + img.getDownloadURL, + # Composited images (e.g. mosaic()) lose their source projection and + # default to 1-degree scale on export — always pass the native scale. + {"name": image_name, "region": ee_geometry, "scale": satellite.pixelSize}, + ), + timeout=180, + ) + await _download_url_to_path(session, url, file_path) + + return [image_name] + + async def download_multiple_images_async( geometry: Polygon | MultiPolygon, start_date: pd.Timestamp | str, @@ -301,6 +347,27 @@ async def download_multiple_images_async( geometry_wgs84 = transform_geometry(geometry, crs) ee_geometry = ee.Geometry(geometry_wgs84.__geo_interface__) ee_feature = ee.Feature(ee_geometry, {"s": start_date, "e": end_date, "0": 1}) + if isinstance(satellite, SingleImageSatellite): + output_path = _compute_images_cache_dir( + satellite=satellite, + start_date=start_date, + end_date=end_date, + centroid_x=geometry_wgs84.centroid.x, + centroid_y=geometry_wgs84.centroid.y, + invalid_images_threshold=invalid_images_threshold, + image_indices=image_indices, + max_retries_per_chunk=max_retries_per_chunk, + crs=crs, + ) + return await _download_single_image_zip_async( + satellite=satellite, + ee_feature=ee_feature, + ee_geometry=ee_geometry, + output_path=output_path, + force_redownload=force_redownload, + max_retries_per_chunk=max_retries_per_chunk, + ) + ee_expression = satellite.imageCollection(ee_feature) output_path = _compute_images_cache_dir( diff --git a/agrigee_lite/sat/soil.py b/agrigee_lite/sat/soil.py index ddd67b5..ba8c9f7 100644 --- a/agrigee_lite/sat/soil.py +++ b/agrigee_lite/sat/soil.py @@ -165,7 +165,7 @@ def __init__( f"Unknown band(s) for PolarisSoilTexture: {invalid}. Valid bands are {sorted(allowed_bands)}" ) - super().__init__ + super().__init__() self.imageNames: dict[str, str] = { "clay": "projects/sat-io/open-datasets/polaris/clay_mean/clay_0_5", "sand": "projects/sat-io/open-datasets/polaris/sand_mean/sand_0_5",