mapchete-eo 2026.2.0__py3-none-any.whl

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 (89) hide show
  1. mapchete_eo/__init__.py +1 -0
  2. mapchete_eo/array/__init__.py +0 -0
  3. mapchete_eo/array/buffer.py +16 -0
  4. mapchete_eo/array/color.py +29 -0
  5. mapchete_eo/array/convert.py +163 -0
  6. mapchete_eo/base.py +653 -0
  7. mapchete_eo/blacklist.txt +175 -0
  8. mapchete_eo/cli/__init__.py +30 -0
  9. mapchete_eo/cli/bounds.py +22 -0
  10. mapchete_eo/cli/options_arguments.py +227 -0
  11. mapchete_eo/cli/s2_brdf.py +77 -0
  12. mapchete_eo/cli/s2_cat_results.py +130 -0
  13. mapchete_eo/cli/s2_find_broken_products.py +77 -0
  14. mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
  15. mapchete_eo/cli/s2_mask.py +71 -0
  16. mapchete_eo/cli/s2_mgrs.py +45 -0
  17. mapchete_eo/cli/s2_rgb.py +114 -0
  18. mapchete_eo/cli/s2_verify.py +129 -0
  19. mapchete_eo/cli/static_catalog.py +82 -0
  20. mapchete_eo/eostac.py +30 -0
  21. mapchete_eo/exceptions.py +87 -0
  22. mapchete_eo/image_operations/__init__.py +12 -0
  23. mapchete_eo/image_operations/blend_functions.py +579 -0
  24. mapchete_eo/image_operations/color_correction.py +136 -0
  25. mapchete_eo/image_operations/compositing.py +266 -0
  26. mapchete_eo/image_operations/dtype_scale.py +43 -0
  27. mapchete_eo/image_operations/fillnodata.py +130 -0
  28. mapchete_eo/image_operations/filters.py +319 -0
  29. mapchete_eo/image_operations/linear_normalization.py +81 -0
  30. mapchete_eo/image_operations/sigmoidal.py +114 -0
  31. mapchete_eo/io/__init__.py +37 -0
  32. mapchete_eo/io/assets.py +496 -0
  33. mapchete_eo/io/items.py +162 -0
  34. mapchete_eo/io/levelled_cubes.py +259 -0
  35. mapchete_eo/io/path.py +155 -0
  36. mapchete_eo/io/products.py +423 -0
  37. mapchete_eo/io/profiles.py +45 -0
  38. mapchete_eo/platforms/sentinel2/__init__.py +17 -0
  39. mapchete_eo/platforms/sentinel2/_mapper_registry.py +89 -0
  40. mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
  41. mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
  42. mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
  43. mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
  44. mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
  45. mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
  46. mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
  47. mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
  48. mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
  49. mapchete_eo/platforms/sentinel2/config.py +241 -0
  50. mapchete_eo/platforms/sentinel2/driver.py +43 -0
  51. mapchete_eo/platforms/sentinel2/masks.py +329 -0
  52. mapchete_eo/platforms/sentinel2/metadata_parser/__init__.py +6 -0
  53. mapchete_eo/platforms/sentinel2/metadata_parser/base.py +56 -0
  54. mapchete_eo/platforms/sentinel2/metadata_parser/default_path_mapper.py +135 -0
  55. mapchete_eo/platforms/sentinel2/metadata_parser/models.py +78 -0
  56. mapchete_eo/platforms/sentinel2/metadata_parser/s2metadata.py +639 -0
  57. mapchete_eo/platforms/sentinel2/preconfigured_sources/__init__.py +57 -0
  58. mapchete_eo/platforms/sentinel2/preconfigured_sources/guessers.py +108 -0
  59. mapchete_eo/platforms/sentinel2/preconfigured_sources/item_mappers.py +171 -0
  60. mapchete_eo/platforms/sentinel2/preconfigured_sources/metadata_xml_mappers.py +217 -0
  61. mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +50 -0
  62. mapchete_eo/platforms/sentinel2/processing_baseline.py +163 -0
  63. mapchete_eo/platforms/sentinel2/product.py +747 -0
  64. mapchete_eo/platforms/sentinel2/source.py +114 -0
  65. mapchete_eo/platforms/sentinel2/types.py +114 -0
  66. mapchete_eo/processes/__init__.py +0 -0
  67. mapchete_eo/processes/config.py +51 -0
  68. mapchete_eo/processes/dtype_scale.py +112 -0
  69. mapchete_eo/processes/eo_to_xarray.py +19 -0
  70. mapchete_eo/processes/merge_rasters.py +239 -0
  71. mapchete_eo/product.py +323 -0
  72. mapchete_eo/protocols.py +61 -0
  73. mapchete_eo/search/__init__.py +14 -0
  74. mapchete_eo/search/base.py +285 -0
  75. mapchete_eo/search/config.py +113 -0
  76. mapchete_eo/search/s2_mgrs.py +313 -0
  77. mapchete_eo/search/stac_search.py +278 -0
  78. mapchete_eo/search/stac_static.py +197 -0
  79. mapchete_eo/search/utm_search.py +251 -0
  80. mapchete_eo/settings.py +25 -0
  81. mapchete_eo/sort.py +60 -0
  82. mapchete_eo/source.py +109 -0
  83. mapchete_eo/time.py +62 -0
  84. mapchete_eo/types.py +76 -0
  85. mapchete_eo-2026.2.0.dist-info/METADATA +91 -0
  86. mapchete_eo-2026.2.0.dist-info/RECORD +89 -0
  87. mapchete_eo-2026.2.0.dist-info/WHEEL +4 -0
  88. mapchete_eo-2026.2.0.dist-info/entry_points.txt +11 -0
  89. mapchete_eo-2026.2.0.dist-info/licenses/LICENSE +21 -0
