PyGeoFetch 1.0.6__tar.gz → 1.0.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygeofetch-1.0.8/CHANGELOG.md +42 -0
- pygeofetch-1.0.8/MANIFEST.in +11 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/PKG-INFO +1 -1
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/__init__.py +2 -2
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/cli/main.py +2 -2
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/core/downloader.py +120 -64
- pygeofetch-1.0.8/pygeofetch/core/logging.py +495 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/core/searcher.py +19 -8
- pygeofetch-1.0.8/pygeofetch/meta.yaml +62 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/models/satellite_data.py +50 -26
- pygeofetch-1.0.8/pygeofetch/processing/base.py +315 -0
- pygeofetch-1.0.8/pygeofetch/processing/indices.py +685 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/processing/preprocessor.py +23 -7
- pygeofetch-1.0.8/pygeofetch/processing/sar.py +336 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/aws_earth.py +3 -2
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch.egg-info/PKG-INFO +1 -1
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch.egg-info/SOURCES.txt +3 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pyproject.toml +1 -1
- pygeofetch-1.0.6/pygeofetch/core/logging.py +0 -199
- pygeofetch-1.0.6/pygeofetch/processing/base.py +0 -102
- pygeofetch-1.0.6/pygeofetch/processing/indices.py +0 -777
- pygeofetch-1.0.6/pygeofetch/processing/sar.py +0 -315
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/LICENSE +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/README.md +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/cli/__init__.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/cli/auth_commands.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/cli/config_commands.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/cli/download_commands.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/cli/index_commands.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/cli/pipeline_process_commands.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/cli/postprocess_commands.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/cli/preprocess_commands.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/cli/sar_commands.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/cli/search_commands.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/config/__init__.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/config/defaults.yaml +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/config/settings.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/core/__init__.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/core/authenticator.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/core/cache_manager.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/core/engine.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/core/orbits.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/core/scheduler.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/models/__init__.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/models/download_task.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/models/search_query.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/models/user_auth.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/processing/__init__.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/processing/batch.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/processing/pipeline.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/processing/postprocessor.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/__init__.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/airbus_oneatlas.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/base.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/copernicus.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/digitalglobe.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/earth_explorer_additional.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/element84.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/esa_scihub.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/geoserver_generic.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/google_earth_engine.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/inpe_cbers.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/isro_bhuvan.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/jaxa_earth.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/maxar_gbdx.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/nasa_earthdata.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/noaa_big_data.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/opentopography.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/planet.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/planetary_computer.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/sentinel_hub.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/terrabotics.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/providers/usgs.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/utils/__init__.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/utils/file_utils.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/utils/geo_utils.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/utils/logging_setup.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/utils/retry_handler.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch/utils/validators.py +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch.egg-info/dependency_links.txt +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch.egg-info/entry_points.txt +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch.egg-info/not-zip-safe +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch.egg-info/requires.txt +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/pygeofetch.egg-info/top_level.txt +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/setup.cfg +0 -0
- {pygeofetch-1.0.6 → pygeofetch-1.0.8}/tests/test_pgv_integration_contract.py +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to PyGeoFetch are documented here.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.8/).
|
|
5
|
+
|
|
6
|
+
## [1.0.8] — 2026-07-12
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- Federated search across 22+ satellite data providers
|
|
10
|
+
- Sentinel-1C and Sentinel-1D constellation support (active since May/April 2026)
|
|
11
|
+
- SLC product type search with automatic provider routing
|
|
12
|
+
- Precise orbit file management (POEORB/RESORB) for InSAR workflows
|
|
13
|
+
- 17 spectral indices: NDVI, EVI, SAVI, NDWI, MNDWI, NDBI, NDSI, NDMI,
|
|
14
|
+
NBR, dNBR, TCT, PCA, Texture, LST, Albedo, Band Math, Stack
|
|
15
|
+
- SAR processing: despeckle, calibrate, flood mapping, coherence
|
|
16
|
+
- 41-step chainable processing pipeline builder
|
|
17
|
+
- YAML pipeline definitions with cron scheduling
|
|
18
|
+
- Cloud Optimized GeoTIFF output
|
|
19
|
+
- Clean search result tables with ANSI-aware column alignment
|
|
20
|
+
- Live download progress with Jupyter notebook HTML widget support
|
|
21
|
+
- Credential redaction in all log output
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- AuthManager.add_credentials() now accepts dict form (BUG 1)
|
|
25
|
+
- download() length/order contract guaranteed (BUG 2)
|
|
26
|
+
- Partial downloads detected via file validation (BUG 3)
|
|
27
|
+
- CRS identity transform detection after reprojection (BUG 4)
|
|
28
|
+
- resolve_band_keys missing import in aws_earth provider
|
|
29
|
+
- Band alias conflict between Sentinel-2 and Landsat naming conventions
|
|
30
|
+
- Copernicus product_type OData filter now correctly applied
|
|
31
|
+
- USGS M2M API authentication payload (authType, catalogId fields)
|
|
32
|
+
- ZIP archive validation no longer attempts rasterio open on archives
|
|
33
|
+
|
|
34
|
+
### Providers
|
|
35
|
+
- Copernicus Dataspace (Sentinel-1/2/3/5P, SLC + GRD)
|
|
36
|
+
- AWS Earth (Sentinel-2, Landsat via STAC)
|
|
37
|
+
- Planetary Computer (Sentinel-2, Landsat, MODIS)
|
|
38
|
+
- Element84 Earth Search (STAC)
|
|
39
|
+
- USGS Earth Explorer (Landsat 1-9, ASTER, SRTM)
|
|
40
|
+
- NASA Earthdata (MODIS, ICESat-2, GEDI)
|
|
41
|
+
- Alaska SAR Facility (Sentinel-1 SLC, ALOS PALSAR)
|
|
42
|
+
- Planet Labs, Sentinel Hub, Maxar, Airbus, OpenTopography and more
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include pyproject.toml
|
|
5
|
+
recursive-include pygeofetch *.py *.yaml *.typed
|
|
6
|
+
recursive-exclude * __pycache__
|
|
7
|
+
recursive-exclude * *.pyc
|
|
8
|
+
recursive-exclude * *.pyo
|
|
9
|
+
exclude tests
|
|
10
|
+
exclude notebooks
|
|
11
|
+
exclude docs
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
PyGeoFetch v1.0.
|
|
2
|
+
PyGeoFetch v1.0.8 — Universal Satellite Data Pipeline.
|
|
3
3
|
|
|
4
4
|
Unified access to 22+ satellite data providers with a consistent
|
|
5
5
|
Python API and CLI. Supports authentication management, federated search,
|
|
@@ -30,7 +30,7 @@ CLI::
|
|
|
30
30
|
pygeofetch doctor
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
__version__ = "1.0.
|
|
33
|
+
__version__ = "1.0.8"
|
|
34
34
|
__author__ = "PyGeoFetch Contributors"
|
|
35
35
|
__license__ = "MIT"
|
|
36
36
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
PyGeoFetch CLI v1.0.
|
|
2
|
+
PyGeoFetch CLI v1.0.8 main entry point.
|
|
3
3
|
|
|
4
4
|
Full command reference:
|
|
5
5
|
pygeofetch auth — credential management
|
|
@@ -59,7 +59,7 @@ def cli(ctx: click.Context, log_level: str, log_file: str, log_format: str,
|
|
|
59
59
|
config_file: str) -> None:
|
|
60
60
|
"""
|
|
61
61
|
\b
|
|
62
|
-
PyGeoFetch v1.0.
|
|
62
|
+
PyGeoFetch v1.0.8 — Universal Satellite Data Pipeline
|
|
63
63
|
===========================================================
|
|
64
64
|
Unified access to 22+ satellite data providers.
|
|
65
65
|
|
|
@@ -30,13 +30,14 @@ from uuid import uuid4
|
|
|
30
30
|
|
|
31
31
|
from pygeofetch.models.download_task import (
|
|
32
32
|
DownloadOptions,
|
|
33
|
-
DownloadProgress,
|
|
33
|
+
DownloadProgress as _ModelDownloadProgress,
|
|
34
34
|
DownloadResult,
|
|
35
35
|
DownloadStatus,
|
|
36
36
|
DownloadTask,
|
|
37
37
|
)
|
|
38
38
|
from pygeofetch.models.satellite_data import SatelliteData
|
|
39
|
-
from pygeofetch.core.logging import get_logger, _render_progress_bar
|
|
39
|
+
from pygeofetch.core.logging import (get_logger, _render_progress_bar,
|
|
40
|
+
DownloadProgress)
|
|
40
41
|
|
|
41
42
|
logger = get_logger(__name__)
|
|
42
43
|
|
|
@@ -91,7 +92,12 @@ class AdaptiveDownloader:
|
|
|
91
92
|
destination.mkdir(parents=True, exist_ok=True)
|
|
92
93
|
|
|
93
94
|
provider = self._get_provider(data.provider)
|
|
94
|
-
|
|
95
|
+
_item_start = __import__("time").time()
|
|
96
|
+
_dl_progress = globals().get("_active_progress")
|
|
97
|
+
if _dl_progress:
|
|
98
|
+
_dl_progress.start_item(str(data.id))
|
|
99
|
+
else:
|
|
100
|
+
logger.info("Downloading %s", str(data.id)[:60])
|
|
95
101
|
|
|
96
102
|
for attempt in range(options.retry_attempts + 1):
|
|
97
103
|
try:
|
|
@@ -179,29 +185,43 @@ class AdaptiveDownloader:
|
|
|
179
185
|
completed_count = 0
|
|
180
186
|
total = len(data_list)
|
|
181
187
|
|
|
182
|
-
|
|
188
|
+
dp = DownloadProgress(total=total, destination=str(destination))
|
|
189
|
+
dp.__enter__()
|
|
190
|
+
|
|
191
|
+
def _run_one(data: SatelliteData, idx: int) -> tuple:
|
|
192
|
+
"""Download one item and return (idx, result, bytes, duration)."""
|
|
193
|
+
import time as _it
|
|
194
|
+
t0 = _it.time()
|
|
195
|
+
dp.start_item(str(data.id))
|
|
196
|
+
try:
|
|
197
|
+
result = self.download(data, destination / data.provider, options)
|
|
198
|
+
dur = _it.time() - t0
|
|
199
|
+
dp.complete_item(
|
|
200
|
+
success=result.success,
|
|
201
|
+
bytes_total=result.bytes_downloaded or 0,
|
|
202
|
+
duration=dur,
|
|
203
|
+
)
|
|
204
|
+
return idx, result
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
dur = _it.time() - t0
|
|
207
|
+
result = DownloadResult(
|
|
208
|
+
status=DownloadStatus.FAILED,
|
|
209
|
+
data_id=data.id,
|
|
210
|
+
provider=data.provider,
|
|
211
|
+
error=str(exc),
|
|
212
|
+
)
|
|
213
|
+
dp.complete_item(success=False, duration=dur)
|
|
214
|
+
return idx, result
|
|
183
215
|
|
|
184
216
|
with ThreadPoolExecutor(max_workers=options.parallel) as executor:
|
|
185
|
-
|
|
186
|
-
executor.submit(
|
|
217
|
+
futures = {
|
|
218
|
+
executor.submit(_run_one, data, i): i
|
|
187
219
|
for i, data in enumerate(data_list)
|
|
188
220
|
}
|
|
189
|
-
for future in as_completed(
|
|
190
|
-
idx =
|
|
191
|
-
|
|
192
|
-
result = future.result()
|
|
193
|
-
results[idx] = result
|
|
194
|
-
except Exception as exc:
|
|
195
|
-
result = DownloadResult(
|
|
196
|
-
status=DownloadStatus.FAILED,
|
|
197
|
-
data_id=data_list[idx].id,
|
|
198
|
-
provider=data_list[idx].provider,
|
|
199
|
-
error=str(exc),
|
|
200
|
-
)
|
|
201
|
-
results[idx] = result
|
|
202
|
-
|
|
221
|
+
for future in as_completed(futures):
|
|
222
|
+
idx, result = future.result()
|
|
223
|
+
results[idx] = result
|
|
203
224
|
completed_count += 1
|
|
204
|
-
self._emit_progress(data_list, results, options)
|
|
205
225
|
if item_done_callback:
|
|
206
226
|
item_done_callback(completed_count, total, result)
|
|
207
227
|
|
|
@@ -218,6 +238,7 @@ class AdaptiveDownloader:
|
|
|
218
238
|
|
|
219
239
|
completed = sum(1 for r in results if r and r.success)
|
|
220
240
|
failed = sum(1 for r in results if r and not r.success)
|
|
241
|
+
dp.__exit__(None, None, None)
|
|
221
242
|
assert len(results) == len(data_list), (
|
|
222
243
|
f"BUG: result count {len(results)} != input count {len(data_list)}"
|
|
223
244
|
)
|
|
@@ -225,11 +246,7 @@ class AdaptiveDownloader:
|
|
|
225
246
|
r.bytes_downloaded / (1024*1024)
|
|
226
247
|
for r in results if r and r.success and r.bytes_downloaded
|
|
227
248
|
)
|
|
228
|
-
|
|
229
|
-
logger.info(
|
|
230
|
-
"Download complete: %d/%d succeeded %.0f MB total",
|
|
231
|
-
completed, len(data_list), total_mb
|
|
232
|
-
)
|
|
249
|
+
# summary printed by DownloadProgress footer
|
|
233
250
|
return results # type: ignore — all slots filled, never None
|
|
234
251
|
|
|
235
252
|
def _get_provider(self, provider_id: str) -> Any:
|
|
@@ -519,55 +536,94 @@ class AdaptiveDownloader:
|
|
|
519
536
|
|
|
520
537
|
def _validate_downloaded_file(self, path) -> tuple:
|
|
521
538
|
"""
|
|
522
|
-
Validate a downloaded
|
|
539
|
+
Validate a downloaded file.
|
|
523
540
|
|
|
524
|
-
|
|
541
|
+
Strategy by file type:
|
|
542
|
+
.zip / .tar.gz — check it's a valid archive (not truncated)
|
|
543
|
+
.tif / .tiff — open with rasterio and read one tile
|
|
544
|
+
.jp2 — open with rasterio (GDAL driver)
|
|
545
|
+
.nc — check size > 0 (netCDF, GDAL can open but slow)
|
|
546
|
+
.json / .xml / .csv / .txt / .parquet — check size > 0
|
|
547
|
+
anything else — check size > 0 (safe fallback, never fail good data)
|
|
525
548
|
|
|
526
549
|
Returns:
|
|
527
550
|
(is_valid: bool, error_message: str)
|
|
528
551
|
"""
|
|
529
552
|
import os
|
|
530
|
-
|
|
531
|
-
|
|
553
|
+
path_str = str(path)
|
|
554
|
+
path_obj = Path(path_str)
|
|
555
|
+
suffix = path_obj.suffix.lower()
|
|
532
556
|
|
|
533
|
-
#
|
|
534
|
-
|
|
535
|
-
|
|
557
|
+
# ── 0. File must exist and be non-empty ───────────────────────────
|
|
558
|
+
try:
|
|
559
|
+
size = os.path.getsize(path_str)
|
|
560
|
+
except OSError as exc:
|
|
561
|
+
return False, f"Cannot stat file: {exc}"
|
|
562
|
+
if size == 0:
|
|
563
|
+
return False, "File is empty (0 bytes)"
|
|
564
|
+
|
|
565
|
+
# ── 1. ZIP / TAR archives — validate archive structure ─────────────
|
|
566
|
+
if suffix == ".zip" or path_str.endswith(".zip"):
|
|
536
567
|
try:
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
568
|
+
import zipfile
|
|
569
|
+
with zipfile.ZipFile(path_str) as zf:
|
|
570
|
+
bad = zf.testzip()
|
|
571
|
+
if bad:
|
|
572
|
+
return False, f"ZIP is corrupt — bad entry: {bad}"
|
|
573
|
+
return True, ""
|
|
574
|
+
except zipfile.BadZipFile as exc:
|
|
575
|
+
return False, f"Invalid ZIP file: {exc}"
|
|
576
|
+
except Exception as exc:
|
|
577
|
+
# If we can't import zipfile or something weird — just trust size
|
|
540
578
|
return True, ""
|
|
541
|
-
except OSError as exc:
|
|
542
|
-
return False, f"Cannot stat file: {exc}"
|
|
543
579
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
580
|
+
if suffix in (".gz", ".tgz") or path_str.endswith(".tar.gz"):
|
|
581
|
+
try:
|
|
582
|
+
import tarfile
|
|
583
|
+
with tarfile.open(path_str) as tf:
|
|
584
|
+
tf.getmembers()
|
|
585
|
+
return True, ""
|
|
586
|
+
except tarfile.TarError as exc:
|
|
587
|
+
return False, f"Invalid TAR file: {exc}"
|
|
588
|
+
except Exception:
|
|
589
|
+
return True, ""
|
|
590
|
+
|
|
591
|
+
# ── 2. Non-raster text/data formats — size check only ─────────────
|
|
592
|
+
NON_RASTER = {
|
|
593
|
+
".json", ".geojson", ".xml", ".csv", ".txt",
|
|
594
|
+
".html", ".md", ".yaml", ".yml", ".parquet", ".nc",
|
|
595
|
+
".EOF", # orbit files
|
|
596
|
+
}
|
|
597
|
+
if suffix in NON_RASTER or not suffix:
|
|
598
|
+
return True, "" # already passed size > 0 above
|
|
599
|
+
|
|
600
|
+
# ── 3. Raster formats — open with rasterio ────────────────────────
|
|
601
|
+
RASTER_SUFFIXES = {".tif", ".tiff", ".jp2", ".img", ".vrt",
|
|
602
|
+
".hdf", ".h4", ".h5", ".hdf5"}
|
|
603
|
+
if suffix in RASTER_SUFFIXES:
|
|
562
604
|
try:
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
605
|
+
import rasterio
|
|
606
|
+
with rasterio.open(path_str) as src:
|
|
607
|
+
if src.count == 0:
|
|
608
|
+
return False, "Raster has zero bands"
|
|
609
|
+
if src.width == 0 or src.height == 0:
|
|
610
|
+
return False, f"Raster has zero dimensions: {src.width}x{src.height}"
|
|
611
|
+
# Read one small tile to confirm data is accessible
|
|
612
|
+
windows = list(src.block_windows(1))
|
|
613
|
+
if windows:
|
|
614
|
+
_, window = windows[0]
|
|
615
|
+
sample = src.read(1, window=window)
|
|
616
|
+
if sample is None or sample.size == 0:
|
|
617
|
+
return False, "First tile read returned empty array"
|
|
618
|
+
return True, ""
|
|
619
|
+
except ImportError:
|
|
620
|
+
# rasterio not installed — size check already passed
|
|
566
621
|
return True, ""
|
|
567
|
-
except
|
|
568
|
-
return False, f"
|
|
569
|
-
|
|
570
|
-
|
|
622
|
+
except Exception as exc:
|
|
623
|
+
return False, f"Raster read failed: {exc}"
|
|
624
|
+
|
|
625
|
+
# ── 4. Unknown extension — trust size check (already passed) ──────
|
|
626
|
+
return True, ""
|
|
571
627
|
|
|
572
628
|
def _has_identity_transform(self, path) -> bool:
|
|
573
629
|
"""
|
|
@@ -673,4 +729,4 @@ class AdaptiveDownloader:
|
|
|
673
729
|
@property
|
|
674
730
|
def _result_total_size_mb(self) -> float:
|
|
675
731
|
"""Property used by result objects."""
|
|
676
|
-
return 0.0
|
|
732
|
+
return 0.0
|