mapchete-eo 2025.10.1__py2.py3-none-any.whl → 2026.1.0__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.
Files changed (63) hide show
  1. mapchete_eo/__init__.py +1 -1
  2. mapchete_eo/base.py +94 -54
  3. mapchete_eo/cli/options_arguments.py +11 -27
  4. mapchete_eo/cli/s2_brdf.py +1 -1
  5. mapchete_eo/cli/s2_cat_results.py +4 -20
  6. mapchete_eo/cli/s2_find_broken_products.py +4 -20
  7. mapchete_eo/cli/s2_jp2_static_catalog.py +2 -2
  8. mapchete_eo/cli/static_catalog.py +4 -45
  9. mapchete_eo/eostac.py +1 -1
  10. mapchete_eo/io/assets.py +20 -16
  11. mapchete_eo/io/items.py +36 -23
  12. mapchete_eo/io/path.py +19 -8
  13. mapchete_eo/io/products.py +22 -24
  14. mapchete_eo/platforms/sentinel2/__init__.py +1 -1
  15. mapchete_eo/platforms/sentinel2/_mapper_registry.py +89 -0
  16. mapchete_eo/platforms/sentinel2/brdf/correction.py +1 -1
  17. mapchete_eo/platforms/sentinel2/brdf/hls.py +1 -1
  18. mapchete_eo/platforms/sentinel2/brdf/models.py +1 -1
  19. mapchete_eo/platforms/sentinel2/brdf/protocols.py +1 -1
  20. mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +1 -1
  21. mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +1 -1
  22. mapchete_eo/platforms/sentinel2/config.py +73 -13
  23. mapchete_eo/platforms/sentinel2/driver.py +0 -39
  24. mapchete_eo/platforms/sentinel2/metadata_parser/__init__.py +6 -0
  25. mapchete_eo/platforms/sentinel2/{path_mappers → metadata_parser}/base.py +1 -1
  26. mapchete_eo/platforms/sentinel2/{path_mappers/metadata_xml.py → metadata_parser/default_path_mapper.py} +2 -2
  27. mapchete_eo/platforms/sentinel2/metadata_parser/models.py +78 -0
  28. mapchete_eo/platforms/sentinel2/{metadata_parser.py → metadata_parser/s2metadata.py} +51 -144
  29. mapchete_eo/platforms/sentinel2/preconfigured_sources/__init__.py +57 -0
  30. mapchete_eo/platforms/sentinel2/preconfigured_sources/guessers.py +108 -0
  31. mapchete_eo/platforms/sentinel2/preconfigured_sources/item_mappers.py +171 -0
  32. mapchete_eo/platforms/sentinel2/preconfigured_sources/metadata_xml_mappers.py +217 -0
  33. mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +22 -1
  34. mapchete_eo/platforms/sentinel2/processing_baseline.py +3 -0
  35. mapchete_eo/platforms/sentinel2/product.py +83 -18
  36. mapchete_eo/platforms/sentinel2/source.py +114 -0
  37. mapchete_eo/platforms/sentinel2/types.py +5 -0
  38. mapchete_eo/product.py +14 -8
  39. mapchete_eo/protocols.py +5 -0
  40. mapchete_eo/search/__init__.py +3 -3
  41. mapchete_eo/search/base.py +127 -99
  42. mapchete_eo/search/config.py +75 -4
  43. mapchete_eo/search/s2_mgrs.py +8 -9
  44. mapchete_eo/search/stac_search.py +99 -97
  45. mapchete_eo/search/stac_static.py +46 -102
  46. mapchete_eo/search/utm_search.py +54 -62
  47. mapchete_eo/settings.py +1 -0
  48. mapchete_eo/sort.py +4 -6
  49. mapchete_eo/source.py +107 -0
  50. {mapchete_eo-2025.10.1.dist-info → mapchete_eo-2026.1.0.dist-info}/METADATA +4 -3
  51. mapchete_eo-2026.1.0.dist-info/RECORD +89 -0
  52. {mapchete_eo-2025.10.1.dist-info → mapchete_eo-2026.1.0.dist-info}/WHEEL +1 -1
  53. {mapchete_eo-2025.10.1.dist-info → mapchete_eo-2026.1.0.dist-info}/entry_points.txt +1 -1
  54. {mapchete_eo-2025.10.1.dist-info → mapchete_eo-2026.1.0.dist-info}/licenses/LICENSE +1 -1
  55. mapchete_eo/archives/__init__.py +0 -0
  56. mapchete_eo/archives/base.py +0 -65
  57. mapchete_eo/geometry.py +0 -271
  58. mapchete_eo/known_catalogs.py +0 -42
  59. mapchete_eo/platforms/sentinel2/archives.py +0 -190
  60. mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +0 -29
  61. mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +0 -34
  62. mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +0 -105
  63. mapchete_eo-2025.10.1.dist-info/RECORD +0 -88
mapchete_eo/settings.py CHANGED
@@ -16,6 +16,7 @@ class Settings(BaseSettings):
16
16
  default_cache_location: MPathLike = MPath("s3://eox-mhub-cache/")
17
17
  default_catalog_crs: CRS = CRS.from_epsg(4326)
18
18
  blacklist: Optional[MPathLike] = None
19
+ lazy_load_stac_items: bool = True
19
20
 
20
21
  # read from environment
21
22
  model_config = SettingsConfigDict(env_prefix="MAPCHETE_EO_")
mapchete_eo/sort.py CHANGED
@@ -5,10 +5,8 @@ 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
9
8
 
