PyGeoFetch 1.0.4__tar.gz → 1.0.5__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.
Files changed (81) hide show
  1. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/PKG-INFO +1 -1
  2. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/__init__.py +2 -2
  3. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/cli/main.py +2 -2
  4. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/models/satellite_data.py +1 -1
  5. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/copernicus.py +211 -74
  6. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/sentinel_hub.py +1 -1
  7. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch.egg-info/PKG-INFO +1 -1
  8. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pyproject.toml +1 -1
  9. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/LICENSE +0 -0
  10. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/README.md +0 -0
  11. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/cli/__init__.py +0 -0
  12. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/cli/auth_commands.py +0 -0
  13. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/cli/config_commands.py +0 -0
  14. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/cli/download_commands.py +0 -0
  15. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/cli/index_commands.py +0 -0
  16. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/cli/pipeline_process_commands.py +0 -0
  17. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/cli/postprocess_commands.py +0 -0
  18. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/cli/preprocess_commands.py +0 -0
  19. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/cli/sar_commands.py +0 -0
  20. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/cli/search_commands.py +0 -0
  21. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/config/__init__.py +0 -0
  22. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/config/defaults.yaml +0 -0
  23. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/config/settings.py +0 -0
  24. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/core/__init__.py +0 -0
  25. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/core/authenticator.py +0 -0
  26. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/core/cache_manager.py +0 -0
  27. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/core/downloader.py +0 -0
  28. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/core/engine.py +0 -0
  29. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/core/logging.py +0 -0
  30. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/core/orbits.py +0 -0
  31. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/core/scheduler.py +0 -0
  32. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/core/searcher.py +0 -0
  33. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/models/__init__.py +0 -0
  34. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/models/download_task.py +0 -0
  35. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/models/search_query.py +0 -0
  36. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/models/user_auth.py +0 -0
  37. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/processing/__init__.py +0 -0
  38. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/processing/base.py +0 -0
  39. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/processing/batch.py +0 -0
  40. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/processing/indices.py +0 -0
  41. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/processing/pipeline.py +0 -0
  42. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/processing/postprocessor.py +0 -0
  43. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/processing/preprocessor.py +0 -0
  44. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/processing/sar.py +0 -0
  45. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/__init__.py +0 -0
  46. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/airbus_oneatlas.py +0 -0
  47. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
  48. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/aws_earth.py +0 -0
  49. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/base.py +0 -0
  50. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/digitalglobe.py +0 -0
  51. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/earth_explorer_additional.py +0 -0
  52. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/element84.py +0 -0
  53. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/esa_scihub.py +0 -0
  54. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/geoserver_generic.py +0 -0
  55. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/google_earth_engine.py +0 -0
  56. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/inpe_cbers.py +0 -0
  57. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/isro_bhuvan.py +0 -0
  58. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/jaxa_earth.py +0 -0
  59. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/maxar_gbdx.py +0 -0
  60. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/nasa_earthdata.py +0 -0
  61. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
  62. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/noaa_big_data.py +0 -0
  63. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/opentopography.py +0 -0
  64. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/planet.py +0 -0
  65. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/planetary_computer.py +0 -0
  66. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/terrabotics.py +0 -0
  67. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/providers/usgs.py +0 -0
  68. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/utils/__init__.py +0 -0
  69. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/utils/file_utils.py +0 -0
  70. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/utils/geo_utils.py +0 -0
  71. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/utils/logging_setup.py +0 -0
  72. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/utils/retry_handler.py +0 -0
  73. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch/utils/validators.py +0 -0
  74. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch.egg-info/SOURCES.txt +0 -0
  75. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch.egg-info/dependency_links.txt +0 -0
  76. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch.egg-info/entry_points.txt +0 -0
  77. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch.egg-info/not-zip-safe +0 -0
  78. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch.egg-info/requires.txt +0 -0
  79. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/pygeofetch.egg-info/top_level.txt +0 -0
  80. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/setup.cfg +0 -0
  81. {pygeofetch-1.0.4 → pygeofetch-1.0.5}/tests/test_pgv_integration_contract.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygeofetch
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: Universal satellite data pipeline — unified access to 22+ providers
5
5
  Author: PyGeoFetch Contributors
6
6
  License: MIT License
