mapchete-eo 2025.10.0__py2.py3-none-any.whl → 2025.10.1__py2.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.
mapchete_eo/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.10.0"
1
+ __version__ = "2025.10.1"
@@ -2,6 +2,7 @@ from typing import List, Optional, Union
2
2
 
3
3
  import numpy as np
4
4
  import numpy.ma as ma
5
+ from numpy.typing import DTypeLike
5
6
  import xarray as xr
6
7
  from mapchete.types import NodataVal
7
8
 
@@ -19,7 +20,9 @@ _NUMPY_FLOAT_DTYPES = [
19
20
 
20
21
 
21
22
  def to_masked_array(
22
- xarr: Union[xr.Dataset, xr.DataArray], copy: bool = False
23
+ xarr: Union[xr.Dataset, xr.DataArray],
24
+ copy: bool = False,
25
+ out_dtype: Optional[DTypeLike] = None,
23
26
  ) -> ma.MaskedArray:
24
27
  """Convert xr.DataArray to ma.MaskedArray."""
25
28
  if isinstance(xarr, xr.Dataset):
@@ -31,6 +34,9 @@ def to_masked_array(
31
34
  "Cannot create masked_array because DataArray fill value is None"
32
35
  )
33
36
 
37
+ if out_dtype:
38
+ xarr = xarr.astype(out_dtype, copy=False)
39
+
34
40
  if xarr.dtype in _NUMPY_FLOAT_DTYPES:
35
41
  return ma.masked_values(xarr, fill_value, copy=copy, shrink=False)
36
42
  else:
mapchete_eo/base.py CHANGED
@@ -6,6 +6,7 @@ from typing import Any, Callable, List, Optional, Type, Union
6
6
 
7
7
  import croniter
8
8
  from mapchete import Bounds
9
+ import numpy as np
9
10
  import numpy.ma as ma
10
11
  import xarray as xr
11
12
  from dateutil.tz import tzutc
@@ -18,6 +19,8 @@ from mapchete.tile import BufferedTile
18
19
  from mapchete.types import MPathLike, NodataVal, NodataVals
19
20
  from pydantic import BaseModel
20
21
  from rasterio.enums import Resampling
22
+ from rasterio.features import geometry_mask
23
+ from shapely.geometry import mapping
21
24
  from shapely.geometry.base import BaseGeometry
22
25
 
23
26
  from mapchete_eo.archives.base import Archive
@@ -62,6 +65,7 @@ class EODataCube(base.InputTile):
62
65
  eo_bands: dict
63
66
  time: List[TimeRange]
64
67
  area: BaseGeometry
68
+ area_pixelbuffer: int = 0
65
69
 
66
70
  def __init__(
67
71
  self,
@@ -367,6 +371,29 @@ class EODataCube(base.InputTile):
367
371
  nodatavals=nodatavals,
368
372
  merge_products_by=merge_products_by,
369
373
  merge_method=merge_method,
374
+ read_mask=self.get_read_mask(),
375
+ )
376
+
377
+ def get_read_mask(self) -> np.ndarray:
378
+ """
379
+ Determine read mask according to input area.
380
+
381
+ This will generate a numpy array where pixel overlapping the input area
382
+ are set True and thus will get filled by the read function. Pixel outside
383
+ of the area are not considered for reading.
384
+
385
+ On staged reading, i.e. first checking the product masks to assess valid
386
+ pixels, this will avoid reading product bands in cases the product only covers
387
+ pixels outside of the intended reading area.
388
+ """
389
+ area = self.area.buffer(self.area_pixelbuffer * self.tile.pixel_x_size)
390
+ if area.is_empty:
391
+ return np.zeros((self.tile.shape), dtype=bool)
392
+ return geometry_mask(
393
+ geometries=[mapping(area)],
394
+ out_shape=self.tile.shape,
395
+ transform=self.tile.transform,
396
+ invert=True,
370
397
  )
371
398
 
372
399
 
@@ -443,8 +470,9 @@ class InputData(base.InputData):
443
470
  input_params.get("delimiters", {}).get("bounds"),
444
471
  crs=getattr(input_params.get("pyramid"), "crs"),
445
472
  ),
473
+ raise_if_empty=False,
446
474
  )