10
- from mapchete_eo.io.items import get_item_property
11
- from mapchete_eo.protocols import DateTimeProtocol
9
+ from mapchete_eo.protocols import DateTimeProtocol, GetPropertyProtocol
12
10
  from mapchete_eo.time import timedelta, to_datetime
13
11
  from mapchete_eo.types import DateTimeLike
14
12
 
@@ -51,11 +49,11 @@ class TargetDateSort(SortMethodConfig):
51
49
 
52
50
 
53
51
  def sort_objects_by_cloud_cover(
54
- objects: List[Item], reverse: bool = False
55
- ) -> List[Item]:
52
+ objects: List[GetPropertyProtocol], reverse: bool = False
53
+ ) -> List[GetPropertyProtocol]:
56
54
  if len(objects) == 0: # pragma: no cover
57
55
  return objects
58
- objects.sort(key=lambda x: get_item_property(x, "eo:cloud_cover"), reverse=reverse)
56
+ objects.sort(key=lambda x: x.get_property("eo:cloud_cover"), reverse=reverse)
59
57
  return objects
60
58
 
61
59
 
mapchete_eo/source.py ADDED
@@ -0,0 +1,107 @@
1
+ from functools import cached_property
2
+ from typing import Any, Dict, List, Literal, Optional, Generator, Union, Callable
3
+
4
+ from mapchete.bounds import Bounds
5
+ from mapchete.path import MPath
6
+ from mapchete.types import BoundsLike, CRSLike, MPathLike
7
+ from pydantic import BaseModel, ConfigDict, model_validator
8
+ from pystac import Item
9
+ from shapely.geometry.base import BaseGeometry
10
+ from shapely.errors import GEOSException
11
+
12
+ from mapchete_eo.exceptions import ItemGeometryError
13
+ from mapchete_eo.search.base import CollectionSearcher
14
+ from mapchete_eo.search import STACSearchCollection, STACStaticCollection
15
+ from mapchete_eo.settings import mapchete_eo_settings
16
+ from mapchete_eo.types import TimeRange
17
+
18
+
19
+ class Source(BaseModel):
20
+ """All information required to consume EO products."""
21
+
22
+ collection: str
23
+ catalog_crs: Optional[CRSLike] = mapchete_eo_settings.default_catalog_crs
24
+ query: Optional[str] = None
25
+ area: Optional[Union[MPathLike, dict, type[BaseGeometry]]] = None
26
+ bounds: Optional[BoundsLike] = None
27
+
28
+ model_config = ConfigDict(arbitrary_types_allowed=True)
29
+
30
+ @property
31
+ def item_modifier_funcs(self) -> List[Callable]:
32
+ return []
33
+
34
+ @cached_property
35
+ def catalog_type(self) -> Literal["search", "static"]:
36
+ # TODO: stupid test but probably sufficient
37
+ return "static" if self.collection.endswith(".json") else "search"
38
+
39
+ def _spatial_subset(
40
+ self,
41
+ bounds: Optional[BoundsLike] = None,
42
+ area: Optional[BaseGeometry] = None,
43
+ ) -> Dict[str, Any]:
44
+ """Combine bounds and area with bounds defined in Source if any."""
45
+ if self.bounds is None:
46
+ return {"bounds": bounds, "area": area}
47
+ self_bounds = Bounds.from_inp(self.bounds)
48
+ out = dict()
49
+ if bounds is not None:
50
+ bounds = Bounds.from_inp(bounds)
51
+ if bounds.intersects(self_bounds):
52
+ out["bounds"] = Bounds.from_inp(
53
+ bounds.geometry.intersection(self_bounds.geometry)
54
+ )
55
+ if area is not None:
56
+ out["area"] = area.intersection(self_bounds.geometry)
57
+ return out
58
+
59
+ def search(
60
+ self,
61
+ time: Optional[Union[TimeRange, List[TimeRange]]] = None,
62
+ bounds: Optional[BoundsLike] = None,
63
+ area: Optional[BaseGeometry] = None,
64
+ base_dir: Optional[MPathLike] = None,
65
+ ) -> Generator[Item, None, None]:
66
+ for item in self.get_catalog(base_dir=base_dir).search(
67
+ time=time,
68
+ query=self.query,
69
+ search_kwargs=dict(query=self.query) if self.query else None,
70
+ **self._spatial_subset(
71
+ bounds=bounds,
72
+ area=area,
73
+ ),
74
+ ):
75
+ yield self.apply_item_modifier_funcs(item)
76
+
77
+ def apply_item_modifier_funcs(self, item: Item) -> Item:
78
+ try:
79
+ for modifier in self.item_modifier_funcs:
80
+ item = modifier(item)
81
+ except GEOSException as exc:
82
+ raise ItemGeometryError(
83
+ f"item {item.get_self_href()} geometry could not be resolved: {str(exc)}"
84
+ )
85
+ return item
86
+
87
+ def get_catalog(self, base_dir: Optional[MPathLike] = None) -> CollectionSearcher:
88
+ match self.catalog_type:
89
+ case "search":
90
+ return STACSearchCollection(self.collection)
91
+ case "static":
92
+ return STACStaticCollection(
93
+ collection=MPath(self.collection).absolute_path(base_dir=base_dir)
94
+ )
95
+
96
+ def eo_bands(self, base_dir: Optional[MPathLike] = None) -> List[str]:
97
+ return self.get_catalog(base_dir=base_dir).eo_bands
98
+
99
+ @model_validator(mode="before")
100
+ def deprecate_max_cloud_cover(cls, values: Dict[str, Any]) -> Dict[str, Any]:
101
+ if "max_cloud_cover" in values:
102
+ raise DeprecationWarning(
103
+ "'max_cloud_cover' will be deprecated soon. Please use 'eo:cloud_cover<=...' in the source 'query' field.",
104
+ )
105
+ elif "area" in values: # pragma: no cover
106
+ raise NotImplementedError("please use 'bounds' as spatial subset for now")
107
+ return values
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapchete-eo
3
- Version: 2025.10.1
3
+ Version: 2026.1.0
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>
@@ -21,6 +21,7 @@ Requires-Dist: mapchete[complete]>=2025.10.0
21
21
  Requires-Dist: opencv-python-headless