@@ -1,5 +1,5 @@
1
1
  """
2
- PyGeoFetch v1.0.4 — Universal Satellite Data Pipeline.
2
+ PyGeoFetch v1.0.5 — 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.4"
33
+ __version__ = "1.0.5"
34
34
  __author__ = "PyGeoFetch Contributors"
35
35
  __license__ = "MIT"
36
36
 
@@ -1,5 +1,5 @@
1
1
  """
2
- PyGeoFetch CLI v1.0.4 main entry point.
2
+ PyGeoFetch CLI v1.0.5 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.4 — Universal Satellite Data Pipeline
62
+ PyGeoFetch v1.0.5 — Universal Satellite Data Pipeline
63
63
  ===========================================================
64
64
  Unified access to 22+ satellite data providers.
65
65
 
@@ -290,7 +290,7 @@ class SatelliteData(BaseModel):
290
290
  """
291
291
  item: Dict[str, Any] = {
292
292
  "type": "Feature",
293
- "stac_version": "1.0.4",
293
+ "stac_version": "1.0.5",
294
294
  "stac_extensions": self.stac_extensions,
295
295
  "id": self.id,
296
296
  "geometry": self.geometry or self._bbox_to_geometry(),
@@ -9,7 +9,7 @@ Authentication:
9
9
  Free registration at https://dataspace.copernicus.eu
10
10
 
11
11
  Supported datasets:
12
- - Sentinel-1 (SAR C-band)
12
+ - Sentinel-1 (SAR C-band, GRD + SLC)
13
13
  - Sentinel-2 (Multispectral, 10-60m)
14
14
  - Sentinel-3 (Ocean/Land colour, OLCI/SLSTR)
15
15
  - Sentinel-5P (Atmospheric composition)
@@ -32,8 +32,8 @@ Example::
32
32
  bbox=(-74.1, 40.6, -73.7, 40.9),
33
33
  start_date="2024-01-01",
34
34
  end_date="2024-03-01",
35
- satellites=["Sentinel-2"],
36
- cloud_cover_max=15,
35
+ satellites=["Sentinel-1"],
36
+ cloud_cover_max=100,
37
37
  ))
38
38
  """
39
39
 
@@ -89,9 +89,56 @@ class CopernicusProvider(AbstractBaseProvider):
89
89
  AUTH_URL = "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token"
90
90
  S3_ENDPOINT = "https://eodata.dataspace.copernicus.eu"
91
91
 