mapchete_eo/product.py ADDED
@@ -0,0 +1,323 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, List, Literal, Optional, Set
5
+
6
+ import numpy as np
7
+ import numpy.ma as ma
8
+ from pystac import Item
9
+ import xarray as xr
10
+ from mapchete import Timer
11
+ from mapchete.io.raster import ReferencedRaster
12
+ from mapchete.path import MPath, MPathLike
13
+ from mapchete.protocols import GridProtocol
14
+ from mapchete.types import Bounds, NodataVals
15
+ from numpy.typing import DTypeLike
16
+ from rasterio.enums import Resampling
17
+ from shapely.geometry import shape
18
+
19
+ from mapchete_eo.array.convert import to_dataarray
20
+ from mapchete_eo.io import get_item_property, item_to_np_array
21
+ from mapchete_eo.protocols import EOProductProtocol
22
+ from mapchete_eo.settings import mapchete_eo_settings
23
+ from mapchete_eo.types import BandLocation
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class EOProduct(EOProductProtocol):
29
+ """
30
+ Wrapper class around a STAC Item which provides data reading capabilities.
31
+ """
32
+
33
+ id: str
34
+ default_dtype: DTypeLike = np.uint16
35
+ _item: Optional[Item] = None
36
+
37
+ def __init__(self, item: Item):
38
+ self.item_dict = item.to_dict()
39
+ self.__geo_interface__ = self.item.geometry
40
+ self.bounds = Bounds.from_inp(shape(self))
41
+ self.crs = mapchete_eo_settings.default_catalog_crs
42
+ self._item = None
43
+ self.id = item.id
44
+
45
+ def __repr__(self):
46
+ return f"<EOProduct product_id={self.item.id}>"
47
+
48
+ def clear_cached_data(self):
49
+ pass
50
+
51
+ @property
52
+ def item(self) -> Item:
53
+ if not self._item:
54
+ self._item = Item.from_dict(self.item_dict)
55
+ return self._item
56
+
57
+ @classmethod
58
+ def from_stac_item(self, item: Item, **kwargs) -> EOProduct:
59
+ return EOProduct(item)
60
+
61
+ def get_mask(self) -> ReferencedRaster: ...
62
+
63
+ def read(
64
+ self,
65
+ assets: Optional[List[str]] = None,
66
+ eo_bands: Optional[List[str]] = None,
67
+ grid: Optional[GridProtocol] = None,
68
+ resampling: Resampling = Resampling.nearest,
69
+ nodatavals: NodataVals = None,
70
+ x_axis_name: str = "x",
71
+ y_axis_name: str = "y",
72
+ raise_empty: bool = True,
73
+ **kwargs,
74
+ ) -> xr.Dataset:
75
+ """
76
+ Read bands and assets into an xarray.Dataset.
77
+
78
+ Args:
79
+ assets: List of asset names.
80
+ eo_bands: List of EO band names.
81
+ grid: Target grid protocol.
82
+ resampling: Resampling algorithm.
83
+ nodatavals: Custom nodata values.
84
+ x_axis_name: Name of X axis in output.
85
+ y_axis_name: Name of Y axis in output.
86
+ raise_empty: Raise exception if no data is found.
87
+
88
+ Returns:
89
+ xr.Dataset: Dataset with assets as data variables.
90
+ """
91
+ # developer info: all fancy stuff for special platforms like Sentinel-2
92
+ # should be implemented in the respective read_np_array() methods which get
93
+ # called by this method. No need to apply masks etc. here too.
94
+ if isinstance(nodatavals, list):
95
+ nodataval = nodatavals[0]
96
+ elif isinstance(nodatavals, float):
97
+ nodataval = nodatavals
98
+ else:
99
+ nodataval = nodatavals
100
+
101
+ assets = assets or []
102
+ eo_bands = eo_bands or []
103
+ data_var_names = assets or eo_bands
104
+ return xr.Dataset(
105
+ data_vars={
106
+ data_var_name: to_dataarray(
107
+ asset_arr,
108
+ x_axis_name=x_axis_name,
109
+ y_axis_name=y_axis_name,
110
+ name=data_var_name,
111
+ attrs=dict(item_id=self.item.id),
112
+ )
113
+ for asset_arr, data_var_name in zip(
114
+ self.read_np_array(
115
+ assets=assets,
116
+ eo_bands=eo_bands,
117
+ grid=grid,
118
+ resampling=resampling,
119
+ nodatavals=nodatavals,
120
+ raise_empty=raise_empty,
121
+ **kwargs,
122
+ ),
123
+ data_var_names,
124
+ )
125
+ },
126
+ coords={},
127
+ attrs=dict(self.item.properties, id=self.item.id, _FillValue=nodataval),
128
+ )
129
+
130
+ def read_np_array(
131
+ self,
132
+ assets: Optional[List[str]] = None,
133
+ eo_bands: Optional[List[str]] = None,
134
+ grid: Optional[GridProtocol] = None,
135
+ resampling: Resampling = Resampling.nearest,
136
+ nodatavals: NodataVals = None,
137
+ raise_empty: bool = True,
138
+ apply_offset: bool = True,
139
+ **kwargs,
140
+ ) -> ma.MaskedArray:
141
+ """
142
+ Read assets or EO bands into a MaskedArray.
143
+
144
+ Args:
145
+ assets: List of asset names.
146
+ eo_bands: List of EO band names.
147
+ grid: Target grid.
148
+ resampling: Resampling method.
149
+ nodatavals: Nodata values.
150
+ raise_empty: Raise if empty.
151
+ apply_offset: Apply offset/scale metadata if present.
152
+
153
+ Returns:
154
+ ma.MaskedArray: Output array.
155
+ """
156
+ assets = assets or []
157
+ eo_bands = eo_bands or []
158
+ bands = assets or eo_bands
159
+ logger.debug("%s: reading assets %s over %s", self, bands, grid)
160
+ with Timer() as t:
161
+ out = item_to_np_array(
162
+ self.item,
163
+ self.assets_eo_bands_to_band_locations(assets, eo_bands),
164
+ grid=grid,
165
+ resampling=resampling,
166
+ nodatavals=nodatavals,
167
+ raise_empty=raise_empty,
168
+ apply_offset=apply_offset,
169
+ )
170
+ logger.debug("%s: read in %s", self, t)
171
+ return out
172
+
173
+ def empty_array(
174
+ self,
175
+ count: int,
176
+ grid: GridProtocol,
177
+ fill_value: int = 0,
178
+ dtype: Optional[DTypeLike] = None,
179
+ ) -> ma.MaskedArray:
180
+ shape = (count, *grid.shape)
181
+ dtype = dtype or self.default_dtype
182
+ return ma.MaskedArray(
183
+ data=np.full(shape, fill_value=fill_value, dtype=dtype),
184
+ mask=np.ones(shape, dtype=bool),
185
+ fill_value=fill_value,
186
+ )
187
+
188
+ def get_property(self, property: str) -> Any:
189
+ return get_item_property(self.item, property)
190
+
191
+ def eo_bands_to_band_location(self, eo_bands: List[str]) -> List[BandLocation]:
192
+ return eo_bands_to_band_locations(self.item, eo_bands)
193
+
194
+ def assets_eo_bands_to_band_locations(
195
+ self,
196
+ assets: Optional[List[str]] = None,
197
+ eo_bands: Optional[List[str]] = None,
198
+ ) -> List[BandLocation]:
199
+ assets = assets or []
200
+ eo_bands = eo_bands or []
201
+ if assets and eo_bands:
202
+ raise ValueError("assets and eo_bands cannot be provided at the same time")
203
+ if assets:
204
+ return [BandLocation(asset_name=asset) for asset in assets]
205
+ elif eo_bands:
206
+ return self.eo_bands_to_band_location(eo_bands)
207
+ else:
208
+ raise ValueError("assets or eo_bands have to be provided")
209
+
210
+
211
+ def eo_bands_to_band_locations(
212
+ item: Item,
213
+ eo_bands: List[str],
214
+ role: Literal["data", "reflectance", "visual"] = "data",
215
+ ) -> List[BandLocation]:
216
+ """
217
+ Map EO band names to asset locations.
218
+
219
+ Args:
220
+ item: STAC Item.
221
+ eo_bands: List of common band names.
222
+ role: Functional role of the assets.
223
+
224
+ Returns:
225
+ List[BandLocation]: List of location objects.
226
+ """
227
+ return [find_eo_band(item, eo_band, role=role) for eo_band in eo_bands]
228
+
229
+
230
+ def find_eo_band(
231
+ item: Item,
232
+ eo_band_name: str,
233
+ role: Literal["data", "reflectance", "visual"] = "data",
234
+ ) -> BandLocation:
235
+ """
236
+ Tries to find the location of the most appropriate band using the EO band name.
237
+
238
+ This function looks into all assets and all eo bands for the given name and role.
239
+ """
240
+ results = []
241
+ for asset_name, asset in item.assets.items():
242
+ # search in eo:bands and alternatively in bands for eo:common_name
243
+ for band_index, band_info in enumerate(
244
+ asset.extra_fields.get("eo:bands", asset.extra_fields.get("bands", [])), 1
245
+ ):
246
+ if (
247
+ # if name matches eo band name
248
+ (
249
+ eo_band_name == band_info.get("name")
250
+ or eo_band_name == band_info.get("eo:common_name")
251
+ )
252
+ # if role is given, make sure it matches with desired role
253
+ and (asset.roles is None or role in asset.roles)
254
+ ):
255
+ results.append(
256
+ BandLocation.from_asset(
257
+ name=asset_name,
258
+ band_index=band_index,
259
+ asset=asset,
260
+ )
261
+ )
262
+
263
+ if len(results) == 0:
264
+ raise KeyError(f"EO band {eo_band_name} not found in item assets")
265
+
266
+ elif len(results) == 1:
267
+ return results[0]
268
+
269
+ # if results are ambiguous, further filter them
270
+ else:
271
+ # only use locations which seem to have the original resolution
272
+ for matches in [_asset_name_equals_eo_name, _is_original_sampling]:
273
+ filtered_results = [
274
+ band_location for band_location in results if matches(band_location)
275
+ ]
276
+ if len(filtered_results) == 1:
277
+ return filtered_results[0]
278
+ else: # pragma: no cover
279
+ raise ValueError(
280
+ f"EO band '{eo_band_name}' found in multiple assets: {', '.join(map(str, results))}"
281
+ )
282
+
283
+
284
+ def _asset_name_equals_eo_name(band_location: BandLocation) -> bool:
285
+ return band_location.asset_name == band_location.eo_band_name
286
+
287
+
288
+ def _is_original_sampling(band_location: BandLocation) -> bool:
289
+ return band_location.roles == [] or "sampling:original" in band_location.roles
290
+
291
+
292
+ def add_to_blacklist(path: MPathLike, blacklist: Optional[MPath] = None) -> None:
293
+ blacklist = blacklist or mapchete_eo_settings.blacklist
294
+
295
+ if blacklist is None:
296
+ return
297
+
298
+ blacklist = MPath.from_inp(blacklist)
299
+
300
+ path = MPath.from_inp(path)
301
+
302
+ # make sure paths stay unique
303
+ if str(path) not in blacklist_products(blacklist):
304
+ logger.debug("add path %s to blacklist", str(path))
305
+ try:
306
+ with blacklist.open("a") as dst:
307
+ dst.write(f"{path}\n")
308
+ except FileNotFoundError:
309
+ with blacklist.open("w") as dst:
310
+ dst.write(f"{path}\n")
311
+
312
+
313
+ def blacklist_products(blacklist: Optional[MPathLike] = None) -> Set[str]:
314
+ blacklist = blacklist or mapchete_eo_settings.blacklist
315
+ if blacklist is None:
316
+ raise ValueError("no blacklist is defined")
317
+ blacklist = MPath.from_inp(blacklist)
318
+
319
+ try:
320
+ return set(blacklist.read_text().splitlines())
321
+ except FileNotFoundError:
322
+ logger.debug("%s does not exist, returning empty set", str(blacklist))
323
+ return set()
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional, Protocol
4
+
5
+ import numpy.ma as ma
6
+ import pystac
7
+ import xarray as xr
8
+ from mapchete.protocols import GridProtocol
9
+ from mapchete.types import Bounds, NodataVals
10
+ from rasterio.crs import CRS
11
+ from rasterio.enums import Resampling
12
+
13
+ from mapchete_eo.types import DateTimeLike
14
+ from mapchete.io.raster import ReferencedRaster
15
+
16
+
17
+ class EOProductProtocol(Protocol):
18
+ id: str
19
+ bounds: Bounds
20
+ crs: CRS
21
+ __geo_interface__: Optional[Dict[str, Any]]
22
+
23
+ @classmethod
24
+ def from_stac_item(self, item: pystac.Item, **kwargs) -> EOProductProtocol: ...
25
+
26
+ def get_mask(self) -> ReferencedRaster: ...
27
+
28
+ def read(
29
+ self,
30
+ assets: Optional[List[str]] = None,
31
+ eo_bands: Optional[List[str]] = None,
32
+ grid: Optional[GridProtocol] = None,
33
+ resampling: Resampling = Resampling.nearest,
34
+ nodatavals: NodataVals = None,
35
+ x_axis_name: str = "x",
36
+ y_axis_name: str = "y",
37
+ **kwargs,
38
+ ) -> xr.Dataset: ...
39
+
40
+ def read_np_array(
41
+ self,
42
+ assets: Optional[List[str]] = None,
43
+ eo_bands: Optional[List[str]] = None,
44
+ grid: Optional[GridProtocol] = None,
45
+ resampling: Resampling = Resampling.nearest,
46
+ nodatavals: NodataVals = None,
47
+ **kwargs,
48
+ ) -> ma.MaskedArray: ...
49
+
50
+ def get_property(self, property: str) -> Any: ...
51
+
52
+ @property
53
+ def item(self) -> pystac.Item: ...
54
+
55
+
56
+ class DateTimeProtocol(Protocol):
57
+ datetime: DateTimeLike
58
+
59
+
60
+ class GetPropertyProtocol(Protocol):
61
+ def get_property(self, property: str) -> Any: ...
@@ -0,0 +1,14 @@
1
+ """
2
+ A catalog is an instance with a specific endpoint and a specific collection.
3
+
4
+ The catalog class aims to abstract product search as well as homogenization
5
+ of product metadata.
6
+
7
+ It helps the InputData class to find the input products and their metadata.
8
+ """
9
+
10
+ from mapchete_eo.search.stac_search import STACSearchCollection
11
+ from mapchete_eo.search.stac_static import STACStaticCollection
12
+ from mapchete_eo.search.utm_search import UTMSearchCatalog
13
+
14
+ __all__ = ["STACSearchCollection", "STACStaticCollection", "UTMSearchCatalog"]
@@ -0,0 +1,285 @@
1
+ from functools import cached_property
2
+ import json
3
+ import logging
4
+ from abc import ABC, abstractmethod
5
+ from typing import Any, Callable, Dict, Generator, List, Optional, Set, Type, Union
6
+
7
+ from pygeofilter.parsers.ecql import parse as parse_ecql
8
+ from pygeofilter.backends.native.evaluate import NativeEvaluator
9
+ from pydantic import BaseModel
10
+ from mapchete.path import MPath, MPathLike
11
+ from mapchete.types import Bounds
12
+ from pystac import Catalog, Item, CatalogType, Extent
13
+ from pystac.collection import Collection
14
+ from pystac.stac_io import DefaultStacIO
15
+ from pystac_client import CollectionClient
16
+ from pystac_client.stac_api_io import StacApiIO
17
+ from rasterio.profiles import Profile
18
+ from shapely.geometry.base import BaseGeometry
19
+
20
+ from mapchete_eo.io.assets import get_assets, get_metadata_assets
21
+ from mapchete_eo.product import blacklist_products
22
+ from mapchete_eo.settings import mapchete_eo_settings
23
+ from mapchete_eo.types import TimeRange
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class FSSpecStacIO(StacApiIO):
29
+ """Custom class which allows I/O operations on object storage."""
30
+
31
+ def read_text(self, source: MPathLike, *args, **kwargs) -> str:
32
+ return MPath.from_inp(source).read_text()
33
+
34
+ def write_text(self, dest: MPathLike, txt: str, *args, **kwargs) -> None:
35
+ path = MPath.from_inp(dest)
36
+ if not path.parent.exists():
37
+ path.parent.makedirs(exist_ok=True)
38
+ with path.open("w") as dst:
39
+ return dst.write(txt)
40
+
41
+ # TODO: investigate in pystac why this has to be a staticmethod
42
+ @staticmethod
43
+ def save_json(dest: MPathLike, json_dict: dict, *args, **kwargs) -> None:
44
+ path = MPath.from_inp(dest)
45
+ if not path.parent.exists():
46
+ path.parent.makedirs(exist_ok=True)
47
+ with path.open("w") as dst:
48
+ return dst.write(json.dumps(json_dict, indent=2))
49
+
50
+
51
+ class CollectionSearcher(ABC):
52
+ """
53
+ Bridge between a Source and a catalog implementation.
54
+ """
55
+
56
+ config_cls: Type[BaseModel]
57
+ collection: str
58
+ stac_item_modifiers: Optional[List[Callable[[Item], Item]]] = None
59
+ blacklist: Set[str] = (
60
+ blacklist_products(mapchete_eo_settings.blacklist)
61
+ if mapchete_eo_settings.blacklist
62
+ else set()
63
+ )
64
+
65
+ def __init__(
66
+ self,
67
+ collection: str,
68
+ stac_item_modifiers: Optional[List[Callable[[Item], Item]]] = None,
69
+ ):
70
+ self.collection = collection
71
+ self.stac_item_modifiers = stac_item_modifiers
72
+
73
+ @abstractmethod
74
+ @cached_property
75
+ def client(self) -> CollectionClient: ...
76
+
77
+ @abstractmethod
78
+ @cached_property
79
+ def eo_bands(self) -> List[str]: ...
80
+
81
+ @property
82
+ def config(self) -> BaseModel:
83
+ return self.config_cls()
84
+
85
+ @cached_property
86
+ def id(self) -> str:
87
+ return self.client.id
88
+
89
+ @cached_property
90
+ def description(self) -> str:
91
+ return self.client.description
92
+
93
+ @cached_property
94
+ def stac_extensions(self) -> List[str]:
95
+ return self.client.stac_extensions
96
+
97
+ @abstractmethod
98
+ def search(
99
+ self,
100
+ time: Optional[Union[TimeRange, List[TimeRange]]] = None,
101
+ bounds: Optional[Bounds] = None,
102
+ area: Optional[BaseGeometry] = None,
103
+ query: Optional[str] = None,
104
+ search_kwargs: Optional[Dict[str, Any]] = None,
105
+ ) -> Generator[Item, None, None]: ...
106
+
107
+
108
+ class StaticCollectionWriterMixin(CollectionSearcher):
109
+ # client: Client
110
+ # id: str
111
+ # description: str
112
+ # stac_extensions: List[str]
113
+
114
+ def write_static_catalog(
115
+ self,
116
+ output_path: MPathLike,
117
+ bounds: Optional[Bounds] = None,
118
+ area: Optional[BaseGeometry] = None,
119
+ time: Optional[TimeRange] = None,
120
+ search_kwargs: Optional[Dict[str, Any]] = None,
121
+ name: Optional[str] = None,
122
+ description: Optional[str] = None,
123
+ assets: Optional[List[str]] = None,
124
+ assets_dst_resolution: Union[None, float, int] = None,
125
+ assets_convert_profile: Optional[Profile] = None,
126
+ copy_metadata: bool = False,
127
+ metadata_parser_classes: Optional[tuple] = None,
128
+ overwrite: bool = False,
129
+ stac_io: DefaultStacIO = FSSpecStacIO(),
130
+ progress_callback: Optional[Callable] = None,
131
+ ) -> MPath:
132
+ """
133
+ Export a static STAC catalog from the search results.
134
+
135
+ Args:
136
+ output_path: Destination directory for the static catalog.
137
+ bounds: Spatial filter bounds.
138
+ area: Spatial filter geometry.
139
+ time: Temporal filter range.
140
+ search_kwargs: Additional search arguments.
141
+ name: Catalog name.
142
+ description: Catalog description.
143
+ assets: List of assets to download.
144
+ assets_dst_resolution: Sub-sampling resolution for assets.
145
+ assets_convert_profile: Output profile for assets (e.g. for COG conversion).
146
+ copy_metadata: Whether to copy sidecar metadata files.
147
+ metadata_parser_classes: Custom parser classes for metadata.
148
+ overwrite: Overwrite existing files.
149
+ stac_io: Custom STAC IO implementation.
150
+ progress_callback: Optional function for progress reporting.
151
+
152
+ Returns:
153
+ MPath: Path to the generated catalog.json.
154
+ """
155
+ output_path = MPath.from_inp(output_path)
156
+ assets = assets or []
157
+ # initialize catalog
158
+ catalog_json = output_path / "catalog.json"
159
+ if catalog_json.exists():
160
+ logger.debug("open existing catalog %s", str(catalog_json))
161
+ catalog = Catalog.from_file(catalog_json)
162
+ # client = Client.from_file(catalog_json)
163
+ # existing_collection = client.get_collection(self.id)
164
+ else:
165
+ # existing_collections = []
166
+ catalog = Catalog(
167
+ name or f"{self.id}",
168
+ description or f"Static subset of {self.description}",
169
+ stac_extensions=self.stac_extensions,
170
+ href=str(catalog_json),
171
+ catalog_type=CatalogType.SELF_CONTAINED,
172
+ )
173
+ src_items = list(
174
+ self.search(
175
+ time=time, bounds=bounds, area=area, search_kwargs=search_kwargs
176
+ )
177
+ )
178
+ # collect all items and download assets if required
179
+ items: List[Item] = []
180
+ item_ids = set()
181
+ for n, item in enumerate(src_items, 1):
182
+ logger.debug("found item %s", item)
183
+ item = item.clone()
184
+ if assets:
185
+ logger.debug("get assets %s", assets)
186
+ item = get_assets(
187
+ item,
188
+ assets,
189
+ output_path / self.id / item.id,
190
+ resolution=assets_dst_resolution,
191
+ convert_profile=assets_convert_profile,
192
+ overwrite=overwrite,
193
+ ignore_if_exists=True,
194
+ )
195
+ if copy_metadata:
196
+ item = get_metadata_assets(
197
+ item,
198
+ output_path / self.id / item.id,
199
+ metadata_parser_classes=metadata_parser_classes,
200
+ resolution=assets_dst_resolution,
201
+ convert_profile=assets_convert_profile,
202
+ overwrite=overwrite,
203
+ )
204
+ # this has to be set to None, otherwise pystac will mess up the asset paths
205
+ # after normalizing
206
+ item.set_self_href(None)
207
+
208
+ items.append(item)
209
+ item_ids.add(item.id)
210
+
211
+ if progress_callback:
212
+ progress_callback(n=n, total=len(src_items))
213
+
214
+ # for existing_collection in existing_collections:
215
+ # if existing_collection.id == collection.id:
216
+ # logger.debug("try to find unregistered items in collection")
217
+ # collection_root_path = MPath.from_inp(
218
+ # existing_collection.get_self_href()
219
+ # ).parent
220
+ # for subpath in collection_root_path.ls():
221
+ # if subpath.is_directory():
222
+ # try:
223
+ # item = Item.from_file(
224
+ # subpath / subpath.with_suffix(".json").name
225
+ # )
226
+ # if item.id not in item_ids:
227
+ # logger.debug(
228
+ # "add existing item with id %s", item.id
229
+ # )
230
+ # items.append(item)
231
+ # item_ids.add(item.id)
232
+ # except FileNotFoundError:
233
+ # pass
234
+ # break
235
+ # create collection and copy metadata
236
+ logger.debug("create new collection")
237
+ out_collection = Collection(
238
+ id=self.id,
239
+ extent=Extent.from_items(items),
240
+ description=self.description,
241
+ title=self.client.title,
242
+ stac_extensions=self.stac_extensions,
243
+ license=self.client.license,
244
+ keywords=self.client.keywords,
245
+ providers=self.client.providers,
246
+ summaries=self.client.summaries,
247
+ extra_fields=self.client.extra_fields,
248
+ catalog_type=CatalogType.SELF_CONTAINED,
249
+ )
250
+
251
+ # finally, add all items to collection
252
+ for item in items:
253
+ out_collection.add_item(item)
254
+
255
+ out_collection.update_extent_from_items()
256
+
257
+ catalog.add_child(out_collection)
258
+
259
+ logger.debug("write catalog to %s", output_path)
260
+ catalog.normalize_hrefs(str(output_path))
261
+ catalog.make_all_asset_hrefs_relative()
262
+ catalog.save(dest_href=str(output_path), stac_io=stac_io)
263
+
264
+ return catalog_json
265
+
266
+
267
+ def filter_items(
268
+ items: Generator[Item, None, None],
269
+ query: Optional[str] = None,
270
+ ) -> Generator[Item, None, None]:
271
+ """
272
+ Only for cloudcover now, this can and should be adapted for filter field and value
273
+ the field and value for the item filter would be defined in search.config.py corresponding configs
274
+ and passed down to the individual search approaches via said config and this Function.
275
+ """
276
+ if query:
277
+ ast = parse_ecql(query)
278
+ evaluator = NativeEvaluator(use_getattr=False)
279
+ filter_func = evaluator.evaluate(ast)
280
+ for item in items:
281
+ # pystac items store metadata in 'properties'
282
+ if filter_func(item.properties):
283
+ yield item
284
+ else:
285
+ yield from items