22
22
  Requires-Dist: pillow
23
23
  Requires-Dist: pydantic
24
+ Requires-Dist: pygeofilter
24
25
  Requires-Dist: pystac-client>=0.7.5
25
26
  Requires-Dist: pystac[urllib3]>=1.12.2
26
27
  Requires-Dist: retry
@@ -32,9 +33,9 @@ Provides-Extra: docs
32
33
  Requires-Dist: sphinx; extra == 'docs'
33
34
  Requires-Dist: sphinx-rtd-theme; extra == 'docs'
34
35
  Provides-Extra: test
36
+ Requires-Dist: pytest; extra == 'test'
35
37
  Requires-Dist: pytest-coverage; extra == 'test'
36
- Requires-Dist: pytest-lazy-fixture; extra == 'test'
37
- Requires-Dist: pytest<8; extra == 'test'
38
+ Requires-Dist: pytest-lazy-fixtures; extra == 'test'
38
39
  Description-Content-Type: text/x-rst
39
40
 
40
41
  .. image:: logo/mapchete_eo.svg
@@ -0,0 +1,89 @@
1
+ mapchete_eo/__init__.py,sha256=Q2R77nEGEKjxOBz3qMm2JGtkZn-nVFmReBAVH_YeZ4M,25
2
+ mapchete_eo/base.py,sha256=_6wbR9IZBBBdRBbZY4FEQ0uu603C-tMPYK4MGQ5xSYY,21953
3
+ mapchete_eo/blacklist.txt,sha256=6KhBY0jNjXgRpKRvKJoOTswvNUoP56IrIcNeCYnd2qo,17471
4
+ mapchete_eo/eostac.py,sha256=M-EL8y8pPw8H9hb85FPxSl-1FL_kIYOzxsTIhkrbBZY,570
5
+ mapchete_eo/exceptions.py,sha256=ul7_9o6_SfJXQPsDOQqRBhh09xv2t4AwHl6YJ7yWN5s,2150
6
+ mapchete_eo/product.py,sha256=0GJDk4pPvdCLxuJkZCMQnM7LIjvOBRUhIzchcQCqMjI,9644
7
+ mapchete_eo/protocols.py,sha256=KUbteFTOCSYEiSUJZj2MeckmrJHfJFrXCvZE70tByP8,1617
8
+ mapchete_eo/settings.py,sha256=lbwYoqiQXBRexxL7Y524Ry0dk4vBKpjN1u09UOP1_CM,734
9
+ mapchete_eo/sort.py,sha256=DfYAGEM6vzHJOFhaKxcKr-GwH5xM5ar4_mlp0uiVUK0,1787
10
+ mapchete_eo/source.py,sha256=Tfo5IIYdRWpS06K-M7mzesl4WIUEt4onGmc1uKJ0F14,4158
11
+ mapchete_eo/time.py,sha256=mCIrn6C-B7CDoFIIM4u8pG15nVV8KWQixF2fuhdDTdE,1799
12
+ mapchete_eo/types.py,sha256=yIHKZHlGCSerJzDBS2AH7yYLun4rY3rZZ73SCKiN26U,1814
13
+ mapchete_eo/array/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ mapchete_eo/array/buffer.py,sha256=GeeM7RVQ-Z_SKlkaeztWUrxEsa_yck-nXwXRIM3w0_I,483
15
+ mapchete_eo/array/color.py,sha256=ArJ3-0qjJJGAlRQodLios6JwD0uHLSRRb9BwfB31v-U,810
16
+ mapchete_eo/array/convert.py,sha256=oimklMfyS2XxQLd73zQ2cr9aE-39z1UIt6ry_2ZQI10,5390
17
+ mapchete_eo/cli/__init__.py,sha256=SRRilPKUtwsUCi1GdMNPxjIrCM-XyYM2hK1T9vvKUI8,946
18
+ mapchete_eo/cli/bounds.py,sha256=QcOXOXMcnd8ecwiCFVWukTLUFIkpj9SbB3ot-GA4heA,551
19
+ mapchete_eo/cli/options_arguments.py,sha256=6VqiwswEKpr5bS0w1ugRSf2z7rMKA-wiEFZdcmri94c,6916
20
+ mapchete_eo/cli/s2_brdf.py,sha256=siUGau8HzXNyQZkDUx1zAeaLcCfonJG5kWHSCvwJOhA,2729
21
+ mapchete_eo/cli/s2_cat_results.py,sha256=tR5Qm6kiCAcDO5V9AkYhoUbHyEx7OItSf6IqtIjhRLg,4648
22
+ mapchete_eo/cli/s2_find_broken_products.py,sha256=4nhtMLkys75enPV5lETw4iOn3CxxNnc-sMyhk_kaKx0,2946
23
+ mapchete_eo/cli/s2_jp2_static_catalog.py,sha256=_j_rfFnYHtE8k79TS9KFuX_IxOx_e-r7u6beoLYyxPY,5911
24
+ mapchete_eo/cli/s2_mask.py,sha256=1K5BiyTKyClGBdh6-Vh_ORTcgJOr6R5JfXtpEpDEQvk,2243
25
+ mapchete_eo/cli/s2_mgrs.py,sha256=EJxbrg4I8IttWIcLUEmmqkjsTUbVLufgrkDVrRbbkYE,1276
26
+ mapchete_eo/cli/s2_rgb.py,sha256=nzK5m4s0m2VOXgtit0uUOOhNJ14wquikWmbEQON5WJw,3747
27
+ mapchete_eo/cli/s2_verify.py,sha256=atfJA5luHZjDSqiM2udaufmnQ2B4uW2Tur_OgtYLa2g,4080
28
+ mapchete_eo/cli/static_catalog.py,sha256=O7oCJB6mxWwnEMowPBo6UMtWOu52mSNUBB9tpSw91a4,3005
29
+ mapchete_eo/image_operations/__init__.py,sha256=c36Vs53mT5UQR9Iwd2Ln_vnT4K_Ni8g-K7J9Fl6UsJo,432
30
+ mapchete_eo/image_operations/blend_functions.py,sha256=YaKLOEapMbsUR-_mkBatXwPcmim8GKpIAVua_GOdaHs,24131
31
+ mapchete_eo/image_operations/color_correction.py,sha256=vMXxcdyH3qOJDSKcXGU3FgXWIcgQxMy-QVe2fGQTbdk,4447
32
+ mapchete_eo/image_operations/compositing.py,sha256=K0bGCZqJuS39IBsjrRiJz5zowItI6RsiA-EBUVlbboo,8449
33
+ mapchete_eo/image_operations/dtype_scale.py,sha256=_CCdTBkMLDkZdjV_PsVEkqLWT6jWuTJPeMzCcRwBOns,1243
34
+ mapchete_eo/image_operations/fillnodata.py,sha256=Xdd-kacfo7VKqX7HRP0hqnnvV5OwNqnHLRTW64QhG24,4856
35
+ mapchete_eo/image_operations/filters.py,sha256=sa_Igv0zMIANHLEALVi8mxohZMoSrdQc6XVjXDYwzy8,7201
36
+ mapchete_eo/image_operations/linear_normalization.py,sha256=-eQX3WLCUYWUv-um3s1u33rgjAwCz84Ht8gcPsMWtJE,2468
37
+ mapchete_eo/image_operations/sigmoidal.py,sha256=IKU8F89HhQJWGUVmIrnkuhqzn_ztlGrTf8xXZaVQWzU,3575
38
+ mapchete_eo/io/__init__.py,sha256=1-1g4cESZZREvyumEUABZhDwgVuSxtxdqbNlLuVKlow,950
39
+ mapchete_eo/io/assets.py,sha256=OVXLxrwCCUa6frP-xpm2CL30MR8eMABnwVD6EpN1cqU,17215
40
+ mapchete_eo/io/items.py,sha256=5H2xWAyLptDuuHh0rjY1MXReJomV0XBZgMr_rpdpYXU,6244
41
+ mapchete_eo/io/levelled_cubes.py,sha256=ZF7BLn9MHnJCCDjAoR9D7MNbHdusjJ9EACdM7rKNlyM,9018
42
+ mapchete_eo/io/path.py,sha256=fCjvowd9rGJbGio5Z-jDLxa49M2BVurYM9hJy53Im48,4831
43
+ mapchete_eo/io/products.py,sha256=2gELjHoEe4QH-Yjk82tA2nZUnBgfPbaGvk5GMHUWs4w,14642
44
+ mapchete_eo/io/profiles.py,sha256=l9YiXbKYs674xz6NLy5EAq3fBEvHTVSf_gopXD-CuSY,935
45
+ mapchete_eo/platforms/sentinel2/__init__.py,sha256=dviB4f5k45t_iOScTtnPc1kaWcohWKRq-8RaQzdggmU,401
46
+ mapchete_eo/platforms/sentinel2/_mapper_registry.py,sha256=3kuyW2g_xJoFUzZrAmhKn9ASHl4gBKuOlHsSZnpt21w,2746
47
+ mapchete_eo/platforms/sentinel2/bandpass_adjustment.py,sha256=DA0cQtjr8UH7r_kizAD-EmBsZvEtVVThf0hD0SCY7b4,3202
48
+ mapchete_eo/platforms/sentinel2/config.py,sha256=ifmzxDEuOFfoGAvbGyWyUWwF3Dw0NxEUEA3bWNByNR0,8482
49
+ mapchete_eo/platforms/sentinel2/driver.py,sha256=hNc9jmLQMmnA2ciVBPSTmVW6H8clL6sydkxcy6BjO40,1166
50
+ mapchete_eo/platforms/sentinel2/masks.py,sha256=6ig8sQhXkm1u6Bwbe7n8ewW8gTdbVJp-iaDCk780Qxg,11220
51
+ mapchete_eo/platforms/sentinel2/preprocessing_tasks.py,sha256=DJhbvYhNDGZ2Er5polK2LXWcg-MASZMFparNOw56yDk,1645
52
+ mapchete_eo/platforms/sentinel2/processing_baseline.py,sha256=uTE7zgzgi_W3NENLIAnJbWjyUD5_znpGa3cV-VoaEws,5179
53
+ mapchete_eo/platforms/sentinel2/product.py,sha256=UUbngHI2qPri5GkMPdmaw-ZS0y6aIQUtgZ5oZXUcIJc,28095
54
+ mapchete_eo/platforms/sentinel2/source.py,sha256=qSziCNTpInGy2LWIMiC3M4EP9G9_VtbCRNXB1XAO1P4,4269
55
+ mapchete_eo/platforms/sentinel2/types.py,sha256=EV7TleKZnCNuDlEMJ0UtJjDsvOUGbvh_pZToeOULQ18,2122
56
+ mapchete_eo/platforms/sentinel2/brdf/__init__.py,sha256=W7zAJZ5F5Xla7j4ua2opRANUbcD3jZbhTwhW5_cpOUI,242
57
+ mapchete_eo/platforms/sentinel2/brdf/config.py,sha256=v3WCEu4r-tEPQgWBEkYNAPLgCQobvYtQ2YiDeAdImMk,1133
58
+ mapchete_eo/platforms/sentinel2/brdf/correction.py,sha256=vi-BwAA6Cbayzk6WOuOESLhRkGLhiUAbVYfJUSfYhXA,8651
59
+ mapchete_eo/platforms/sentinel2/brdf/hls.py,sha256=pzrRTNfJhuqRLidLQvKJpSnaz816x7jUesXt_aRgFjc,8818
60
+ mapchete_eo/platforms/sentinel2/brdf/models.py,sha256=BBwGX1OLMRLncK1jD2reidaF82IKR4sjgNGHdTgBXaw,1446
61
+ mapchete_eo/platforms/sentinel2/brdf/protocols.py,sha256=fxLDFeaL44eIPC2UwcbEQ-AXPREWgBv6Iytc74AQ5eA,752
62
+ mapchete_eo/platforms/sentinel2/brdf/ross_thick.py,sha256=OvuuQxqWN8qrKpz8kDhmCd8R1uMLa8h5LISJSJ_NvYk,4685
63
+ mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py,sha256=RxhQ8vBU0r5vsQ3FnyfDdYS0EGDGKCwAXND0Gn1m5mQ,2148
64
+ mapchete_eo/platforms/sentinel2/metadata_parser/__init__.py,sha256=Vio87daAQ1eBQfDfA97c9byVnAUEPCidjTBg6jnZhXk,167
65
+ mapchete_eo/platforms/sentinel2/metadata_parser/base.py,sha256=AvPfsQEAyXOrwOQ_MQgWLUFFJL0VhTzzWHpuSe14IS0,1520
66
+ mapchete_eo/platforms/sentinel2/metadata_parser/default_path_mapper.py,sha256=Gt9jUZwO3u_p6iH3hkOGNS5Tad5yn-Y1F7uADtalBSo,5344
67
+ mapchete_eo/platforms/sentinel2/metadata_parser/models.py,sha256=FOdEeIKk_Sa_O3iGRqoR-5YLQuwjPG6pNbdFRgQVYm8,2325
68
+ mapchete_eo/platforms/sentinel2/metadata_parser/s2metadata.py,sha256=i6O9t0Bbb5BCiktKmXgDBKWdzdtVoGJ-Nt-xib-w3qw,23786
69
+ mapchete_eo/platforms/sentinel2/preconfigured_sources/__init__.py,sha256=W-PFNuC0zXfF07uTUH_-r2Macv-_5qlz_YsP04Jxh8Y,1821
70
+ mapchete_eo/platforms/sentinel2/preconfigured_sources/guessers.py,sha256=lkftO9NCJqwV663K3YqUFkNQxQWrCuwp_enbfTkjdbQ,3746
71
+ mapchete_eo/platforms/sentinel2/preconfigured_sources/item_mappers.py,sha256=b_lvs6-rKQjb-8vBjOLV0Zi4a2ZXZxMqinyeRljA5lY,5957
72
+ mapchete_eo/platforms/sentinel2/preconfigured_sources/metadata_xml_mappers.py,sha256=8UbF5Riox1FZIYGuDMBiGgxNfAQkOmu07ZMc2Eqiel0,8647
73
+ mapchete_eo/processes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
+ mapchete_eo/processes/config.py,sha256=WorMzHJzJaZPh-aEyEEwcXN08ALxn6zAhVMrjkDUWDU,1745
75
+ mapchete_eo/processes/dtype_scale.py,sha256=7hJlIe2DsE6Kmk1c3PLwLr8rnn_4_9S6Cz7bmRuzP9M,4176
76
+ mapchete_eo/processes/eo_to_xarray.py,sha256=Gcp4qju2C9S8KeUnVY5f3nAsrdckPhGRguzsgRtWBFs,514
77
+ mapchete_eo/processes/merge_rasters.py,sha256=jSXUI8pEWXbQWrM9iMtrH4ACH2VkCaBUH9CQg4z6zLA,8486
78
+ mapchete_eo/search/__init__.py,sha256=bHvaqoHrtoNyO8_3kyJfvhh1yCz0hYvU2fzXhjOXNhU,528
79
+ mapchete_eo/search/base.py,sha256=B8vlwgvDYRqxHre5aYWfPZ0tmUic-mb1VBnzl0llUSQ,9757
80
+ mapchete_eo/search/config.py,sha256=L3DBEuKNGh_WXi8_Ha-qj6HW9ksRioYeFyPYGGN09qA,3696
81
+ mapchete_eo/search/s2_mgrs.py,sha256=x12m-tJZNc41lFHNR2h7eSyEZ-ceBO4Gdz785btcVN8,10859
82
+ mapchete_eo/search/stac_search.py,sha256=gWMSmDaXZTfH_GV1YiQ8ThbnJYsxHB2j_y_SgMX41Bg,10079
83
+ mapchete_eo/search/stac_static.py,sha256=e53nKu8QRmanGvPmrvIDavHXm8v_4aT5V-wvU8wnkiw,6979
84
+ mapchete_eo/search/utm_search.py,sha256=ZbNJCf461H_709Bpqzuwiu4_iwXC4M-wkemxXKmGenk,9489
85
+ mapchete_eo-2026.1.0.dist-info/METADATA,sha256=0qagoc1eCiRIjxn78e_jJIieQkjV018I_0ylh-1leQQ,3262
86
+ mapchete_eo-2026.1.0.dist-info/WHEEL,sha256=aha0VrrYvgDJ3Xxl3db_g_MDIW-ZexDdrc_m-Hk8YY4,105
87
+ mapchete_eo-2026.1.0.dist-info/entry_points.txt,sha256=edU29CA3j8hwBafEd6jO2Zcou1sGkyjNro-y4_LnAnw,320
88
+ mapchete_eo-2026.1.0.dist-info/licenses/LICENSE,sha256=gmZNf2EjyzaNbUyhfsBJlS0yQeG7GEmd-29miNXtEwA,1089
89
+ mapchete_eo-2026.1.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any
@@ -2,7 +2,7 @@
2
2
  eo = mapchete_eo.cli:eo