92
- # Map user-friendly satellite names to CDSE collection names
92
+ # ── STAC collection ID → OData collection name + product type ─────────────
93
+ #
94
+ # ROOT CAUSE OF BUG: The original code did [c.upper() for c in query.collections]
95
+ # which turned "sentinel-1-grd" into "SENTINEL-1-GRD". The Copernicus OData API
96
+ # only accepts TOP-LEVEL collection names ("SENTINEL-1", "SENTINEL-2" etc.).
97
+ # The product type (GRD, SLC) is a separate OData attribute filter.
98
+ #
99
+ # STAC collection IDs (planetary_computer / dataspace STAC):
100
+ # "sentinel-1-grd", "sentinel-1-slc"
101
+ # OData collection names:
102
+ # "SENTINEL-1" (plus productType attribute filter "GRD" or "SLC")
103
+ #
104
+ STAC_TO_ODATA_COLLECTION: Dict[str, str] = {
105
+ # Sentinel-1 — all product types map to the same OData collection
106
+ "sentinel-1-grd": "SENTINEL-1",
107
+ "sentinel-1-slc": "SENTINEL-1",
108
+ "sentinel-1-grd-cog": "SENTINEL-1",
109
+ "sentinel-1-ocn": "SENTINEL-1",
110
+ "sentinel-1-rtc": "SENTINEL-1",
111
+ "sentinel-1": "SENTINEL-1",
112
+ # Sentinel-2
113
+ "sentinel-2-l2a": "SENTINEL-2",
114
+ "sentinel-2-l1c": "SENTINEL-2",
115
+ "sentinel-2": "SENTINEL-2",
116
+ # Sentinel-3
117
+ "sentinel-3-olci": "SENTINEL-3",
118
+ "sentinel-3-slstr": "SENTINEL-3",
119
+ "sentinel-3": "SENTINEL-3",
120
+ # Sentinel-5P
121
+ "sentinel-5p-tropomi": "SENTINEL-5P",
122
+ "sentinel-5p": "SENTINEL-5P",
123
+ # Sentinel-6
124
+ "sentinel-6": "SENTINEL-6",
125
+ }
126
+
127
+ # Infer OData productType from STAC collection ID.
128
+ # These are passed to the productType attribute filter automatically.
129
+ STAC_TO_PRODUCT_TYPE: Dict[str, Optional[str]] = {
130
+ "sentinel-1-grd": "GRD",
131
+ "sentinel-1-slc": "SLC",
132
+ "sentinel-1-grd-cog": "GRD-COG",
133
+ "sentinel-1-ocn": "OCN",
134
+ "sentinel-1-rtc": "GRD", # RTC is derived from GRD
135
+ "sentinel-2-l2a": None, # Sentinel-2 uses processingLevel, not productType
136
+ "sentinel-2-l1c": None,
137
+ }
138
+
139
+ # Map user-friendly satellite names to Copernicus OData collection codes
93
140
  SATELLITE_COLLECTION_MAP = {
94
- # Sentinel-1 — all platform variants map to the same collection
141
+ # Sentinel-1 — all platform variants map to the same OData collection
95
142
  "sentinel-1": "SENTINEL-1",
96
143
  "sentinel-1a": "SENTINEL-1",
97
144
  "sentinel-1b": "SENTINEL-1",
@@ -214,12 +261,18 @@ class CopernicusProvider(AbstractBaseProvider):
214
261
  self.require_auth()
215
262
  self._refresh_token_if_needed()
216
263
 
217
- collections = self._resolve_collections(query)
264
+ collections, inferred_product_type = self._resolve_collections(query)
218
265
  results: List[SatelliteData] = []
219
266
 
267
+ # If a product type was inferred from the STAC collection ID and the
268
+ # query doesn't already have one set, apply it as the search filter.
269
+ effective_product_type = getattr(query, "product_type", None) or inferred_product_type
270
+
220
271
  for collection in collections:
221
272
  try:
222
- items = self._search_collection(collection, query)
273
+ items = self._search_collection(
274
+ collection, query, product_type=effective_product_type
275
+ )
223
276
  results.extend(items)
224
277
  if len(results) >= query.max_results:
225
278
  break
@@ -228,51 +281,111 @@ class CopernicusProvider(AbstractBaseProvider):
228
281
 
229
282
  return results[: query.max_results]
230
283
 
231
- def _resolve_collections(self, query: SearchQuery) -> List[str]:
232
- """Map query satellite names to Copernicus collection codes."""
233
- if query.collections:
284
+ def _resolve_collections(
285
+ self, query: SearchQuery
286
+ ) -> tuple[List[str], Optional[str]]:
287
+ """
288
+ Map query satellite names or STAC collection IDs to OData collection names.
289
+
290
+ Returns:
291
+ (odata_collection_names, inferred_product_type)
292
+
293
+ ── Key fix ────────────────────────────────────────────────────────────
294
+ The original implementation did:
234
295
  return [c.upper() for c in query.collections]
235
296
 
297
+ This turned "sentinel-1-grd" into "SENTINEL-1-GRD" — which the
298
+ Copernicus OData API rejects with HTTP 400 because the valid OData
299
+ collection name is "SENTINEL-1" (top-level). The product type (GRD,
300
+ SLC) is passed separately as an OData attribute filter.
301
+
302
+ Now we map STAC collection IDs → OData collection names and also
303
+ extract the product type so it is applied as a filter automatically.
304
+ ───────────────────────────────────────────────────────────────────────
305
+ """
306
+ inferred_product_type: Optional[str] = None
307
+
308
+ if query.collections:
309
+ resolved: set[str] = set()
310
+ for c in query.collections:
311
+ c_lower = c.lower().strip()
312
+
313
+ # Try exact STAC collection ID match first
314
+ if c_lower in self.STAC_TO_ODATA_COLLECTION:
315
+ resolved.add(self.STAC_TO_ODATA_COLLECTION[c_lower])
316
+ # Infer product type from STAC collection if not set
317
+ pt = self.STAC_TO_PRODUCT_TYPE.get(c_lower)
318
+ if pt and inferred_product_type is None:
319
+ inferred_product_type = pt
320
+
321
+ # Already a valid OData collection name (user knew what they wanted)
322
+ elif c.upper() in ("SENTINEL-1", "SENTINEL-2", "SENTINEL-3",
323
+ "SENTINEL-5P", "SENTINEL-6"):
324
+ resolved.add(c.upper())
325
+
326
+ # Prefix fallback: "sentinel-1-*" → SENTINEL-1
327
+ elif c_lower.startswith("sentinel-1") or c_lower.startswith("s1"):
328
+ resolved.add("SENTINEL-1")
329
+ elif c_lower.startswith("sentinel-2") or c_lower.startswith("s2"):
330
+ resolved.add("SENTINEL-2")
331
+ elif c_lower.startswith("sentinel-3") or c_lower.startswith("s3"):
332
+ resolved.add("SENTINEL-3")
333
+ elif c_lower.startswith("sentinel-5") or c_lower.startswith("s5"):
334
+ resolved.add("SENTINEL-5P")
335
+
336
+ else:
337
+ # Unknown — pass through uppercased and log a warning
338
+ self._logger.warning(
339
+ "Unknown collection ID %r. Expected a STAC ID like "
340
+ "'sentinel-1-grd' or an OData name like 'SENTINEL-1'. "
341
+ "Passing through as-is — search may return 400.",
342
+ c
343
+ )
344
+ resolved.add(c.upper())
345
+
346
+ return list(resolved), inferred_product_type
347
+
236
348
  if not query.satellites:
237
- # Use product_type to pick a sensible default collection
349
+ # No collection, no satellite — use product_type to pick a default
238
350
  pt = getattr(query, "product_type", None) or ""
239
351
  if pt.upper() in ("SLC", "GRD", "GRD-COG", "OCN"):
240
- return ["SENTINEL-1"] # SAR product type → Sentinel-1
241
- return ["SENTINEL-2"] # default for optical
352
+ return ["SENTINEL-1"], None
353
+ return ["SENTINEL-2"], None
242
354
 
243
- collections = set()
355
+ # Resolve from satellite names
356
+ resolved_from_sat: set[str] = set()
244
357
  for sat in query.satellites:
245
358
  sat_lower = sat.lower().replace("-", "").replace("_", "").replace(" ", "")
246
359
  sat_orig = sat.lower()
247
360
  matched = False
248
- # Try exact match first, then prefix/substring
249
361
  for key, col in self.SATELLITE_COLLECTION_MAP.items():
250
362
  key_norm = key.replace("-", "").replace("_", "")
251
363
  if key_norm == sat_lower or key in sat_orig or sat_orig.startswith(key):
252
- collections.add(col)
364
+ resolved_from_sat.add(col)
253
365
  matched = True
254
366
  break
255
367
  if not matched:
256
- # Fall back: if it mentions "sentinel-1" anywhere, use SENTINEL-1
257
368
  if "sentinel-1" in sat_orig or sat_orig.startswith("s1"):
258
- collections.add("SENTINEL-1")
369
+ resolved_from_sat.add("SENTINEL-1")
259
370
  elif "sentinel-2" in sat_orig or sat_orig.startswith("s2"):
260
- collections.add("SENTINEL-2")
371
+ resolved_from_sat.add("SENTINEL-2")
261
372
  else:
262
- # Pass through as-is (user may know the collection name)
263
- collections.add(sat.upper())
373
+ resolved_from_sat.add(sat.upper())
264
374
 
265
- # If no satellite matched but product_type suggests SAR, use Sentinel-1
266
- if not collections:
375
+ if not resolved_from_sat:
267
376
  pt = getattr(query, "product_type", None) or ""
268
377
  if pt.upper() in ("SLC", "GRD", "GRD-COG", "OCN"):
269
- collections.add("SENTINEL-1")
270
- else:
271
- collections.add("SENTINEL-2")
378
+ return ["SENTINEL-1"], None
379
+ return ["SENTINEL-2"], None
272
380
 
273
- return list(collections)
381
+ return list(resolved_from_sat), inferred_product_type
274
382
 
275
- def _build_odata_filter(self, collection: str, query: SearchQuery) -> str:
383
+ def _build_odata_filter(
384
+ self,
385
+ collection: str,
386
+ query: SearchQuery,
387
+ product_type: Optional[str] = None,
388
+ ) -> str:
276
389
  """Build OData $filter string for the given collection and query."""
277
390
  filters = [f"Collection/Name eq '{collection}'"]
278
391
 
@@ -293,29 +406,44 @@ class CopernicusProvider(AbstractBaseProvider):
293
406
  )
