mapchete-eo 2025.11.0__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.
mapchete_eo/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.11.0"
1
+ __version__ = "2026.1.0"
mapchete_eo/io/assets.py CHANGED
@@ -32,7 +32,7 @@ logger = logging.getLogger(__name__)
32
32
 
33
33
 
34
34
  class STACRasterBandProperties(BaseModel):
35
- nodata: NodataVal = None
35
+ nodata: Optional[NodataVal] = None
36
36
  data_type: Optional[str] = None
37
37
  scale: float = 1.0
38
38
  offset: float = 0.0
@@ -40,9 +40,9 @@ class STACRasterBandProperties(BaseModel):
40
40
  @staticmethod
41
41
  def from_asset(
42
42
  asset: pystac.Asset,
43
- nodataval: NodataVal = None,
43
+ nodataval: Optional[NodataVal] = None,
44
44
  ) -> STACRasterBandProperties:
45
- if asset.extra_fields.get("raster:offset") is not None:
45
+ if asset.extra_fields.get("raster:offset", {}):
46
46
  properties = dict(
47
47
  offset=asset.extra_fields.get("raster:offset"),
48
48
  scale=asset.extra_fields.get("raster:scale"),
@@ -87,16 +87,21 @@ def asset_to_np_array(
87
87
  )
88
88
 
89
89
  logger.debug("reading asset %s and indexes %s ...", asset, indexes)
90
- data = read_raster(
90
+ array = read_raster(
91
91
  inp=path,
92
92
  indexes=indexes,
93
93
  grid=grid,
94
94
  resampling=resampling.name,
95
95
  dst_nodata=band_properties.nodata,
96
- ).data
97
-
96
+ ).array
98
97
  if apply_offset and band_properties.offset:
99
- data_type = band_properties.data_type or data.dtype
98
+ logger.debug(
99
+ "apply offset %s and scale %s to asset %s",
100
+ band_properties.offset,
101
+ band_properties.scale,
102
+ asset,
103
+ )
104
+ data_type = band_properties.data_type or array.dtype
100
105
 
101
106
  # determine value range for the target data_type
102
107
  clip_min, clip_max = dtype_ranges[str(data_type)]
@@ -105,9 +110,9 @@ def asset_to_np_array(
105
110
  if clip_min == band_properties.nodata:
106
111
  clip_min += 1
107
112
 
108
- data[:] = (
113
+ array[~array.mask] = (
109
114
  (
110
- ((data * band_properties.scale) + band_properties.offset)
115
+ ((array[~array.mask] * band_properties.scale) + band_properties.offset)
111
116
  / band_properties.scale
112
117
  )
113
118
  .round()
@@ -115,8 +120,7 @@ def asset_to_np_array(
115
120
  .astype(data_type, copy=False)
116
121
  .data
117
122
  )
118
-
119
- return data
123
+ return array
120
124
 
121
125
 
122
126
  def get_assets(
@@ -2,9 +2,10 @@ from functools import cached_property
2
2
  import json
3
3
  import logging
4
4
  from abc import ABC, abstractmethod
5
- from typing import Any, Callable, Dict, Generator, List, Optional, Type, Union
5
+ from typing import Any, Callable, Dict, Generator, List, Optional, Set, Type, Union
6
6
 
7
- from cql2 import Expr
7
+ from pygeofilter.parsers.ecql import parse as parse_ecql
8
+ from pygeofilter.backends.native.evaluate import NativeEvaluator
8
9
  from pydantic import BaseModel
9
10
  from mapchete.path import MPath, MPathLike
10
11
  from mapchete.types import Bounds
@@ -17,6 +18,8 @@ from rasterio.profiles import Profile
17
18
  from shapely.geometry.base import BaseGeometry
18
19
 
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
20
23
  from mapchete_eo.types import TimeRange
21
24
 
22
25
  logger = logging.getLogger(__name__)
@@ -53,6 +56,11 @@ class CollectionSearcher(ABC):
53
56
  config_cls: Type[BaseModel]
54
57
  collection: str
55
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
+ )
56
64
 
57
65
  def __init__(
58
66
  self,
@@ -70,17 +78,21 @@ class CollectionSearcher(ABC):
70
78
  @cached_property
71
79
  def eo_bands(self) -> List[str]: ...
72
80
 
73
- @abstractmethod
81
+ @property
82
+ def config(self) -> BaseModel:
83
+ return self.config_cls()
84
+
74
85
  @cached_property
75
- def id(self) -> str: ...
86
+ def id(self) -> str:
87
+ return self.client.id
76
88
 
77
- @abstractmethod
78
89
  @cached_property
79
- def description(self) -> str: ...
90
+ def description(self) -> str:
91
+ return self.client.description
80
92
 
81
- @abstractmethod
82
93
  @cached_property
83
- def stac_extensions(self) -> List[str]: ...
94
+ def stac_extensions(self) -> List[str]:
95
+ return self.client.stac_extensions
84
96
 
85
97
  @abstractmethod
86
98
  def search(
@@ -240,9 +252,12 @@ def filter_items(
240
252
  and passed down to the individual search approaches via said config and this Function.
241
253
  """
242
254
  if query:
243
- expr = Expr(query)
255
+ ast = parse_ecql(query)
256
+ evaluator = NativeEvaluator(use_getattr=False)
257
+ filter_func = evaluator.evaluate(ast)
244
258
  for item in items:
245
- if expr.matches(item.properties):
259
+ # pystac items store metadata in 'properties'
260
+ if filter_func(item.properties):
246
261
  yield item
247
262
  else:
248
263
  yield from items
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+
4
+ from contextlib import contextmanager
1
5
  from typing import Optional, Dict, Any
2
6
 
3
7
  from mapchete.path import MPath, MPathLike
@@ -46,18 +50,64 @@ class UTMSearchConfig(BaseModel):
46
50
  path=MPath(
47
51
  "https://sentinel-s2-l2a-stac.s3.amazonaws.com/sentinel-s2-l2a.json"
48
52
  ),
53
+ endpoint="s3://sentinel-s2-l2a-stac",
49
54
  ),
50
55
  S2_L1C=dict(
51
56
  id="sentinel-s2-l1c",
52
57
  path=MPath(
53
58
  "https://sentinel-s2-l1c-stac.s3.amazonaws.com/sentinel-s2-l1c.json"
54
59
  ),
60
+ endpoint="s3://sentinel-s2-l1c-stac",
55
61
  ),
56
62
  S1_GRD=dict(
57
63
  id="sentinel-s1-l1c",
58
64
  path=MPath(
59
65
  "https://sentinel-s1-l1c-stac.s3.amazonaws.com/sentinel-s1-l1c.json"
60
66
  ),
67
+ endpoint="s3://sentinel-s1-l1c-stac",
61
68
  ),
62
69
  )
63
70
  search_index: Optional[MPathLike] = None
71
+
72
+
73
+ @contextmanager
74
+ def patch_invalid_assets():
75
+ """
76
+ Context manager/decorator to fix pystac crash on malformed assets (strings instead of dicts).
77
+
78
+ """
79
+ try:
80
+ from pystac.extensions.file import FileExtensionHooks
81
+ except ImportError: # pragma: no cover
82
+ yield
83
+ return
84
+
85
+ logger = logging.getLogger(__name__)
86
+
87
+ _original_migrate = FileExtensionHooks.migrate
88
+
89
+ def _safe_migrate(self, obj, version, info):
90
+ if "assets" in obj and isinstance(obj["assets"], dict):
91
+ bad_keys = []
92
+ for key, asset in obj["assets"].items():
93
+ if not isinstance(asset, dict):
94
+ logger.debug(
95
+ "Removing malformed asset '%s' (type %s) from item %s",
96
+ key,
97
+ type(asset),
98
+ obj.get("id", "unknown"),
99
+ )
100
+ bad_keys.append(key)
101
+
102
+ for key in bad_keys:
103
+ del obj["assets"][key]
104
+
105
+ return _original_migrate(self, obj, version, info)
106
+
107
+ # Apply patch
108
+ FileExtensionHooks.migrate = _safe_migrate
109
+ try:
110
+ yield
111
+ finally:
112
+ # Restore original
113
+ FileExtensionHooks.migrate = _original_migrate
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
  from datetime import datetime
5
5
  from functools import cached_property
6
- from typing import Any, Dict, Generator, Iterator, List, Optional, Set, Union
6
+ from typing import Any, Dict, Generator, Iterator, List, Optional, Union
7
7
 
8
8
  from mapchete import Timer
9
9
  from mapchete.tile import BufferedTilePyramid
@@ -13,10 +13,8 @@ from pystac_client import Client, CollectionClient, ItemSearch
13
13
  from shapely.geometry import shape, box
14
14
  from shapely.geometry.base import BaseGeometry
15
15
 
16
- from mapchete_eo.product import blacklist_products
17
16
  from mapchete_eo.search.base import CollectionSearcher, StaticCollectionWriterMixin
18
- from mapchete_eo.search.config import StacSearchConfig
19
- from mapchete_eo.settings import mapchete_eo_settings
17
+ from mapchete_eo.search.config import StacSearchConfig, patch_invalid_assets
20
18
  from mapchete_eo.types import TimeRange
21
19
 
22
20
  logger = logging.getLogger(__name__)
@@ -24,11 +22,6 @@ logger = logging.getLogger(__name__)
24
22
 
25
23
  class STACSearchCollection(StaticCollectionWriterMixin, CollectionSearcher):
26
24
  collection: str
27
- blacklist: Set[str] = (
28
- blacklist_products(mapchete_eo_settings.blacklist)
29
- if mapchete_eo_settings.blacklist
30
- else set()
31
- )
32
25
  config_cls = StacSearchConfig
33
26
 
34
27
  @cached_property
@@ -45,18 +38,6 @@ class STACSearchCollection(StaticCollectionWriterMixin, CollectionSearcher):
45
38
  logger.debug("cannot find eo:bands definition from collections")
46
39
  return []
47
40
 
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
-
60
41
  def search(
61
42
  self,
62
43
  time: Optional[Union[TimeRange, List[TimeRange]]] = None,
@@ -138,14 +119,16 @@ class STACSearchCollection(StaticCollectionWriterMixin, CollectionSearcher):
138
119
  query=query,
139
120
  )
140
121
 
141
- for search in _searches():
142
- for item in search.items():
143
- if item.get_self_href() in self.blacklist: # pragma: no cover
144
- logger.debug(
145
- "item %s found in blacklist and skipping", item.get_self_href()
146
- )
147
- continue
148
- yield item
122
+ with patch_invalid_assets():
123
+ for search in _searches():
124
+ for item in search.items():
125
+ if item.get_self_href() in self.blacklist: # pragma: no cover
126
+ logger.debug(
127
+ "item %s found in blacklist and skipping",
128
+ item.get_self_href(),
129
+ )
130
+ continue
131
+ yield item
149
132
 
150
133
  @cached_property
151
134
  def default_search_params(self):
@@ -65,18 +65,6 @@ class STACStaticCollection(StaticCollectionWriterMixin, CollectionSearcher):
65
65
  logger.debug("cannot find eo:bands definition")
66
66
  return []
67
67
 
68
- @cached_property
69
- def id(self) -> str:
70
- return self.client.id
71
-
72
- @cached_property
73
- def description(self) -> str:
74
- return self.client.description
75
-
76
- @cached_property
77
- def stac_extensions(self) -> List[str]:
78
- return self.client.stac_extensions
79
-
80
68
  def search(
81
69
  self,
82
70
  time: Optional[Union[TimeRange, List[TimeRange]]] = None,
@@ -1,19 +1,19 @@
1
1
  import datetime
2
2
  from functools import cached_property
3
3
  import logging
4
- from typing import Any, Dict, Generator, List, Optional, Set, Union
4
+ from typing import Any, Dict, Generator, List, Optional, Union
5
5
 
6
6
  from mapchete.io.vector import fiona_open
7
7
  from mapchete.path import MPath, MPathLike
8
8
  from mapchete.types import Bounds, BoundsLike
9
9
  from pystac.collection import Collection
10
10
  from pystac.item import Item
11
+ from pystac_client import CollectionClient
11
12
  from shapely.errors import GEOSException
12
13
  from shapely.geometry import shape
13
14
  from shapely.geometry.base import BaseGeometry
14
15
 
15
16
  from mapchete_eo.exceptions import ItemGeometryError
16
- from mapchete_eo.product import blacklist_products
17
17
  from mapchete_eo.search.base import (
18
18
  CollectionSearcher,
19
19
  StaticCollectionWriterMixin,
@@ -21,7 +21,6 @@ from mapchete_eo.search.base import (
21
21
  )
22
22
  from mapchete_eo.search.config import UTMSearchConfig
23
23
  from mapchete_eo.search.s2_mgrs import S2Tile, s2_tiles_from_bounds
24
- from mapchete_eo.settings import mapchete_eo_settings
25
24
  from mapchete_eo.time import day_range, to_datetime
26
25
  from mapchete_eo.types import TimeRange
27
26
 
@@ -29,19 +28,24 @@ logger = logging.getLogger(__name__)
29
28
 
30
29
 
31
30
  class UTMSearchCatalog(StaticCollectionWriterMixin, CollectionSearcher):
32
- endpoint: str
33
- id: str
34
- day_subdir_schema: str
35
- stac_json_endswith: str
36
- description: str
37
- stac_extensions: List[str]
38
- blacklist: Set[str] = (
39
- blacklist_products(mapchete_eo_settings.blacklist)
40
- if mapchete_eo_settings.blacklist
41
- else set()
42
- )
43
31
  config_cls = UTMSearchConfig
44
32
 
33
+ @cached_property
34
+ def endpoint(self) -> Optional[str]:
35
+ for collection_properties in self.config.sinergise_aws_collections.values():
36
+ if collection_properties["id"] == self.collection.split("/")[-1].replace(
37
+ ".json", ""
38
+ ):
39
+ return collection_properties.get("endpoint")
40
+ return None
41
+
42
+ day_subdir_schema: str = "{year}/{month:02d}/{day:02d}"
43
+ stac_json_endswith: str = "T{tile_id}.json"
44
+
45
+ @cached_property
46
+ def client(self) -> CollectionClient:
47
+ return next(self.get_collections())
48
+
45
49
  @cached_property
46
50
  def eo_bands(self) -> List[str]: # pragma: no cover
47
51
  for (
@@ -85,8 +89,9 @@ class UTMSearchCatalog(StaticCollectionWriterMixin, CollectionSearcher):
85
89
  time: Optional[Union[TimeRange, List[TimeRange]]] = None,
86
90
  bounds: Optional[Bounds] = None,
87
91
  area: Optional[BaseGeometry] = None,
88
- config: UTMSearchConfig = UTMSearchConfig(),
92
+ config: Optional[UTMSearchConfig] = None,
89
93
  ) -> Generator[Item, None, None]:
94
+ config = config or UTMSearchConfig()
90
95
  if time is None:
91
96
  raise ValueError("time must be given")
92
97
  if area is not None and area.is_empty:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapchete-eo
3
- Version: 2025.11.0
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>
@@ -15,13 +15,13 @@ Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Topic :: Scientific/Engineering :: GIS
17
17
  Requires-Dist: click
18
- Requires-Dist: cql2
19
18
  Requires-Dist: croniter
20
19
  Requires-Dist: lxml
21
20
  Requires-Dist: mapchete[complete]>=2025.10.0
22
21
  Requires-Dist: opencv-python-headless
23
22
  Requires-Dist: pillow
24
23
  Requires-Dist: pydantic
24
+ Requires-Dist: pygeofilter
25
25
  Requires-Dist: pystac-client>=0.7.5
26
26
  Requires-Dist: pystac[urllib3]>=1.12.2
27
27
  Requires-Dist: retry
@@ -33,9 +33,9 @@ Provides-Extra: docs
33
33
  Requires-Dist: sphinx; extra == 'docs'
34
34
  Requires-Dist: sphinx-rtd-theme; extra == 'docs'
35
35
  Provides-Extra: test
36
+ Requires-Dist: pytest; extra == 'test'
36
37
  Requires-Dist: pytest-coverage; extra == 'test'
37
- Requires-Dist: pytest-lazy-fixture; extra == 'test'
38
- Requires-Dist: pytest<8; extra == 'test'
38
+ Requires-Dist: pytest-lazy-fixtures; extra == 'test'
39
39
  Description-Content-Type: text/x-rst
40
40
 
41
41
  .. image:: logo/mapchete_eo.svg
@@ -1,4 +1,4 @@
1
- mapchete_eo/__init__.py,sha256=2E0oLwEHFZUcghiMB3GjI_PKEVs-4pasu50WwMW-H2A,26
1
+ mapchete_eo/__init__.py,sha256=Q2R77nEGEKjxOBz3qMm2JGtkZn-nVFmReBAVH_YeZ4M,25
2
2
  mapchete_eo/base.py,sha256=_6wbR9IZBBBdRBbZY4FEQ0uu603C-tMPYK4MGQ5xSYY,21953
3
3
  mapchete_eo/blacklist.txt,sha256=6KhBY0jNjXgRpKRvKJoOTswvNUoP56IrIcNeCYnd2qo,17471
4
4
  mapchete_eo/eostac.py,sha256=M-EL8y8pPw8H9hb85FPxSl-1FL_kIYOzxsTIhkrbBZY,570
@@ -36,7 +36,7 @@ mapchete_eo/image_operations/filters.py,sha256=sa_Igv0zMIANHLEALVi8mxohZMoSrdQc6
36
36
  mapchete_eo/image_operations/linear_normalization.py,sha256=-eQX3WLCUYWUv-um3s1u33rgjAwCz84Ht8gcPsMWtJE,2468
37
37
  mapchete_eo/image_operations/sigmoidal.py,sha256=IKU8F89HhQJWGUVmIrnkuhqzn_ztlGrTf8xXZaVQWzU,3575
38
38
  mapchete_eo/io/__init__.py,sha256=1-1g4cESZZREvyumEUABZhDwgVuSxtxdqbNlLuVKlow,950
39
- mapchete_eo/io/assets.py,sha256=E7phBOu8JaqyRhuEwl7wnmWHvG1yY1sw38QaMCuYkB4,16998
39
+ mapchete_eo/io/assets.py,sha256=OVXLxrwCCUa6frP-xpm2CL30MR8eMABnwVD6EpN1cqU,17215
40
40
  mapchete_eo/io/items.py,sha256=5H2xWAyLptDuuHh0rjY1MXReJomV0XBZgMr_rpdpYXU,6244
41
41
  mapchete_eo/io/levelled_cubes.py,sha256=ZF7BLn9MHnJCCDjAoR9D7MNbHdusjJ9EACdM7rKNlyM,9018
42
42
  mapchete_eo/io/path.py,sha256=fCjvowd9rGJbGio5Z-jDLxa49M2BVurYM9hJy53Im48,4831
@@ -76,14 +76,14 @@ mapchete_eo/processes/dtype_scale.py,sha256=7hJlIe2DsE6Kmk1c3PLwLr8rnn_4_9S6Cz7b
76
76
  mapchete_eo/processes/eo_to_xarray.py,sha256=Gcp4qju2C9S8KeUnVY5f3nAsrdckPhGRguzsgRtWBFs,514
77
77
  mapchete_eo/processes/merge_rasters.py,sha256=jSXUI8pEWXbQWrM9iMtrH4ACH2VkCaBUH9CQg4z6zLA,8486
78
78
  mapchete_eo/search/__init__.py,sha256=bHvaqoHrtoNyO8_3kyJfvhh1yCz0hYvU2fzXhjOXNhU,528
79
- mapchete_eo/search/base.py,sha256=Xdhq7js96erhV5qgAeKviUakF4qJmzUkbvprrTCP6Zc,9107
80
- mapchete_eo/search/config.py,sha256=-3TzCBzjTIkCVpuoJMDDShHh4rm_9_QFKYruedEwbjY,2241
79
+ mapchete_eo/search/base.py,sha256=B8vlwgvDYRqxHre5aYWfPZ0tmUic-mb1VBnzl0llUSQ,9757
80
+ mapchete_eo/search/config.py,sha256=L3DBEuKNGh_WXi8_Ha-qj6HW9ksRioYeFyPYGGN09qA,3696
81
81
  mapchete_eo/search/s2_mgrs.py,sha256=x12m-tJZNc41lFHNR2h7eSyEZ-ceBO4Gdz785btcVN8,10859
82
- mapchete_eo/search/stac_search.py,sha256=p38veJiUZtEzXq7KJJ0YzqeEG1L87Q3CLdPIME1RQ3A,10504
83
- mapchete_eo/search/stac_static.py,sha256=nPvJPayImPHkjVr4XEDp7ucOOhbDpUTlGyMJeaRFmAI,7260
84
- mapchete_eo/search/utm_search.py,sha256=6T9HtbgT8mrWjzX_7_fkggmQBCWDBhf5i7BJ9dk6BZw,9230
85
- mapchete_eo-2025.11.0.dist-info/METADATA,sha256=TkqXdAgVkEPhNiAbMS4VSXUwuhKGnITHET5MyvgGwDQ,3257
86
- mapchete_eo-2025.11.0.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
87
- mapchete_eo-2025.11.0.dist-info/entry_points.txt,sha256=edU29CA3j8hwBafEd6jO2Zcou1sGkyjNro-y4_LnAnw,320
88
- mapchete_eo-2025.11.0.dist-info/licenses/LICENSE,sha256=TC5JwvBnFrUgsSQSCDFPc3cqlbth2N0q8MWrhY1EVd0,1089
89
- mapchete_eo-2025.11.0.dist-info/RECORD,,
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
@@ -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