3
3
 
4
4
  [mapchete.formats.drivers]
5
- eostac_dev = mapchete_eo.eostac
5
+ eostac = mapchete_eo.eostac
6
6
  sentinel2 = mapchete_eo.platforms.sentinel2
7
7
 
8
8
  [mapchete.processes]
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2022 - 2025 EOX IT Services
3
+ Copyright (c) 2022 - 2026 EOX IT Services
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
File without changes
@@ -1,65 +0,0 @@
1
- from abc import ABC
2
- import logging
3
- from typing import Any, Callable, Dict, Generator, List, Optional, Union
4
-
5
- from mapchete.io.vector import IndexedFeatures
6
- from mapchete.types import Bounds
7
- from pystac import Item
8
- from shapely.errors import GEOSException
9
- from shapely.geometry.base import BaseGeometry
10
-
11
- from mapchete_eo.exceptions import ItemGeometryError
12
- from mapchete_eo.search.base import CatalogSearcher
13
- from mapchete_eo.types import TimeRange
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- class Archive(ABC):
19
- """
20
- An archive combines a Catalog and a Storage.
21
- """
22
-
23
- time: Union[TimeRange, List[TimeRange]]
24
- area: BaseGeometry
25
- catalog: CatalogSearcher
26
- search_kwargs: Dict[str, Any]
27
- _items: Optional[IndexedFeatures] = None
28
- item_modifier_funcs: Optional[List[Callable[[Item], Item]]] = None
29
-
30
- def __init__(
31
- self,
32
- time: Union[TimeRange, List[TimeRange]],
33
- bounds: Optional[Bounds] = None,
34
- area: Optional[BaseGeometry] = None,
35
- search_kwargs: Optional[Dict[str, Any]] = None,
36
- catalog: Optional[CatalogSearcher] = None,
37
- ):
38
- if bounds is None and area is None:
39
- raise ValueError("either bounds or area have to be provided")
40
- elif area is None:
41
- area = Bounds.from_inp(bounds).geometry
42
- self.time = time
43
- self.area = area
44
- self.search_kwargs = search_kwargs or {}
45
- if catalog:
46
- self.catalog = catalog
47
-
48
- def get_catalog_config(self):
49
- return self.catalog.config_cls(**self.search_kwargs)
50
-
51
- def apply_item_modifier_funcs(self, item: Item) -> Item:
52
- try:
53
- for modifier in self.item_modifier_funcs or []:
54
- item = modifier(item)
55
- except GEOSException as exc:
56
- raise ItemGeometryError(
57
- f"item {item.get_self_href()} geometry could not be resolved: {str(exc)}"
58
- )
59
- return item
60
-
61
- def items(self) -> Generator[Item, None, None]:
62
- for item in self.catalog.search(
63
- time=self.time, area=self.area, search_kwargs=self.search_kwargs
64
- ):
65
- yield self.apply_item_modifier_funcs(item)
mapchete_eo/geometry.py DELETED
@@ -1,271 +0,0 @@
1
- import logging
2
- import math
3
- from functools import partial
4
- from typing import Callable, Iterable, Tuple
5
-
6
- from fiona.crs import CRS
7
- from fiona.transform import transform as fiona_transform
8
- from mapchete.geometry import reproject_geometry
9
- from mapchete.types import Bounds, CRSLike
10
- from shapely.geometry import (
11
- GeometryCollection,
12
- LinearRing,
13
- LineString,
14
- MultiLineString,
15
- MultiPoint,
16
- MultiPolygon,
17
- Point,
18
- Polygon,
19
- box,
20
- shape,
21
- )
22
- from shapely.geometry.base import BaseGeometry
23
- from shapely.ops import unary_union
24
-
25
- CoordArrays = Tuple[Iterable[float], Iterable[float]]
26
-
27
-
28
- logger = logging.getLogger(__name__)
29
-
30
-
31
- def transform_to_latlon(
32
- geometry: BaseGeometry, src_crs: CRSLike, width_threshold: float = 180.0
33
- ) -> BaseGeometry:
34
- """Transforms a geometry to lat/lon coordinates.
35
-
36
- If resulting geometry crosses the Antimeridian it will be fixed by moving coordinates
37
- from the Western Hemisphere to outside of the lat/lon bounds on the East, making sure
38
- the correct geometry shape is preserved.
39
-
40
- As a next step, repair_antimeridian_geometry() can be applied, which then splits up
41
- this geometry into a multipart geometry where all of its subgeometries are within the
42
- lat/lon bounds again.
43
- """
44
- latlon_crs = CRS.from_epsg(4326)
45
-
46
- def transform_shift_coords(coords: CoordArrays) -> CoordArrays:
47
- out_x_coords, out_y_coords = fiona_transform(src_crs, latlon_crs, *coords)
48
- if max(out_x_coords) - min(out_x_coords) > width_threshold:
49
- # we probably have an antimeridian crossing here!
50
- out_x_coords, out_y_coords = coords_longitudinal_shift(
51
- coords_transform(coords, src_crs, latlon_crs), only_negative_coords=True
52
- )
53
- return (out_x_coords, out_y_coords)
54
-
55
- return custom_transform(geometry, transform_shift_coords)
56
-
57
-
58
- def repair_antimeridian_geometry(
59
- geometry: BaseGeometry, width_threshold: float = 180.0
60
- ) -> BaseGeometry:
61
- """
62
- Repair geometry and apply fix if it crosses the Antimeridian.
63
-
64
- A geometry crosses the Antimeridian if it is at least partly outside of the
65
- lat/lon bounding box or if its width exceeds a certain threshold. This can happen
66
- after reprojection if the geometry coordinates are transformed separately and land
67
- left and right of the Antimeridian, thus resulting in a polygon spanning almost the
68
- whole lat/lon bounding box width.
69
- """
70
- # repair geometry if it is broken
71
- geometry = geometry.buffer(0)
72
- latlon_bbox = box(-180, -90, 180, 90)
73
-
74
- # only attempt to fix if geometry is too wide or reaches over the lat/lon bounds
75
- if (
76
- Bounds.from_inp(geometry).width >= width_threshold
77
- or not geometry.difference(latlon_bbox).is_empty
78
- ):
79
- # (1) shift only coordinates on the western hemisphere by 360°, thus "fixing"
80
- # the footprint, but letting it cross the antimeridian
81
- shifted_geometry = longitudinal_shift(geometry, only_negative_coords=True)
82
-
83
- # (2) split up geometry in one outside of latlon bounds and one inside
84
- inside = shifted_geometry.intersection(latlon_bbox)
85
- outside = shifted_geometry.difference(latlon_bbox)
86
-
87
- # (3) shift back only the polygon outside of latlon bounds by -360, thus moving
88
- # it back to the western hemisphere
89
- outside_shifted = longitudinal_shift(
90
- outside, offset=-360, only_negative_coords=False
91
- )
92
-
93
- # (4) create a MultiPolygon out from these two polygons
94
- geometry = unary_union([inside, outside_shifted])
95
-
96
- return geometry
97
-
98
-
99
- def buffer_antimeridian_safe(
100
- footprint: BaseGeometry, buffer_m: float = 0
101
- ) -> BaseGeometry:
102
- """Buffer geometry by meters and make it Antimeridian-safe.
103
-
104
- Safe means that if it crosses the Antimeridian and is a MultiPolygon,
105
- the buffer will only be applied to the edges facing away from the Antimeridian
106
- thus leaving the polygon intact if shifted back.
107
- """
108
- if footprint.is_empty:
109
- return footprint
110
-
111
- # repair geometry if it is broken
112
- footprint = footprint.buffer(0)
113
-
114
- if not buffer_m:
115
- return footprint
116
-
117
- if isinstance(footprint, MultiPolygon):
118
- # we have a shifted footprint here!
119
- # (1) unshift one part
120
- subpolygons = []
121
- for polygon in footprint.geoms:
122
- lon = polygon.centroid.x
123
- if lon < 0:
124
- polygon = longitudinal_shift(polygon)
125
- subpolygons.append(polygon)
126
- # (2) merge to single polygon
127
- merged = unary_union(subpolygons)
128
-
129
- # (3) apply buffer
130
- if isinstance(merged, MultiPolygon):
131
- buffered = unary_union(
132
- [
133
- buffer_antimeridian_safe(polygon, buffer_m=buffer_m)
134
- for polygon in merged.geoms
135
- ]
136
- )
137
- else:
138
- buffered = buffer_antimeridian_safe(merged, buffer_m=buffer_m)
139
-
140
- # (4) fix again
141
- return repair_antimeridian_geometry(buffered)
142
-
143
- # UTM zone CRS
144
- utm_crs = latlon_to_utm_crs(footprint.centroid.y, footprint.centroid.x)
145
- latlon_crs = CRS.from_string("EPSG:4326")
146
-
147
- return transform_to_latlon(
148
- reproject_geometry(
149
- footprint, src_crs=latlon_crs, dst_crs=utm_crs, clip_to_crs_bounds=False
150
- ).buffer(buffer_m),
151
- src_crs=utm_crs,
152
- )
153
-
154
-
155
- def longitudinal_shift(
156
- geometry: BaseGeometry, offset: float = 360.0, only_negative_coords: bool = False
157
- ) -> BaseGeometry:
158
- """Return geometry with either all or Western hemisphere coordinates shifted by some offset."""
159
- return custom_transform(
160
- geometry,
161
- partial(
162
- coords_longitudinal_shift,
163
- by=offset,
164
- only_negative_coords=only_negative_coords,
165
- ),
166
- )
167
-
168
-
169
- def latlon_to_utm_crs(lat: float, lon: float) -> CRS:
170
- min_zone = 1
171
- max_zone = 60
172
- utm_zone = (
173
- f"{max([min([(math.floor((lon + 180) / 6) + 1), max_zone]), min_zone]):02}"
174
- )
175
- hemisphere_code = "7" if lat <= 0 else "6"
176
- return CRS.from_string(f"EPSG:32{hemisphere_code}{utm_zone}")
177
-
178
-
179
- def bounds_to_geom(bounds: Bounds) -> BaseGeometry:
180
- # TODO: move into core package
181
- if bounds.left < -180:
182
- part1 = Bounds(-180, bounds.bottom, bounds.right, bounds.top)
183
- part2 = Bounds(bounds.left + 360, bounds.bottom, 180, bounds.top)
184
- return unary_union([shape(part1), shape(part2)])
185
- elif bounds.right > 180:
186
- part1 = Bounds(-180, bounds.bottom, bounds.right - 360, bounds.top)
187
- part2 = Bounds(bounds.left, bounds.bottom, 180, bounds.top)
188
- return unary_union([shape(part1), shape(part2)])
189
- else:
190
- return shape(bounds)
191
-
192
-
193
- def custom_transform(geometry: BaseGeometry, func: Callable) -> BaseGeometry:
194
- # todo: shapely.transform.transform maybe can make this code more simple
195
- # https://shapely.readthedocs.io/en/stable/reference/shapely.transform.html#shapely.transform
196
- def _point(point: Point) -> Point:
197
- return Point(zip(*func(point.xy)))
198
-
199
- def _multipoint(multipoint: MultiPoint) -> MultiPoint:
200
- return MultiPoint([_point(point) for point in multipoint])
201
-
202
- def _linestring(linestring: LineString) -> LineString:
203
- return LineString(zip(*func(linestring.xy)))
204
-
205
- def _multilinestring(multilinestring: MultiLineString) -> MultiLineString:
206
- return MultiLineString(
207
- [_linestring(linestring) for linestring in multilinestring.geoms]
208
- )
209
-
210
- def _linearring(linearring: LinearRing) -> LinearRing:
211
- return LinearRing(((x, y) for x, y in zip(*func(linearring.xy))))
212
-
213
- def _polygon(polygon: Polygon) -> Polygon:
214
- return Polygon(
215
- _linearring(polygon.exterior),
216
- holes=list(map(_linearring, polygon.interiors)),
217
- )
218
-
219
- def _multipolygon(multipolygon: MultiPolygon) -> MultiPolygon:
220
- return MultiPolygon([_polygon(polygon) for polygon in multipolygon.geoms])
221
-
222
- def _geometrycollection(
223
- geometrycollection: GeometryCollection,
224
- ) -> GeometryCollection:
225
- return GeometryCollection(
226
- [_any_geometry(subgeometry) for subgeometry in geometrycollection.geoms]
227
- )
228
-
229
- def _any_geometry(geometry: BaseGeometry) -> BaseGeometry:
230
- transform_funcs = {
231
- Point: _point,
232
- MultiPoint: _multipoint,
233
- LineString: _linestring,
234
- MultiLineString: _multilinestring,
235
- Polygon: _polygon,
236
- MultiPolygon: _multipolygon,
237
- GeometryCollection: _geometrycollection,
238
- }
239
- try:
240
- return transform_funcs[type(geometry)](geometry)
241
- except KeyError:
242
- raise TypeError(f"unknown geometry {geometry} of type {type(geometry)}")
243
-
244
- if geometry.is_empty:
245
- return geometry
246
-
247
- # make valid by buffering
248
- return _any_geometry(geometry).buffer(0)
249
-
250
-
251
- def coords_transform(
252
- coords: CoordArrays, src_crs: CRSLike, dst_crs: CRSLike
253
- ) -> CoordArrays:
254
- return fiona_transform(src_crs, dst_crs, *coords)
255
-
256
-
257
- def coords_longitudinal_shift(
258
- coords: CoordArrays,
259
- by: float = 360,
260
- only_negative_coords: bool = False,
261
- ) -> CoordArrays:
262
- x_coords, y_coords = coords
263
- x_coords = (
264
- (
265
- x_coord + by
266
- if (only_negative_coords and x_coord < 0) or not only_negative_coords
267
- else x_coord
268
- )
269
- for x_coord in x_coords
270
- )
271
- return x_coords, y_coords
@@ -1,42 +0,0 @@
1
- """
2
- Catalogs define access to a search interface which provide products
3
- as pystac Items.
4
- """
5
-
6
- from typing import List
7
-
8
- from mapchete_eo.search import STACSearchCatalog, UTMSearchCatalog
9
-
10
-
11
- class EarthSearchV1S2L2A(STACSearchCatalog):
12
- """Earth-Search catalog for Sentinel-2 Level 2A COGs."""
13
-
14
- endpoint: str = "https://earth-search.aws.element84.com/v1/"
15
-
16
-
17
- class CDSESearch(STACSearchCatalog):
18
- """Copernicus Data Space Ecosystem (CDSE) STAC API."""
19
-
20
- endpoint: str = "https://stac.dataspace.copernicus.eu/v1"
21
-
22
-
23
- class PlanetaryComputerSearch(STACSearchCatalog):
24
- """Planetary Computer Search."""
25
-
26
- endpoint: str = "https://planetarycomputer.microsoft.com/api/stac/v1/"
27
-
28
-
29
- class AWSSearchCatalogS2L2A(UTMSearchCatalog):
30
- """
31
- Not a search endpoint, just hanging STAC collection with items separately.
32
- Need custom parser/browser to find scenes based on date and UTM MGRS Granule
33
-
34
- https://sentinel-s2-l2a-stac.s3.amazonaws.com/sentinel-s2-l2a.json
35
- """
36
-
37
- id: str = "sentinel-s2-l2a"
38
- endpoint: str = "s3://sentinel-s2-l2a-stac/"
39
- day_subdir_schema: str = "{year}/{month:02d}/{day:02d}"
40
- stac_json_endswith: str = "T{tile_id}.json"
41
- description: str = "Sentinel-2 L2A JPEG2000 archive on AWS."
42
- stac_extensions: List[str] = []