294
407
  filters.append(f"OData.CSC.Intersects(area=geography'SRID=4326;{polygon}')")
295
408
 
296
- if (query.cloud_cover_max or 100) < 100:
297
- filters.append(f"Attributes/OData.CSC.DoubleAttribute/any(att:att/Name eq 'cloudCover' and att/OData.CSC.DoubleAttribute/Value le {query.cloud_cover_max})")
298
- if getattr(query, 'cloud_cover_min', 0) > 0:
299
- filters.append(f"Attributes/OData.CSC.DoubleAttribute/any(att:att/Name eq 'cloudCover' and att/OData.CSC.DoubleAttribute/Value ge {getattr(query, 'cloud_cover_min', 0)})")
409
+ # Cloud cover — only meaningful for optical (Sentinel-2/3)
410
+ # Sentinel-1 SAR has no cloud cover — skip filter to avoid empty results
411
+ is_sar = collection == "SENTINEL-1"
412
+ if not is_sar and (query.cloud_cover_max or 100) < 100:
413
+ filters.append(
414
+ f"Attributes/OData.CSC.DoubleAttribute/any("
415
+ f"att:att/Name eq 'cloudCover' and "
416
+ f"att/OData.CSC.DoubleAttribute/Value le {query.cloud_cover_max})"
417
+ )
418
+ if not is_sar and getattr(query, "cloud_cover_min", 0) > 0:
419
+ filters.append(
420
+ f"Attributes/OData.CSC.DoubleAttribute/any("
421
+ f"att:att/Name eq 'cloudCover' and "
422
+ f"att/OData.CSC.DoubleAttribute/Value ge {getattr(query, 'cloud_cover_min', 0)})"
423
+ )
300
424
 