447
- return process_area.intersection(
475
+ process_area = process_area.intersection(
448
476
  reproject_geometry(
449
477
  configured_area,
450
478
  src_crs=configured_area_crs or self.crs,
mapchete_eo/io/items.py CHANGED
@@ -56,7 +56,7 @@ def item_to_np_array(
56
56
  return out
57
57
 
58
58
 
59
- def expand_params(param, length):
59
+ def expand_params(param: Any, length: int) -> List[Any]:
60
60
  """
61
61
  Expand parameters if they are not a list.
62
62
  """
@@ -104,8 +104,10 @@ def get_item_property(
104
104
  | ``collection`` | The collection ID of an Item's collection. |
105
105
  +--------------------+--------------------------------------------------------+
106
106
  """
107
- if property in ["year", "month", "day", "date", "datetime"]:
108
- if item.datetime is None:
107
+ if property == "id":
108
+ return item.id
109
+ elif property in ["year", "month", "day", "date", "datetime"]:
110
+ if item.datetime is None: # pragma: no cover
109
111
  raise ValueError(
110
112
  f"STAC item has no datetime attached, thus cannot get property {property}"
111
113
  )
@@ -40,27 +40,50 @@ def read_levelled_cube_to_np_array(
40
40
  raise_empty: bool = True,
41
41
  out_dtype: DTypeLike = np.uint16,
42
42
  out_fill_value: NodataVal = 0,
43
+ read_mask: Optional[np.ndarray] = None,
43
44
  ) -> ma.MaskedArray:
44
45
  """
45
46
  Read products as slices into a cube by filling up nodata gaps with next slice.
47
+
48
+ If a read_mask is provided, only the pixels marked True are considered to be read.
46
49
  """
47
- if len(products) == 0:
50
+ if len(products) == 0: # pragma: no cover
48
51
  raise NoSourceProducts("no products to read")
49
-
50
52
  bands = assets or eo_bands
51
- if bands is None:
53
+ if bands is None: # pragma: no cover
52
54
  raise ValueError("either assets or eo_bands have to be set")
53
-
54
55
  out_shape = (target_height, len(bands), *grid.shape)
56
+
57
+ # 2D read_mask shape
58
+ if read_mask is None:
59
+ read_mask = np.ones(grid.shape, dtype=bool)
60
+ elif read_mask.ndim != 2: # pragma: no cover
61
+ raise ValueError(
62
+ "read_mask must be 2-dimensional, not %s-dimensional",
63
+ read_mask.ndim,
64
+ )
55
65
  out: ma.MaskedArray = ma.masked_array(
56
- data=np.zeros(out_shape, dtype=out_dtype),
57
- mask=np.ones(out_shape, dtype=out_dtype),
66
+ data=np.full(out_shape, out_fill_value, dtype=out_dtype),
67
+ mask=np.ones(out_shape, dtype=bool),
58
68
  fill_value=out_fill_value,
59
69
  )
70
+
71
+ if not read_mask.any():
72
+ logger.debug("nothing to read")
73
+ return out
74
+
75
+ # extrude mask to match each layer
76
+ layer_read_mask = np.stack([read_mask for _ in bands])
77
+
78
+ def _cube_read_mask() -> np.ndarray:
79
+ # This is only needed for debug output, thus there is no need to materialize always
80
+ return np.stack([layer_read_mask for _ in range(target_height)])
81
+
60
82
  logger.debug(
61
- "empty cube with shape %s has %s",
83
+ "empty cube with shape %s has %s and %s pixels to be filled",
62
84
  out.shape,
63
85
  pretty_bytes(out.size * out.itemsize),
86
+ _cube_read_mask().sum(),
64
87
  )
65
88
 
66
89
  logger.debug("sort products into slices ...")
@@ -76,25 +99,25 @@ def read_levelled_cube_to_np_array(
76
99
  slices_read_count, slices_skip_count = 0, 0
77
100
 
78
101
  # pick slices one by one
79
- for slice_count, slice in enumerate(slices, 1):
102
+ for slice_count, slice_ in enumerate(slices, 1):
80
103
  # all filled up? let's get outta here!
81
104
  if not out.mask.any():
82
- logger.debug("cube is full, quitting!")
105
+ logger.debug("cube has no pixels to be filled, quitting!")
83
106
  break
84
107
 
85
108
  # generate 2D mask of holes to be filled in output cube
86
- cube_nodata_mask = out.mask.any(axis=0).any(axis=0)
109
+ cube_nodata_mask = np.logical_and(out.mask.any(axis=0).any(axis=0), read_mask)
87
110
 
88
111
  # read slice
89
112
  try:
90
113
  logger.debug(
91
114
  "see if slice %s %s has some of the %s unmasked pixels for cube",
92
115
  slice_count,
93
- slice,
116
+ slice_,
94
117
  cube_nodata_mask.sum(),
95
118
  )
96
- with slice.cached():
97
- slice_array = slice.read(
119
+ with slice_.cached():
120
+ slice_array = slice_.read(
98
121
  merge_method=merge_method,
99
122
  product_read_kwargs=dict(
100
123
  product_read_kwargs,
@@ -104,17 +127,18 @@ def read_levelled_cube_to_np_array(
104
127
  resampling=resampling,
105
128
  nodatavals=nodatavals,
106
129
  raise_empty=raise_empty,
107
- target_mask=~cube_nodata_mask.copy(),
130
+ read_mask=cube_nodata_mask.copy(),
131
+ out_dtype=out_dtype,
108
132
  ),
109
133
  )
110
134
  slices_read_count += 1
111
135
  except (EmptySliceException, CorruptedSlice) as exc:
112
- logger.debug("skipped slice %s: %s", slice, str(exc))
136
+ logger.debug("skipped slice %s: %s", slice_, str(exc))
113
137
  slices_skip_count += 1
114
138
  continue
115
139
 
116
140
  # if slice was not empty, fill pixels into cube
117
- logger.debug("add slice %s array to cube", slice)
141
+ logger.debug("add slice %s array to cube", slice_)
118
142
 
119
143
  # iterate through layers of cube
120
144
  for layer_index in range(target_height):
@@ -124,34 +148,35 @@ def read_levelled_cube_to_np_array(
124
148
  continue
125
149
 
126
150
  # determine empty patches of current layer
127
- empty_patches = out[layer_index].mask.copy()
128
- pixels_for_layer = (~slice_array[empty_patches].mask).sum()
151
+ empty_patches = np.logical_and(out[layer_index].mask, layer_read_mask)
152
+ remaining_pixels_for_layer = (~slice_array[empty_patches].mask).sum()
129
153
 
130
154
  # when slice has nothing to offer for this layer, skip
131
- if pixels_for_layer == 0:
155
+ if remaining_pixels_for_layer == 0:
132
156
  logger.debug(
133
157
  "layer %s: slice has no pixels for this layer, jump to next",
134
158
  layer_index,
135
159
  )
136
160
  continue
137
161
 
162
+ # insert slice data into empty patches of layer
138
163
  logger.debug(
139
164
  "layer %s: fill with %s pixels ...",
140
165
  layer_index,
141
- pixels_for_layer,
166
+ remaining_pixels_for_layer,
142
167
  )
143
- # insert slice data into empty patches of layer
144
168
  out[layer_index][empty_patches] = slice_array[empty_patches]
145
- masked_pixels = out[layer_index].mask.sum()
146
- total_pixels = out[layer_index].size
147
- percent_full = round(
148
- 100 * ((total_pixels - masked_pixels) / total_pixels), 2
149
- )
169
+
170
+ # report on layer fill status
150
171
  logger.debug(
151
- "layer %s: %s%% filled (%s empty pixels remaining)",
172
+ "layer %s: %s",
152
173
  layer_index,
153
- percent_full,
154
- out[layer_index].mask.sum(),
174
+ _percent_full(
175
+ remaining=np.logical_and(
176
+ out[layer_index].mask, layer_read_mask
177
+ ).sum(),
178
+ total=layer_read_mask.sum(),
179
+ ),
155
180
  )
156
181
 
157
182
  # remove slice values which were just inserted for next layer
@@ -161,13 +186,13 @@ def read_levelled_cube_to_np_array(
161
186
  logger.debug("slice fully inserted into cube, skipping")
162
187
  break
163
188
 
164
- masked_pixels = out.mask.sum()
165
- total_pixels = out.size
166
- percent_full = round(100 * ((total_pixels - masked_pixels) / total_pixels), 2)
189
+ # report on layer fill status
167
190
  logger.debug(
168
- "cube is %s%% filled (%s empty pixels remaining)",
169
- percent_full,
170
- masked_pixels,
191
+ "cube is %s",
192
+ _percent_full(
193
+ remaining=np.logical_and(out.mask, _cube_read_mask()).sum(),
194
+ total=_cube_read_mask().sum(),
195
+ ),
171
196
  )
172
197
 
173
198
  logger.debug(
@@ -197,6 +222,7 @@ def read_levelled_cube_to_xarray(
197
222
  band_axis_name: str = "bands",
198
223
  x_axis_name: str = "x",
199
224
  y_axis_name: str = "y",
225
+ read_mask: Optional[np.ndarray] = None,
200
226
  ) -> xr.Dataset:
201
227
  """
202
228
  Read products as slices into a cube by filling up nodata gaps with next slice.
@@ -218,6 +244,7 @@ def read_levelled_cube_to_xarray(
218
244
  sort=sort,
219
245
  product_read_kwargs=product_read_kwargs,
220
246
  raise_empty=raise_empty,
247
+ read_mask=read_mask,
221
248
  ),
222
249
  slice_names=[f"layer-{ii}" for ii in range(target_height)],
223
250
  band_names=variables,
@@ -226,3 +253,7 @@ def read_levelled_cube_to_xarray(
226
253
  x_axis_name=x_axis_name,
227
254
  y_axis_name=y_axis_name,
228
255
  )
256
+
257
+
258
+ def _percent_full(remaining: int, total: int, ndigits: int = 2) -> str:
259
+ return f"{round(100 * (total - remaining) / total, ndigits=ndigits)}% full ({remaining} remaining emtpy pixels)"
@@ -10,6 +10,7 @@ from typing import Any, Dict, Generator, Iterator, List, Optional, Sequence
10
10
  from mapchete import Timer
11
11
  import numpy as np
12
12
  import numpy.ma as ma
13
+ from numpy.typing import DTypeLike
13
14
  import xarray as xr
14
15
  from mapchete.config import get_hash
15
16
  from mapchete.geometry import to_shape
@@ -49,11 +50,13 @@ def products_to_np_array(
49
50
  sort: Optional[SortMethodConfig] = None,
50
51
  product_read_kwargs: dict = {},
51
52
  raise_empty: bool = True,
53
+ out_dtype: Optional[DTypeLike] = None,
54
+ read_mask: Optional[np.ndarray] = None,
52
55
  ) -> ma.MaskedArray:
53
56
  """Read grid window of EOProducts and merge into a 4D xarray."""
54
57
  return ma.stack(
55
58
  [
56
- to_masked_array(s)
59
+ to_masked_array(s, out_dtype=out_dtype)
57
60
  for s in generate_slice_dataarrays(
58
61
  products=products,
59
62
  assets=assets,
@@ -66,6 +69,7 @@ def products_to_np_array(
66
69
  sort=sort,
67
70
  product_read_kwargs=product_read_kwargs,
68
71
  raise_empty=raise_empty,
72
+ read_mask=read_mask,
69
73
  )
70
74
  ]
71
75
  )
@@ -87,6 +91,7 @@ def products_to_xarray(
87
91
  sort: Optional[SortMethodConfig] = None,
88
92
  raise_empty: bool = True,
89
93
  product_read_kwargs: dict = {},
94
+ read_mask: Optional[np.ndarray] = None,
90
95
  ) -> xr.Dataset:
91
96
  """Read grid window of EOProducts and merge into a 4D xarray."""
92
97
  data_vars = [
@@ -103,6 +108,7 @@ def products_to_xarray(
103
108
  sort=sort,
104
109
  product_read_kwargs=product_read_kwargs,
105
110
  raise_empty=raise_empty,
111
+ read_mask=read_mask,
106
112
  )
107
113
  ]
108
114
  if merge_products_by and merge_products_by not in ["date", "datetime"]:
@@ -322,8 +328,11 @@ def merge_products(
322
328
  valid_arrays = [a for a in arrays if not ma.getmaskarray(a).all()]
323
329
 
324
330
  if valid_arrays:
325
- stacked = ma.stack(valid_arrays, dtype=out.dtype)
326
- out = stacked.mean(axis=0, dtype=out.dtype)
331
+ out_dtype = out.dtype
332
+ out_fill_value = out.fill_value
333
+ stacked = ma.stack(valid_arrays, dtype=out_dtype)
334
+ out = stacked.mean(axis=0, dtype=out_dtype).astype(out_dtype, copy=False)
335
+ out.set_fill_value(out_fill_value)
327
336
  else:
328
337
  # All arrays were fully masked — return fully masked output
329
338
  out = ma.masked_all(out.shape, dtype=out.dtype)
@@ -351,10 +360,12 @@ def generate_slice_dataarrays(
351
360
  sort: Optional[SortMethodConfig] = None,
352
361
  product_read_kwargs: dict = {},
353
362
  raise_empty: bool = True,
363
+ read_mask: Optional[np.ndarray] = None,
354
364
  ) -> Iterator[xr.DataArray]:
355
365
  """
356
366
  Yield products or merged products into slices as DataArrays.
357
367
  """
368
+
358
369
  if len(products) == 0:
359
370
  raise NoSourceProducts("no products to read")
360
371
 
@@ -396,6 +407,7 @@ def generate_slice_dataarrays(
396
407
  resampling=resampling,
397
408
  nodatavals=nodatavals,
398
409
  raise_empty=raise_empty,
410
+ read_mask=read_mask,
399
411
  ),
400
412
  raise_empty=raise_empty,
401
413
  ),
@@ -161,10 +161,8 @@ class S2Metadata:
161
161
  return f"<S2Metadata id={self.product_id}, processing_baseline={self.processing_baseline}>"
162
162
 
163
163
  def clear_cached_data(self):
164
- logger.debug("clear S2Metadata internal caches")
165
164
  self._cache = dict(viewing_incidence_angles=dict(), detector_footprints=dict())
166
165
  if self._cached_xml_root is not None:
167
- logger.debug("clear S2Metadata xml cache")
168
166
  self._cached_xml_root.clear()
169
167
  self._cached_xml_root = None
170
168
  self.path_mapper.clear_cached_data()
@@ -195,7 +195,6 @@ class S2Product(EOProduct, EOProductProtocol):
195
195
  return f"<S2Product product_id={self.id}>"
196
196
 
197
197
  def clear_cached_data(self):
198
- logger.debug("clear S2Product caches")
199
198
  if self._metadata is not None:
200
199
  self._metadata.clear_cached_data()
201
200
  self._metadata = None
@@ -215,7 +214,7 @@ class S2Product(EOProduct, EOProductProtocol):
215
214
  mask_config: MaskConfig = MaskConfig(),
216
215
  brdf_config: Optional[BRDFConfig] = None,
217
216
  fill_value: int = 0,
218
- target_mask: Optional[np.ndarray] = None,
217
+ read_mask: Optional[np.ndarray] = None,
219
218
  **kwargs,
220
219
  ) -> ma.MaskedArray:
221
220
  assets = assets or []
@@ -228,7 +227,9 @@ class S2Product(EOProduct, EOProductProtocol):
228
227
  count = len(assets)
229
228
  if isinstance(grid, Resolution):
230
229
  grid = self.metadata.grid(grid)
231
- mask = self.get_mask(grid, mask_config, target_mask=target_mask).data
230
+ mask = self.get_mask(
231
+ grid, mask_config, target_mask=None if read_mask is None else ~read_mask
232
+ ).data
232
233
  if nodatavals is None:
233
234
  nodatavals = fill_value
234
235
  elif fill_value is None and nodatavals is not None:
@@ -464,13 +465,12 @@ class S2Product(EOProduct, EOProductProtocol):
464
465
  if isinstance(grid, Resolution)
465
466
  else Grid.from_obj(grid)
466
467
  )
467
-
468
468
  if target_mask is None:
469
469
  target_mask = np.zeros(shape=grid.shape, dtype=bool)
470
470
  else:
471
471
  if target_mask.shape != grid.shape:
472
472
  raise ValueError("a target mask must have the same shape as the grid")
473
- logger.debug("got custom target mask to start with: %s", target_mask)
473
+ logger.debug("got custom target mask to start with: %s", target_mask.shape)
474
474
 
475
475
  def _check_full(arr):
476
476
  # ATTENTION: target_mask and out have to be combined *after* mask was buffered!
@@ -181,15 +181,19 @@ def gradient_merge(
181
181
  # footprint coverage)
182
182
  # set 1 to 0:
183
183
  gradient_1band[gradient_1band == 1] = 0
184
- logger.debug(f"gradient_1band: {gradient_1band}")
184
+ logger.debug(
185
+ f"gradient_1band; min: {np.min(gradient_1band)}, max: {np.max(gradient_1band)}"
186
+ )
185
187
 
186
188
  # extrude array to match number of raster bands
187
189
  gradient_8bit = np.stack([gradient_1band for _ in range(raster.shape[0])])
188
- logger.debug(f"gradient_8bit: {gradient_8bit}")
190
+ logger.debug(
191
+ f"gradient_8bit; min: {np.min(gradient_8bit)}, max: {np.max(gradient_8bit)}"
192
+ )
189
193
 
190
194
  # scale gradient from 0 to 1
191
195
  gradient = gradient_8bit / 255
192
- logger.debug(f"gradient: {gradient}")
196
+ logger.debug(f"gradient; min: {np.min(gradient)} , max: {np.max(gradient)}")
193
197
 
194
198
  # now only apply the gradient where out and raster have values
195
199
  # otherwise pick the remaining existing value or keep a masked
mapchete_eo/product.py CHANGED
@@ -113,7 +113,6 @@ class EOProduct(EOProductProtocol):
113
113
  nodatavals: NodataVals = None,
114
114
  raise_empty: bool = True,
115
115
  apply_offset: bool = True,
116
- apply_scale: bool = False,
117
116
  **kwargs,
118
117
  ) -> ma.MaskedArray:
119
118
  assets = assets or []
@@ -1,3 +1,4 @@
1
+ from functools import cached_property
1
2
  import json
2
3
  import logging
3
4
  from abc import ABC, abstractmethod
@@ -48,13 +49,25 @@ class CatalogSearcher(ABC):
48
49
  This class serves as a bridge between an Archive and a catalog implementation.
49
50
  """
50
51
 
51
- eo_bands: List[str]
52
- id: str
53
- description: str
54
- stac_extensions: List[str]
55
52
  collections: List[str]
56
53
  config_cls: Type[BaseModel]
57
54
 
55
+ @abstractmethod
56
+ @cached_property
57
+ def eo_bands(self) -> List[str]: ...
58
+
59
+ @abstractmethod
60
+ @cached_property
61
+ def id(self) -> str: ...
62
+
63
+ @abstractmethod
64
+ @cached_property
65
+ def description(self) -> str: ...
66
+
67
+ @abstractmethod
68
+ @cached_property
69
+ def stac_extensions(self) -> List[str]: ...
70
+
58
71
  @abstractmethod
59
72
  def search(
60
73
  self,
@@ -66,10 +79,10 @@ class CatalogSearcher(ABC):
66
79
 
67
80
 
68
81
  class StaticCatalogWriterMixin(CatalogSearcher):
69
- client: Client
70
- id: str
71
- description: str
72
- stac_extensions: List[str]
82
+ # client: Client
83
+ # id: str
84
+ # description: str
85
+ # stac_extensions: List[str]
73
86
 
74
87
  @abstractmethod
75
88
  def get_collections(self) -> List[Collection]: # pragma: no cover
@@ -36,17 +36,34 @@ class STACSearchCatalog(StaticCatalogWriterMixin, CatalogSearcher):
36
36
  stac_item_modifiers: Optional[List[Callable[[Item], Item]]] = None,
37
37
  endpoint: Optional[MPathLike] = None,
38
38
  ):
39
+ if endpoint is not None:
40
+ self.endpoint = endpoint
39
41
  if collections:
40
42
  self.collections = collections
41
43
  else: # pragma: no cover
42
44
  raise ValueError("collections must be given")
43
- self.client = Client.open(endpoint or self.endpoint)
44
- self.id = self.client.id
45
- self.description = self.client.description
46
- self.stac_extensions = self.client.stac_extensions
47
- self.eo_bands = self._eo_bands()
48
45
  self.stac_item_modifiers = stac_item_modifiers
49
46
 
47
+ @cached_property
48
+ def client(self) -> Client:
49
+ return Client.open(self.endpoint)
50
+
51
+ @cached_property
52
+ def eo_bands(self) -> List[str]:
53
+ return self._eo_bands()
54
+
55
+ @cached_property
56
+ def id(self) -> str:
57
+ return self.client.id
58
+
59
+ @cached_property
60
+ def description(self) -> str:
61
+ return self.client.description
62
+
63
+ @cached_property
64
+ def stac_extensions(self) -> List[str]:
65
+ return self.client.stac_extensions
66
+
50
67
  def search(
51
68
  self,
52
69
  time: Optional[Union[TimeRange, List[TimeRange]]] = None,
@@ -1,3 +1,4 @@
1
+ from functools import cached_property
1
2
  import logging
2
3
  import warnings
3
4
  from typing import Any, Callable, Dict, Generator, List, Optional, Union
@@ -37,13 +38,25 @@ class STACStaticCatalog(StaticCatalogWriterMixin, CatalogSearcher):
37
38
  stac_item_modifiers: Optional[List[Callable[[Item], Item]]] = None,
38
39
  ):
39
40
  self.client = Client.from_file(str(baseurl), stac_io=FSSpecStacIO())
40
- self.id = self.client.id
41
- self.description = self.client.description
42
- self.stac_extensions = self.client.stac_extensions
43
41
  self.collections = [c.id for c in self.client.get_children()]
44
- self.eo_bands = self._eo_bands()
45
42
  self.stac_item_modifiers = stac_item_modifiers
46
43
 
44
+ @cached_property
45
+ def eo_bands(self) -> List[str]:
46
+ return self._eo_bands()
47
+
48
+ @cached_property
49
+ def id(self) -> str:
50
+ return self.client.id
51
+
52
+ @cached_property
53
+ def description(self) -> str:
54
+ return self.client.description
55
+
56
+ @cached_property
57
+ def stac_extensions(self) -> List[str]:
58
+ return self.client.stac_extensions
59
+
47
60
  def search(
48
61
  self,
49
62
  time: Optional[Union[TimeRange, List[TimeRange]]] = None,
@@ -1,4 +1,5 @@
1
1
  import datetime
2
+ from functools import cached_property
2
3
  import logging
3
4
  from typing import Any, Callable, Dict, Generator, List, Optional, Set, Union
4
5
 
@@ -51,9 +52,12 @@ class UTMSearchCatalog(StaticCatalogWriterMixin, CatalogSearcher):
51
52
  if len(collections) == 0: # pragma: no cover
52
53
  raise ValueError("no collections provided")
53
54
  self.collections = collections
54
- self.eo_bands = self._eo_bands()
55
55
  self.stac_item_modifiers = stac_item_modifiers
56
56
 
57
+ @cached_property
58
+ def eo_bands(self) -> List[str]: # pragma: no cover
59
+ return self._eo_bands()
60
+
57
61
  def search(
58
62
  self,
59
63
  time: Optional[Union[TimeRange, List[TimeRange]]] = None,
mapchete_eo/sort.py CHANGED
@@ -5,7 +5,9 @@ This module holds all code required to sort products or slices.
5
5
  from typing import Callable, List, Optional
6
6
 
7
7
  from pydantic import BaseModel
8
+ from pystac import Item
8
9
 
10
+ from mapchete_eo.io.items import get_item_property
9
11
  from mapchete_eo.protocols import DateTimeProtocol
10
12
  from mapchete_eo.time import timedelta, to_datetime
11
13
  from mapchete_eo.types import DateTimeLike
@@ -22,7 +24,7 @@ def sort_objects_by_target_date(
22
24
  **kwargs,
23
25
  ) -> List[DateTimeProtocol]:
24
26
  """
25
- Return sorted list of onjects according to their distance to the target_date.
27
+ Return sorted list of objects according to their distance to the target_date.
26
28
 
27
29
  Default for target date is the middle between the objects start date and end date.
28
30
  """
@@ -46,3 +48,17 @@ class TargetDateSort(SortMethodConfig):
46
48
  func: Callable = sort_objects_by_target_date
47
49
  target_date: Optional[DateTimeLike] = None
48
50
  reverse: bool = False
51
+
52
+
53
+ def sort_objects_by_cloud_cover(
54
+ objects: List[Item], reverse: bool = False
55
+ ) -> List[Item]:
56
+ if len(objects) == 0: # pragma: no cover
57
+ return objects
58
+ objects.sort(key=lambda x: get_item_property(x, "eo:cloud_cover"), reverse=reverse)
59
+ return objects
60
+
61
+
62
+ class CloudCoverSort(SortMethodConfig):
63
+ func: Callable = sort_objects_by_cloud_cover
64
+ reverse: bool = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapchete-eo
3
- Version: 2025.10.0
3
+ Version: 2025.10.1
4
4
  Summary: mapchete EO data reader
5
5
  Project-URL: Homepage, https://gitlab.eox.at/maps/mapchete_eo
6
6
  Author-email: Joachim Ungar <joachim.ungar@eox.at>, Petr Sevcik <petr.sevcik@eox.at>
@@ -1,14 +1,14 @@
1
- mapchete_eo/__init__.py,sha256=HtoUGacYQ9lSC7EssjuZ7xIyD0DlTBcOEDAUU3Qep04,26
2
- mapchete_eo/base.py,sha256=Ka1HDqKeeTZYgnhW2LFpgR1AaI9buXCrbNztXWkxU78,19164
1
+ mapchete_eo/__init__.py,sha256=r2vzraIeM6daquybaheJf__iYb9hpxuEfM5uy2Ygulg,26
2
+ mapchete_eo/base.py,sha256=G4DFwU9AjzWLfF_8hWPKmjsBhpwtJxQ_Z31-uZjAeFc,20294
3
3
  mapchete_eo/blacklist.txt,sha256=6KhBY0jNjXgRpKRvKJoOTswvNUoP56IrIcNeCYnd2qo,17471
4
4
  mapchete_eo/eostac.py,sha256=5K08Mr4wm-VOXTEvTB6DQHX7rbFLYw8mjW1wlpPltC0,574
5
5
  mapchete_eo/exceptions.py,sha256=ul7_9o6_SfJXQPsDOQqRBhh09xv2t4AwHl6YJ7yWN5s,2150
6
6
  mapchete_eo/geometry.py,sha256=NiLeXSnp2c_AsBin8XUFzlr_ndqHbKos-dHQSDApIro,9420
7
7
  mapchete_eo/known_catalogs.py,sha256=dlxImeNwaUVuss-sGddixTcxjcrznyR-XWKfkX9S5PA,1274
8
- mapchete_eo/product.py,sha256=t7Hj_0FEIK_yboXcSm3sEUD2tpUMROLMsJBBen-MncE,9558
8
+ mapchete_eo/product.py,sha256=_KULHdTWbUw0TkRfpm8xvD2qukhuMmm9ov7cJz9aY18,9523
9
9
  mapchete_eo/protocols.py,sha256=_WxiHPgErDjiJ0dzWJiWjHm0ZgoDlG16oEw1X1fzUXc,1512
10
10
  mapchete_eo/settings.py,sha256=-i4UPX0KJ7NWGpGzOUkhzCABOMz2xc-q3sVLBFT4JAM,696
11
- mapchete_eo/sort.py,sha256=k2QgZyn4xZ2vx8MgfbAZBA5YM4P-tQOzzsQ1mSTESUc,1350
11
+ mapchete_eo/sort.py,sha256=puj3YBTL-AikC--CZwvfYCKVQWy7URMUzobf2rm9FGU,1817
12
12
  mapchete_eo/time.py,sha256=mCIrn6C-B7CDoFIIM4u8pG15nVV8KWQixF2fuhdDTdE,1799
13
13
  mapchete_eo/types.py,sha256=yIHKZHlGCSerJzDBS2AH7yYLun4rY3rZZ73SCKiN26U,1814
14
14
  mapchete_eo/archives/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -16,7 +16,7 @@ mapchete_eo/archives/base.py,sha256=32NCFg46p95Uv3rO-M0Y-W6iCH4BzLtfUHpHfyzzrC4,
16
16
  mapchete_eo/array/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  mapchete_eo/array/buffer.py,sha256=GeeM7RVQ-Z_SKlkaeztWUrxEsa_yck-nXwXRIM3w0_I,483
18
18
  mapchete_eo/array/color.py,sha256=ArJ3-0qjJJGAlRQodLios6JwD0uHLSRRb9BwfB31v-U,810
19
- mapchete_eo/array/convert.py,sha256=LhNlTtAeZpDy2zI58zvqOmQLw9GYjvgroHMiD7Y0PiE,5238
19
+ mapchete_eo/array/convert.py,sha256=oimklMfyS2XxQLd73zQ2cr9aE-39z1UIt6ry_2ZQI10,5390
20
20
  mapchete_eo/cli/__init__.py,sha256=SRRilPKUtwsUCi1GdMNPxjIrCM-XyYM2hK1T9vvKUI8,946
21
21
  mapchete_eo/cli/bounds.py,sha256=QcOXOXMcnd8ecwiCFVWukTLUFIkpj9SbB3ot-GA4heA,551
22
22
  mapchete_eo/cli/options_arguments.py,sha256=6rnnABT8WU6DF6vvy7lGDj5-OpMMo1Vb36fL1oan56c,7344
@@ -40,10 +40,10 @@ mapchete_eo/image_operations/linear_normalization.py,sha256=-eQX3WLCUYWUv-um3s1u
40
40
  mapchete_eo/image_operations/sigmoidal.py,sha256=IKU8F89HhQJWGUVmIrnkuhqzn_ztlGrTf8xXZaVQWzU,3575
41
41
  mapchete_eo/io/__init__.py,sha256=1-1g4cESZZREvyumEUABZhDwgVuSxtxdqbNlLuVKlow,950
42
42
  mapchete_eo/io/assets.py,sha256=8OrqvrCzJOY0pExh_vKRL7Bz_RGk6M_LfayQqxHo8ag,17014
43
- mapchete_eo/io/items.py,sha256=FDDMRtFvNNL3Iwf0xiaSI8G3s-54vhqfYKo7xzCQq-w,5657
44
- mapchete_eo/io/levelled_cubes.py,sha256=Bhl4LM39eueKHW-BOT2LlXkWn7Kgdhp22mfLAkYrK9A,7782
43
+ mapchete_eo/io/items.py,sha256=7l6A5E6fDNOAx7cjvu3VDP1WlD-ufEUBzt1uBBRD80Q,5750
44
+ mapchete_eo/io/levelled_cubes.py,sha256=ZF7BLn9MHnJCCDjAoR9D7MNbHdusjJ9EACdM7rKNlyM,9018
45
45
  mapchete_eo/io/path.py,sha256=y5aYr-dE-0BafgF1_RCSXCvbF3taCI-EOz0R58rNxO8,4413
46
- mapchete_eo/io/products.py,sha256=3tsMCv3ksKL5DSIdSPN6_1e58gHXA0khzURTGuQD7TY,14121
46
+ mapchete_eo/io/products.py,sha256=2IdReyvVNY06BHUgdb7EwePqrUL0bL070c6Dnv6RhB0,14627
47
47
  mapchete_eo/io/profiles.py,sha256=l9YiXbKYs674xz6NLy5EAq3fBEvHTVSf_gopXD-CuSY,935
48
48
  mapchete_eo/platforms/sentinel2/__init__.py,sha256=zAyBzOhwKSIyNJyfEpyY6G-PxPDhLvOCky8aA2PsW_k,390
49
49
  mapchete_eo/platforms/sentinel2/archives.py,sha256=uhIY0EUta45Ka94ky35NZ33Hxi0qPA2yIu21ad1z6CQ,5830
@@ -51,10 +51,10 @@ mapchete_eo/platforms/sentinel2/bandpass_adjustment.py,sha256=DA0cQtjr8UH7r_kizA
51
51
  mapchete_eo/platforms/sentinel2/config.py,sha256=Mn_uw2bdR6gcIWPS_istY6E5HaN6JLuxQO05HtEovNU,5974
52
52
  mapchete_eo/platforms/sentinel2/driver.py,sha256=ny9Rfnvi07NeB29RwimqBIF7BbCxPPRfZO_rfDJyWzI,2776
53
53
  mapchete_eo/platforms/sentinel2/masks.py,sha256=6ig8sQhXkm1u6Bwbe7n8ewW8gTdbVJp-iaDCk780Qxg,11220
54
- mapchete_eo/platforms/sentinel2/metadata_parser.py,sha256=M1w6sdKx0u_BFxHMsjx8TsSx1v7p5C16cZc8_FOQpns,26835
54
+ mapchete_eo/platforms/sentinel2/metadata_parser.py,sha256=czh-2M_r79sWFbZETk_-0eOyvonbhaY7Hkb3tRTeF5A,26723
55
55
  mapchete_eo/platforms/sentinel2/preprocessing_tasks.py,sha256=eJh1wSDsQkEKtQ6G6vmVkFBoBl5nZUV5vNHQM4RifQg,772
56
56
  mapchete_eo/platforms/sentinel2/processing_baseline.py,sha256=B2-t5H7xC54g2aIvcdBSRQPHygfqccbKEhXSgClAADY,5085
57
- mapchete_eo/platforms/sentinel2/product.py,sha256=2FvHSS52Bqur1dn-D1RHnPeTVzqVv3XpfHWf2MeGes8,25368
57
+ mapchete_eo/platforms/sentinel2/product.py,sha256=9grX6TGCryVxS8TH6Lzc-yca-8zKzCx_WD1r3_bPK9E,25376
58
58
  mapchete_eo/platforms/sentinel2/types.py,sha256=Cd0bmDT1mLETuSJYEAoF8Kl4-QOPv2fFrwgR8xHTka0,2009
59
59
  mapchete_eo/platforms/sentinel2/brdf/__init__.py,sha256=W7zAJZ5F5Xla7j4ua2opRANUbcD3jZbhTwhW5_cpOUI,242
60
60
  mapchete_eo/platforms/sentinel2/brdf/config.py,sha256=v3WCEu4r-tEPQgWBEkYNAPLgCQobvYtQ2YiDeAdImMk,1133
@@ -73,16 +73,16 @@ mapchete_eo/processes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
73
73
  mapchete_eo/processes/config.py,sha256=WorMzHJzJaZPh-aEyEEwcXN08ALxn6zAhVMrjkDUWDU,1745
74
74
  mapchete_eo/processes/dtype_scale.py,sha256=7hJlIe2DsE6Kmk1c3PLwLr8rnn_4_9S6Cz7bmRuzP9M,4176
75
75
  mapchete_eo/processes/eo_to_xarray.py,sha256=Gcp4qju2C9S8KeUnVY5f3nAsrdckPhGRguzsgRtWBFs,514
76
- mapchete_eo/processes/merge_rasters.py,sha256=f9QG8_nIJRIVYC-hkq9hcmQOZIdoRzAWsJp1OlRS4qQ,8316
76
+ mapchete_eo/processes/merge_rasters.py,sha256=jSXUI8pEWXbQWrM9iMtrH4ACH2VkCaBUH9CQg4z6zLA,8486
77
77
  mapchete_eo/search/__init__.py,sha256=71VuR1BFFbFiV9rae5qXt0RLtwWk1DP_CYbZflMZDWA,516
78
- mapchete_eo/search/base.py,sha256=8B7bg6dj3n79lv9SalVMVjjLth12AIcTuqJjyggZLCk,8601
78
+ mapchete_eo/search/base.py,sha256=LQfCwsZW5YFZx5pvyNSpfDbcObB-UGypjfrvL-3VUZE,8883
79
79
  mapchete_eo/search/config.py,sha256=0idsjXt2T8scerlw-PzNVc0u8JxpOxHgrEO2asSRtNE,1122
80
80
  mapchete_eo/search/s2_mgrs.py,sha256=5LWl9c7WzFvXODr9f8m37cv-8yyf-LvnN0-TSSPcXKM,10868
81
- mapchete_eo/search/stac_search.py,sha256=YnISHpdvMJfrY_wfM1dHOtHYqgsbXsYLMYrRdJlAZTo,9551
82
- mapchete_eo/search/stac_static.py,sha256=P1C2OC33UopV80xIvFeL-HbOuaxxe7qsPDdnkU8IXcA,8613
83
- mapchete_eo/search/utm_search.py,sha256=wKrG2KwqL2fjSrkDUTZuRayiVAR27BtWpX-PFab0Dng,9646
84
- mapchete_eo-2025.10.0.dist-info/METADATA,sha256=IUBh6j50RcAjOSWYsJ5UPME5ahbnaFAf9H0YtLK-1sM,3237
85
- mapchete_eo-2025.10.0.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
86
- mapchete_eo-2025.10.0.dist-info/entry_points.txt,sha256=ewk6R4FGdAclOnUpikhlPZGWI40MWeksVIIwu4jVebk,324
87
- mapchete_eo-2025.10.0.dist-info/licenses/LICENSE,sha256=TC5JwvBnFrUgsSQSCDFPc3cqlbth2N0q8MWrhY1EVd0,1089
88
- mapchete_eo-2025.10.0.dist-info/RECORD,,
81
+ mapchete_eo/search/stac_search.py,sha256=TXsCSMi3PBst80_vexXv0pvY5DNcD8sgtsw3zOQtoRA,9844
82
+ mapchete_eo/search/stac_static.py,sha256=3PA1uUIQy0bHywFA3_hmyn65pjlWEqeCW4Gx2DdZMZI,8839
83
+ mapchete_eo/search/utm_search.py,sha256=__HZCw5no9_8Rb6kO1pCfCEUS_ZwmgrVwlcyE78SY6k,9754
84
+ mapchete_eo-2025.10.1.dist-info/METADATA,sha256=edkh71VZo0bygu4yQ44RIeR9CnshfudaH-HOx76lg7w,3237
85
+ mapchete_eo-2025.10.1.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
86
+ mapchete_eo-2025.10.1.dist-info/entry_points.txt,sha256=ewk6R4FGdAclOnUpikhlPZGWI40MWeksVIIwu4jVebk,324
87
+ mapchete_eo-2025.10.1.dist-info/licenses/LICENSE,sha256=TC5JwvBnFrUgsSQSCDFPc3cqlbth2N0q8MWrhY1EVd0,1089
88
+ mapchete_eo-2025.10.1.dist-info/RECORD,,