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.
- mapchete_eo/__init__.py +1 -0
- mapchete_eo/array/__init__.py +0 -0
- mapchete_eo/array/buffer.py +16 -0
- mapchete_eo/array/color.py +29 -0
- mapchete_eo/array/convert.py +163 -0
- mapchete_eo/base.py +653 -0
- mapchete_eo/blacklist.txt +175 -0
- mapchete_eo/cli/__init__.py +30 -0
- mapchete_eo/cli/bounds.py +22 -0
- mapchete_eo/cli/options_arguments.py +227 -0
- mapchete_eo/cli/s2_brdf.py +77 -0
- mapchete_eo/cli/s2_cat_results.py +130 -0
- mapchete_eo/cli/s2_find_broken_products.py +77 -0
- mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
- mapchete_eo/cli/s2_mask.py +71 -0
- mapchete_eo/cli/s2_mgrs.py +45 -0
- mapchete_eo/cli/s2_rgb.py +114 -0
- mapchete_eo/cli/s2_verify.py +129 -0
- mapchete_eo/cli/static_catalog.py +82 -0
- mapchete_eo/eostac.py +30 -0
- mapchete_eo/exceptions.py +87 -0
- mapchete_eo/image_operations/__init__.py +12 -0
- mapchete_eo/image_operations/blend_functions.py +579 -0
- mapchete_eo/image_operations/color_correction.py +136 -0
- mapchete_eo/image_operations/compositing.py +266 -0
- mapchete_eo/image_operations/dtype_scale.py +43 -0
- mapchete_eo/image_operations/fillnodata.py +130 -0
- mapchete_eo/image_operations/filters.py +319 -0
- mapchete_eo/image_operations/linear_normalization.py +81 -0
- mapchete_eo/image_operations/sigmoidal.py +114 -0
- mapchete_eo/io/__init__.py +37 -0
- mapchete_eo/io/assets.py +496 -0
- mapchete_eo/io/items.py +162 -0
- mapchete_eo/io/levelled_cubes.py +259 -0
- mapchete_eo/io/path.py +155 -0
- mapchete_eo/io/products.py +423 -0
- mapchete_eo/io/profiles.py +45 -0
- mapchete_eo/platforms/sentinel2/__init__.py +17 -0
- mapchete_eo/platforms/sentinel2/_mapper_registry.py +89 -0
- mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
- mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
- mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
- mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
- mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
- mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
- mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
- mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
- mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
- mapchete_eo/platforms/sentinel2/config.py +241 -0
- mapchete_eo/platforms/sentinel2/driver.py +43 -0
- mapchete_eo/platforms/sentinel2/masks.py +329 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/__init__.py +6 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/base.py +56 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/default_path_mapper.py +135 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/models.py +78 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/s2metadata.py +639 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/__init__.py +57 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/guessers.py +108 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/item_mappers.py +171 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/metadata_xml_mappers.py +217 -0
- mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +50 -0
- mapchete_eo/platforms/sentinel2/processing_baseline.py +163 -0
- mapchete_eo/platforms/sentinel2/product.py +747 -0
- mapchete_eo/platforms/sentinel2/source.py +114 -0
- mapchete_eo/platforms/sentinel2/types.py +114 -0
- mapchete_eo/processes/__init__.py +0 -0
- mapchete_eo/processes/config.py +51 -0
- mapchete_eo/processes/dtype_scale.py +112 -0
- mapchete_eo/processes/eo_to_xarray.py +19 -0
- mapchete_eo/processes/merge_rasters.py +239 -0
- mapchete_eo/product.py +323 -0
- mapchete_eo/protocols.py +61 -0
- mapchete_eo/search/__init__.py +14 -0
- mapchete_eo/search/base.py +285 -0
- mapchete_eo/search/config.py +113 -0
- mapchete_eo/search/s2_mgrs.py +313 -0
- mapchete_eo/search/stac_search.py +278 -0
- mapchete_eo/search/stac_static.py +197 -0
- mapchete_eo/search/utm_search.py +251 -0
- mapchete_eo/settings.py +25 -0
- mapchete_eo/sort.py +60 -0
- mapchete_eo/source.py +109 -0
- mapchete_eo/time.py +62 -0
- mapchete_eo/types.py +76 -0
- mapchete_eo-2026.2.0.dist-info/METADATA +91 -0
- mapchete_eo-2026.2.0.dist-info/RECORD +89 -0
- mapchete_eo-2026.2.0.dist-info/WHEEL +4 -0
- mapchete_eo-2026.2.0.dist-info/entry_points.txt +11 -0
- mapchete_eo-2026.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import numpy.ma as ma
|
|
6
|
+
from numpy.typing import DTypeLike
|
|
7
|
+
import xarray as xr
|
|
8
|
+
from mapchete.pretty import pretty_bytes
|
|
9
|
+
from mapchete.protocols import GridProtocol
|
|
10
|
+
from mapchete.types import NodataVals, NodataVal
|
|
11
|
+
from rasterio.enums import Resampling
|
|
12
|
+
|
|
13
|
+
from mapchete_eo.array.convert import to_dataset
|
|
14
|
+
from mapchete_eo.exceptions import (
|
|
15
|
+
CorruptedSlice,
|
|
16
|
+
EmptySliceException,
|
|
17
|
+
EmptyStackException,
|
|
18
|
+
NoSourceProducts,
|
|
19
|
+
)
|
|
20
|
+
from mapchete_eo.io.products import products_to_slices
|
|
21
|
+
from mapchete_eo.protocols import EOProductProtocol
|
|
22
|
+
from mapchete_eo.sort import SortMethodConfig, TargetDateSort
|
|
23
|
+
from mapchete_eo.types import MergeMethod
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def read_levelled_cube_to_np_array(
|
|
29
|
+
products: List[EOProductProtocol],
|
|
30
|
+
target_height: int,
|
|
31
|
+
grid: GridProtocol,
|
|
32
|
+
assets: Optional[List[str]] = None,
|
|
33
|
+
eo_bands: Optional[List[str]] = None,
|
|
34
|
+
resampling: Resampling = Resampling.nearest,
|
|
35
|
+
nodatavals: NodataVals = None,
|
|
36
|
+
merge_products_by: Optional[str] = None,
|
|
37
|
+
merge_method: MergeMethod = MergeMethod.first,
|
|
38
|
+
sort: SortMethodConfig = TargetDateSort(),
|
|
39
|
+
product_read_kwargs: dict = {},
|
|
40
|
+
raise_empty: bool = True,
|
|
41
|
+
out_dtype: DTypeLike = np.uint16,
|
|
42
|
+
out_fill_value: NodataVal = 0,
|
|
43
|
+
read_mask: Optional[np.ndarray] = None,
|
|
44
|
+
) -> ma.MaskedArray:
|
|
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.
|
|
49
|
+
"""
|
|
50
|
+
if len(products) == 0: # pragma: no cover
|
|
51
|
+
raise NoSourceProducts("no products to read")
|
|
52
|
+
bands = assets or eo_bands
|
|
53
|
+
if bands is None: # pragma: no cover
|
|
54
|
+
raise ValueError("either assets or eo_bands have to be set")
|
|
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
|
+
)
|
|
65
|
+
out: ma.MaskedArray = ma.masked_array(
|
|
66
|
+
data=np.full(out_shape, out_fill_value, dtype=out_dtype),
|
|
67
|
+
mask=np.ones(out_shape, dtype=bool),
|
|
68
|
+
fill_value=out_fill_value,
|
|
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
|
+
|
|
82
|
+
logger.debug(
|
|
83
|
+
"empty cube with shape %s has %s and %s pixels to be filled",
|
|
84
|
+
out.shape,
|
|
85
|
+
pretty_bytes(out.size * out.itemsize),
|
|
86
|
+
_cube_read_mask().sum(),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
logger.debug("sort products into slices ...")
|
|
90
|
+
slices = products_to_slices(
|
|
91
|
+
products=products, group_by_property=merge_products_by, sort=sort
|
|
92
|
+
)
|
|
93
|
+
logger.debug(
|
|
94
|
+
"generating levelled cube with height %s from %s slices",
|
|
95
|
+
target_height,
|
|
96
|
+
len(slices),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
slices_read_count, slices_skip_count = 0, 0
|
|
100
|
+
|
|
101
|
+
# pick slices one by one
|
|
102
|
+
for slice_count, slice_ in enumerate(slices, 1):
|
|
103
|
+
# all filled up? let's get outta here!
|
|
104
|
+
if not out.mask.any():
|
|
105
|
+
logger.debug("cube has no pixels to be filled, quitting!")
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
# generate 2D mask of holes to be filled in output cube
|
|
109
|
+
cube_nodata_mask = np.logical_and(out.mask.any(axis=0).any(axis=0), read_mask)
|
|
110
|
+
|
|
111
|
+
# read slice
|
|
112
|
+
try:
|
|
113
|
+
logger.debug(
|
|
114
|
+
"see if slice %s %s has some of the %s unmasked pixels for cube",
|
|
115
|
+
slice_count,
|
|
116
|
+
slice_,
|
|
117
|
+
cube_nodata_mask.sum(),
|
|
118
|
+
)
|
|
119
|
+
with slice_.cached():
|
|
120
|
+
slice_array = slice_.read(
|
|
121
|
+
merge_method=merge_method,
|
|
122
|
+
product_read_kwargs=dict(
|
|
123
|
+
product_read_kwargs,
|
|
124
|
+
assets=assets,
|
|
125
|
+
eo_bands=eo_bands,
|
|
126
|
+
grid=grid,
|
|
127
|
+
resampling=resampling,
|
|
128
|
+
nodatavals=nodatavals,
|
|
129
|
+
raise_empty=raise_empty,
|
|
130
|
+
read_mask=cube_nodata_mask.copy(),
|
|
131
|
+
out_dtype=out_dtype,
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
slices_read_count += 1
|
|
135
|
+
except (EmptySliceException, CorruptedSlice) as exc:
|
|
136
|
+
logger.debug("skipped slice %s: %s", slice_, str(exc))
|
|
137
|
+
slices_skip_count += 1
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
# if slice was not empty, fill pixels into cube
|
|
141
|
+
logger.debug("add slice %s array to cube", slice_)
|
|
142
|
+
|
|
143
|
+
# iterate through layers of cube
|
|
144
|
+
for layer_index in range(target_height):
|
|
145
|
+
# go to next layer if layer is full
|
|
146
|
+
if not out[layer_index].mask.any():
|
|
147
|
+
logger.debug("layer %s: full, jump to next", layer_index)
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# determine empty patches of current layer
|
|
151
|
+
empty_patches = np.logical_and(out[layer_index].mask, layer_read_mask)
|
|
152
|
+
remaining_pixels_for_layer = (~slice_array[empty_patches].mask).sum()
|
|
153
|
+
|
|
154
|
+
# when slice has nothing to offer for this layer, skip
|
|
155
|
+
if remaining_pixels_for_layer == 0:
|
|
156
|
+
logger.debug(
|
|
157
|
+
"layer %s: slice has no pixels for this layer, jump to next",
|
|
158
|
+
layer_index,
|
|
159
|
+
)
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
# insert slice data into empty patches of layer
|
|
163
|
+
logger.debug(
|
|
164
|
+
"layer %s: fill with %s pixels ...",
|
|
165
|
+
layer_index,
|
|
166
|
+
remaining_pixels_for_layer,
|
|
167
|
+
)
|
|
168
|
+
out[layer_index][empty_patches] = slice_array[empty_patches]
|
|
169
|
+
|
|
170
|
+
# report on layer fill status
|
|
171
|
+
logger.debug(
|
|
172
|
+
"layer %s: %s",
|
|
173
|
+
layer_index,
|
|
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
|
+
),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# remove slice values which were just inserted for next layer
|
|
183
|
+
slice_array[empty_patches] = ma.masked
|
|
184
|
+
|
|
185
|
+
if slice_array.mask.all():
|
|
186
|
+
logger.debug("slice fully inserted into cube, skipping")
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
# report on layer fill status
|
|
190
|
+
logger.debug(
|
|
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
|
+
),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
logger.debug(
|
|
199
|
+
"%s slices read, %s slices skipped", slices_read_count, slices_skip_count
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if raise_empty and out.mask.all():
|
|
203
|
+
raise EmptyStackException("all slices in stack are empty or corrupt")
|
|
204
|
+
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def read_levelled_cube_to_xarray(
|
|
209
|
+
products: List[EOProductProtocol],
|
|
210
|
+
target_height: int,
|
|
211
|
+
assets: Optional[List[str]] = None,
|
|
212
|
+
eo_bands: Optional[List[str]] = None,
|
|
213
|
+
grid: Optional[GridProtocol] = None,
|
|
214
|
+
resampling: Resampling = Resampling.nearest,
|
|
215
|
+
nodatavals: NodataVals = None,
|
|
216
|
+
merge_products_by: Optional[str] = None,
|
|
217
|
+
merge_method: MergeMethod = MergeMethod.first,
|
|
218
|
+
sort: SortMethodConfig = TargetDateSort(),
|
|
219
|
+
product_read_kwargs: dict = {},
|
|
220
|
+
raise_empty: bool = True,
|
|
221
|
+
slice_axis_name: str = "layers",
|
|
222
|
+
band_axis_name: str = "bands",
|
|
223
|
+
x_axis_name: str = "x",
|
|
224
|
+
y_axis_name: str = "y",
|
|
225
|
+
read_mask: Optional[np.ndarray] = None,
|
|
226
|
+
) -> xr.Dataset:
|
|
227
|
+
"""
|
|
228
|
+
Read products as slices into a cube by filling up nodata gaps with next slice.
|
|
229
|
+
"""
|
|
230
|
+
assets = assets or []
|
|
231
|
+
eo_bands = eo_bands or []
|
|
232
|
+
variables = assets or eo_bands
|
|
233
|
+
return to_dataset(
|
|
234
|
+
read_levelled_cube_to_np_array(
|
|
235
|
+
products=products,
|
|
236
|
+
target_height=target_height,
|
|
237
|
+
assets=assets,
|
|
238
|
+
eo_bands=eo_bands,
|
|
239
|
+
grid=grid,
|
|
240
|
+
resampling=resampling,
|
|
241
|
+
nodatavals=nodatavals,
|
|
242
|
+
merge_products_by=merge_products_by,
|
|
243
|
+
merge_method=merge_method,
|
|
244
|
+
sort=sort,
|
|
245
|
+
product_read_kwargs=product_read_kwargs,
|
|
246
|
+
raise_empty=raise_empty,
|
|
247
|
+
read_mask=read_mask,
|
|
248
|
+
),
|
|
249
|
+
slice_names=[f"layer-{ii}" for ii in range(target_height)],
|
|
250
|
+
band_names=variables,
|
|
251
|
+
slice_axis_name=slice_axis_name,
|
|
252
|
+
band_axis_name=band_axis_name,
|
|
253
|
+
x_axis_name=x_axis_name,
|
|
254
|
+
y_axis_name=y_axis_name,
|
|
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)"
|
mapchete_eo/io/path.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from tempfile import TemporaryDirectory
|
|
6
|
+
from typing import Generator, Tuple, Union
|
|
7
|
+
from xml.etree.ElementTree import Element, fromstring
|
|
8
|
+
|
|
9
|
+
import fsspec
|
|
10
|
+
import pystac
|
|
11
|
+
from mapchete.io import copy
|
|
12
|
+
from mapchete.path import MPath
|
|
13
|
+
from mapchete.settings import IORetrySettings
|
|
14
|
+
from retry import retry
|
|
15
|
+
|
|
16
|
+
from mapchete_eo.exceptions import AssetKeyError
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
COMMON_RASTER_EXTENSIONS = [".tif", ".jp2"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@retry(logger=logger, **dict(IORetrySettings()))
|
|
25
|
+
def open_xml(path: MPath) -> Element:
|
|
26
|
+
"""Parse an XML file path into an etree root element."""
|
|
27
|
+
logger.debug("open %s", path)
|
|
28
|
+
return fromstring(path.read_text())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ProductPathGenerationMethod(str, Enum):
|
|
32
|
+
"""Option to generate product cache path."""
|
|
33
|
+
|
|
34
|
+
# <cache_basepath>/<product-id>
|
|
35
|
+
product_id = "product_id"
|
|
36
|
+
|
|
37
|
+
# <cache_basepath>/<product-hash>
|
|
38
|
+
hash = "hash"
|
|
39
|
+
|
|
40
|
+
# <cache_basepath>/<product-day>/<product-month>/<product-year>/<product-id>
|
|
41
|
+
date_day_first = "date_day_first"
|
|
42
|
+
|
|
43
|
+
# <cache_basepath>/<product-year>/<product-month>/<product-day>/<product-id>
|
|
44
|
+
date_year_first = "date_year_first"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_product_cache_path(
|
|
48
|
+
item: pystac.Item,
|
|
49
|
+
basepath: MPath,
|
|
50
|
+
path_generation_method: ProductPathGenerationMethod = ProductPathGenerationMethod.product_id,
|
|
51
|
+
) -> MPath:
|
|
52
|
+
"""
|
|
53
|
+
Create product path with high cardinality prefixes optimized for S3.
|
|
54
|
+
|
|
55
|
+
product_path_generation option:
|
|
56
|
+
|
|
57
|
+
"product_id":
|
|
58
|
+
<cache_basepath>/<product-id>
|
|
59
|
+
|
|
60
|
+
"product_hash":
|
|
61
|
+
<cache_basepath>/<product-hash>
|
|
62
|
+
|
|
63
|
+
"date_day_first":
|
|
64
|
+
<cache_basepath>/<product-day>/<product-month>/<product-year>/<product-id>
|
|
65
|
+
|
|
66
|
+
"date_year_first":
|
|
67
|
+
<cache_basepath>/<product-year>/<product-month>/<product-day>/<product-id>
|
|
68
|
+
"""
|
|
69
|
+
path_generation_method = ProductPathGenerationMethod[path_generation_method]
|
|
70
|
+
if path_generation_method == ProductPathGenerationMethod.product_id:
|
|
71
|
+
return basepath / item.id
|
|
72
|
+
|
|
73
|
+
elif path_generation_method == ProductPathGenerationMethod.hash:
|
|
74
|
+
return basepath / hashlib.md5(f"{item.id}".encode()).hexdigest()
|
|
75
|
+
|
|
76
|
+
else:
|
|
77
|
+
if item.datetime is None: # pragma: no cover
|
|
78
|
+
raise AttributeError(f"stac item must have a valid datetime object: {item}")
|
|
79
|
+
elif path_generation_method == ProductPathGenerationMethod.date_day_first:
|
|
80
|
+
return (
|
|
81
|
+
basepath
|
|
82
|
+
/ item.datetime.day
|
|
83
|
+
/ item.datetime.month
|
|
84
|
+
/ item.datetime.year
|
|
85
|
+
/ item.id
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
elif path_generation_method == ProductPathGenerationMethod.date_year_first:
|
|
89
|
+
return (
|
|
90
|
+
basepath
|
|
91
|
+
/ item.datetime.year
|
|
92
|
+
/ item.datetime.month
|
|
93
|
+
/ item.datetime.day
|
|
94
|
+
/ item.id
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def path_in_paths(path, existing_paths) -> bool:
|
|
99
|
+
"""Check if path is contained in list of existing paths independent of path prefix."""
|
|
100
|
+
if path.startswith("s3://"):
|
|
101
|
+
return path.lstrip("s3://") in existing_paths
|
|
102
|
+
else:
|
|
103
|
+
for existing_path in existing_paths:
|
|
104
|
+
if existing_path.endswith(path):
|
|
105
|
+
return True
|
|
106
|
+
else:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@contextmanager
|
|
111
|
+
@retry(logger=logger, **dict(IORetrySettings()))
|
|
112
|
+
def cached_path(path: MPath, active: bool = True) -> Generator[MPath, None, None]:
|
|
113
|
+
"""If path is remote, download to temporary directory and return path."""
|
|
114
|
+
if active and path.is_remote():
|
|
115
|
+
with TemporaryDirectory() as tempdir:
|
|
116
|
+
tempfile = MPath(tempdir) / path.name
|
|
117
|
+
logger.debug("%s is remote, download to %s", path, tempfile)
|
|
118
|
+
copy(
|
|
119
|
+
path,
|
|
120
|
+
tempfile,
|
|
121
|
+
)
|
|
122
|
+
yield tempfile
|
|
123
|
+
else:
|
|
124
|
+
yield path
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def asset_mpath(
|
|
128
|
+
item: pystac.Item,
|
|
129
|
+
asset: Union[str, Tuple[str, ...]],
|
|
130
|
+
fs: fsspec.AbstractFileSystem = None,
|
|
131
|
+
absolute_path: bool = True,
|
|
132
|
+
) -> MPath:
|
|
133
|
+
"""Return MPath instance with asset href."""
|
|
134
|
+
|
|
135
|
+
def _asset_mpath(
|
|
136
|
+
item: pystac.Item,
|
|
137
|
+
asset: str,
|
|
138
|
+
fs: fsspec.AbstractFileSystem = None,
|
|
139
|
+
absolute_path: bool = True,
|
|
140
|
+
) -> MPath:
|
|
141
|
+
asset_path = MPath(item.assets[asset].href, fs=fs)
|
|
142
|
+
if absolute_path and not asset_path.is_absolute():
|
|
143
|
+
return MPath(item.get_self_href(), fs=fs).parent / asset_path
|
|
144
|
+
else:
|
|
145
|
+
return asset_path
|
|
146
|
+
|
|
147
|
+
for single_asset in asset if isinstance(asset, tuple) else (asset,):
|
|
148
|
+
try:
|
|
149
|
+
return _asset_mpath(item, single_asset, fs=fs, absolute_path=absolute_path)
|
|
150
|
+
except KeyError:
|
|
151
|
+
pass
|
|
152
|
+
else:
|
|
153
|
+
raise AssetKeyError(
|
|
154
|
+
f"{item.id} no asset named '{asset}' found in assets: {', '.join(item.assets.keys())}"
|
|
155
|
+
)
|