301
- # Processing level filter
425
+ # Processing level filter (Sentinel-2: L1C, L2A)
302
426
  if query.processing_levels:
303
427
  level_filter = " or ".join(
304
- f"Attributes/OData.CSC.StringAttribute/any(att:att/Name eq 'processingLevel' and att/OData.CSC.StringAttribute/Value eq '{level}')"
428
+ f"Attributes/OData.CSC.StringAttribute/any("
429
+ f"att:att/Name eq 'processingLevel' and "
430
+ f"att/OData.CSC.StringAttribute/Value eq '{level}')"
305
431
  for level in query.processing_levels
306
432
  )
307
433
  filters.append(f"({level_filter})")
308
434
 
309
- # Product type filter (SLC, GRD, GRD-COG, OCN for Sentinel-1)
310
- pt = getattr(query, "product_type", None)
311
- if pt:
435
+ # Product type filter — GRD, SLC, GRD-COG, OCN (Sentinel-1)
436
+ # product_type comes from either the query directly or was inferred
437
+ # from the STAC collection ID (e.g. "sentinel-1-grd" → "GRD")
438
+ effective_pt = product_type or getattr(query, "product_type", None)
439
+ if effective_pt:
312
440
  filters.append(
313
441
  f"Attributes/OData.CSC.StringAttribute/any("
314
442
  f"att:att/Name eq 'productType' and "
315
- f"att/OData.CSC.StringAttribute/Value eq '{pt.upper()}')"
443
+ f"att/OData.CSC.StringAttribute/Value eq '{effective_pt.upper()}')"
316
444
  )
317
445
 
318
- # Polarisation filter (VV, VH, VV+VH, HH etc.)
446
+ # Polarisation filter (VV+VH, HH, etc.)
319
447
  pol = getattr(query, "polarisation", None)
320
448
  if pol:
321
449
  filters.append(
@@ -324,7 +452,7 @@ class CopernicusProvider(AbstractBaseProvider):
324
452
  f"att/OData.CSC.StringAttribute/Value eq '{pol.upper()}')"
325
453
  )
326
454
 
327
- # Pass direction filter (ascending / descending)
455
+ # Pass direction filter (ASCENDING / DESCENDING)
328
456
  pass_dir = getattr(query, "pass_direction", None)
329
457
  if pass_dir:
330
458
  filters.append(
@@ -335,15 +463,20 @@ class CopernicusProvider(AbstractBaseProvider):
335
463
 
336
464
  return " and ".join(filters)
337
465
 
338
- def _search_collection(self, collection: str, query: SearchQuery) -> List[SatelliteData]:
466
+ def _search_collection(
467
+ self,
468
+ collection: str,
469
+ query: SearchQuery,
470
+ product_type: Optional[str] = None,
471
+ ) -> List[SatelliteData]:
339
472
  """Perform OData search for a specific collection."""
340
473
  import httpx
341
474
 
342
475
  params: Dict[str, Any] = {
343
- "$filter": self._build_odata_filter(collection, query),
476
+ "$filter": self._build_odata_filter(collection, query, product_type),
344
477
  "$orderby": "ContentDate/Start desc",
345
- "$top": min(query.max_results, 1000),
346
- "$expand": "Attributes",
478
+ "$top": min(query.max_results, 1000),
479
+ "$expand": "Attributes",
347
480
  }
348
481
 
349
482
  response = httpx.get(
@@ -359,7 +492,7 @@ class CopernicusProvider(AbstractBaseProvider):
359
492
  def _product_to_satellite_data(self, product: Dict) -> SatelliteData:
360
493
  """Convert a Copernicus OData product dict to SatelliteData."""
361
494
  product_id = product.get("Id", "")
362
- name = product.get("Name", "")
495
+ name = product.get("Name", "")
363
496
  collection = product.get("S3Path", "").split("/")[2] if product.get("S3Path") else ""
364
497
 
365
498
  # Parse footprint
@@ -387,7 +520,7 @@ class CopernicusProvider(AbstractBaseProvider):
387
520
  except Exception:
388
521
  pass
389
522
 
390
- # Extract attributes
523
+ # Extract attributes from OData $expand=Attributes
391
524
  attributes = {
392
525
  a.get("Name"): a.get("Value")
393
526
  for a in (product.get("Attributes") or [])
@@ -399,7 +532,8 @@ class CopernicusProvider(AbstractBaseProvider):
399
532
  except (ValueError, TypeError):
400
533
  cloud_cover = None
401
534
 
402
- # Determine satellite platform from product name (e.g. S1C_IW_SLC...)
535
+ # Determine satellite platform from product name
536
+ # (e.g. S1C_IW_SLC → Sentinel-1C)
403
537
  satellite = "Sentinel"
404
538
  platform = attributes.get("platform") or attributes.get("platformShortName", "")
405
539
  if name.startswith("S1C") or "SENTINEL-1C" in platform.upper():
@@ -419,24 +553,23 @@ class CopernicusProvider(AbstractBaseProvider):
419
553
  elif name.startswith("S5P"):
420
554
  satellite = "Sentinel-5P"
421
555
 
422
- # Extract SAR-specific attributes
423
- # Copernicus OData attribute names (from $expand=Attributes):
424
- product_type = attributes.get("productType") # "SLC", "GRD", "GRD-COG"
425
- polarisation = attributes.get("polarisationChannels") # "VV VH", "HH", etc.
426
- pass_direction = attributes.get("orbitDirection") # "ASCENDING", "DESCENDING"
427
- rel_orbit = attributes.get("relativeOrbitNumber")
428
- abs_orbit = attributes.get("absoluteOrbitNumber")
556
+ # SAR-specific attributes (all from OData $expand=Attributes)
557
+ product_type = attributes.get("productType") # "SLC", "GRD", "GRD-COG"
558
+ polarisation = attributes.get("polarisationChannels") # "VV VH", "HH", etc.
559
+ pass_direction = attributes.get("orbitDirection") # "ASCENDING", "DESCENDING"
560
+ rel_orbit = attributes.get("relativeOrbitNumber")
561
+ abs_orbit = attributes.get("absoluteOrbitNumber")
429
562
  incidence_angle = attributes.get("incidenceAngleMax")
430
563
 
431
- # Normalise polarisation to "VV+VH" style
564
+ # Normalise polarisation string: "VV VH" → "VV+VH"
432
565
  if polarisation:
433
566
  polarisation = polarisation.strip().replace(" ", "+")
434
567
 
435
- # Normalise pass direction to lowercase
568
+ # Normalise pass direction to lowercase: "ASCENDING" → "ascending"
436
569
  if pass_direction:
437
- pass_direction = pass_direction.lower() # "ascending" / "descending"
570
+ pass_direction = pass_direction.lower()
438
571
 
439
- # Parse orbit numbers to int
572
+ # Parse numeric fields
440
573
  try:
441
574
  rel_orbit = int(rel_orbit) if rel_orbit is not None else None
442
575
  except (ValueError, TypeError):
@@ -454,7 +587,7 @@ class CopernicusProvider(AbstractBaseProvider):
454
587
  gsd_map = {"SLC": 5.0, "GRD": 10.0, "GRD-COG": 10.0, "OCN": None}
455
588
  gsd_m = gsd_map.get(product_type or "", None)
456
589
 
457
- # Build assets
590
+ # Build assets dict
458
591
  assets: Dict[str, SatelliteAsset] = {}
459
592
  s3_path = product.get("S3Path")
460
593
  if s3_path:
@@ -493,19 +626,19 @@ class CopernicusProvider(AbstractBaseProvider):
493
626
  processing_level=proc_level,
494
627
  assets=assets,
495
628
  properties={
496
- "name": name,
629
+ "name": name,
497
630
  "s3_path": s3_path,
498
631
  **attributes,
499
632
  },
500
- # SAR / platform fields (Capability 1 + 2)
501
- product_type=product_type,
502
- polarisation=polarisation,
503
- pass_direction=pass_direction,
504
- relative_orbit=rel_orbit,
505
- orbit_number=abs_orbit,
506
- incidence_angle=incidence_angle,
507
- gsd_m=gsd_m,
508
- resolution_m=gsd_m,
633
+ # SAR / platform metadata fields
634
+ product_type = product_type,
635
+ polarisation = polarisation,
636
+ pass_direction = pass_direction,
637
+ relative_orbit = rel_orbit,
638
+ orbit_number = abs_orbit,
639
+ incidence_angle = incidence_angle,
640
+ gsd_m = gsd_m,
641
+ resolution_m = gsd_m,
509
642
  )
510
643
 
511
644
  def download(
@@ -518,9 +651,9 @@ class CopernicusProvider(AbstractBaseProvider):
518
651
  Download a Copernicus product via authenticated HTTPS.
519
652
 
520
653
  Args:
521
- data: SatelliteData to download.
654
+ data: SatelliteData to download.
522
655
  destination: Output directory.
523
- options: Download configuration.
656
+ options: Download configuration.
524
657
 
525
658
  Returns:
526
659
  DownloadResult.
@@ -556,10 +689,12 @@ class CopernicusProvider(AbstractBaseProvider):
556
689
  ) as resp:
557
690
  self._handle_http_error(resp)
558
691
  with open(output_file, "wb") as f:
559
- for chunk in resp.iter_bytes(chunk_size=int(options.chunk_size_mb * 1024 * 1024)):
692
+ for chunk in resp.iter_bytes(
693
+ chunk_size=int(options.chunk_size_mb * 1024 * 1024)
694
+ ):
560
695
  f.write(chunk)
561
696
 
562
- duration = time.time() - start_time
697
+ duration = time.time() - start_time
563
698
  file_size = output_file.stat().st_size
564
699
 
565
700
  return DownloadResult(
@@ -590,7 +725,9 @@ class CopernicusProvider(AbstractBaseProvider):
590
725
  streaming=True,
591
726
  stac=False,
592
727
  requires_auth=True,
593
- supported_satellites=["Sentinel-1", "Sentinel-2", "Sentinel-3", "Sentinel-5P", "Sentinel-6"],
728
+ supported_satellites=[
729
+ "Sentinel-1", "Sentinel-2", "Sentinel-3", "Sentinel-5P", "Sentinel-6"
730
+ ],
594
731
  supported_formats=[DataFormat.SAFE, DataFormat.GEOTIFF, DataFormat.ZIP],
595
732
  supports_cloud_filter=True,
596
733
  supports_date_filter=True,
@@ -107,7 +107,7 @@ class SentinelHubProvider(AbstractBaseProvider):
107
107
  payload["filter"] = {"maxCloudCoverage": query.cloud_cover_max}
108
108
  try:
109
109
  resp = httpx.post(
110
- f"{self.BASE_URL}/api/v1/catalog/1.0.4/search",
110
+ f"{self.BASE_URL}/api/v1/catalog/1.0.5/search",
111
111
  json=payload, headers={"Authorization": f"Bearer {self._session.access_token}"},
112
112
  timeout=self.config.get("timeout", 60),
113
113
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygeofetch
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: Universal satellite data pipeline — unified access to 22+ providers
5
5
  Author: PyGeoFetch Contributors
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pygeofetch"
7
- version = "1.0.4"
7
+ version = "1.0.5"
8
8
  description = "Universal satellite data pipeline — unified access to 22+ providers"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
File without changes
File without changes
File without changes