Skip to content

Commit 5c91ba2

Browse files
feat(asset): move download_link property to an asset #2029
+ update/complete asset mapping behaviour + update provider configuration + update specific providers + centralize/deduplicate source
1 parent 798a34a commit 5c91ba2

10 files changed

Lines changed: 1242 additions & 296 deletions

File tree

eodag/api/product/_assets.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@
1717
# limitations under the License.
1818
from __future__ import annotations
1919

20+
import logging
2021
import re
2122
from collections import UserDict
22-
from typing import TYPE_CHECKING, Any, Optional
23+
from typing import TYPE_CHECKING, Any, Optional, Union
2324

25+
from typing_extensions import override
26+
27+
from eodag.api.product.metadata_mapping import NOT_AVAILABLE, OFFLINE_STATUS
28+
from eodag.utils import guess_file_type
2429
from eodag.utils.exceptions import NotAvailableError
2530
from eodag.utils.repr import dict_to_html_table
2631

@@ -29,6 +34,8 @@
2934
from eodag.types.download_args import DownloadConf
3035
from eodag.utils import Unpack
3136

37+
logger = logging.getLogger("eodag.api.product.assets")
38+
3239

3340
class AssetsDict(UserDict):
3441
"""A UserDict object which values are :class:`~eodag.api.product._assets.Asset`
@@ -56,12 +63,84 @@ class AssetsDict(UserDict):
5663

5764
product: EOProduct
5865

66+
TECHNICAL_ASSETS = ["download_link", "quicklook", "thumbnail"]
67+
5968
def __init__(self, product: EOProduct, *args: Any, **kwargs: Any) -> None:
6069
self.product = product
6170
super(AssetsDict, self).__init__(*args, **kwargs)
6271

6372
def __setitem__(self, key: str, value: dict[str, Any]) -> None:
73+
if not self._check(key, value):
74+
return
6475
super().__setitem__(key, Asset(self.product, key, value))
76+
self.sort()
77+
78+
@override
79+
def update(self, value: Union[dict[str, Any], AssetsDict]) -> None: # type: ignore
80+
"""Used to self update with exernal value"""
81+
buffer: dict = {}
82+
for key in value:
83+
if self._check(key, value[key]):
84+
buffer[key] = value[key]
85+
super().update(buffer)
86+
self.sort()
87+
88+
def _check(self, asset_key: str, asset_values: dict[str, Any]) -> bool:
89+
90+
# Asset must have href or order_link
91+
href = asset_values.pop("href", None)
92+
if href not in [None, "", NOT_AVAILABLE]:
93+
asset_values["href"] = href
94+
order_link = asset_values.pop("order_link", None)
95+
if order_link not in [None, "", NOT_AVAILABLE]:
96+
asset_values["order_link"] = order_link
97+
98+
if "href" not in asset_values and "order_link" not in asset_values:
99+
logger.warning(
100+
"asset '{}' skipped ignored because neither href nor order_link is available".format(
101+
asset_key
102+
),
103+
)
104+
return False
105+
106+
def target_url(asset: dict) -> Optional[str]:
107+
target_url = None
108+
if "href" in asset:
109+
target_url = asset["href"]
110+
elif "order_link" in asset:
111+
target_url = asset["order_link"]
112+
return target_url
113+
114+
assets = self.as_dict()
115+
used_urls = []
116+
for key in assets:
117+
if key == asset_key:
118+
# Duplicated key
119+
return False
120+
used_urls.append(target_url(assets[key]))
121+
122+
# Prevent asset key / asset target url replication (out from technical ones)
123+
# thumbnail and quicklook can share same url
124+
url = target_url(asset_values)
125+
if asset_key not in AssetsDict.TECHNICAL_ASSETS and (url in used_urls):
126+
# Duplicated url
127+
return False
128+
129+
return True
130+
131+
def sort(self):
132+
"""Used to self sort"""
133+
sorted_assets = {}
134+
data = self.as_dict()
135+
# Keep technical assets first
136+
for key in AssetsDict.TECHNICAL_ASSETS:
137+
if key in data:
138+
sorted_assets[key] = data.pop(key)
139+
# Sort and add others
140+
data = dict(sorted(data.items()))
141+
for key in data:
142+
sorted_assets[key] = data[key]
143+
super().update(sorted_assets)
65144

66145
def as_dict(self) -> dict[str, Any]:
67146
"""Builds a representation of AssetsDict to enable its serialization
@@ -165,14 +244,55 @@ class Asset(UserDict):
165244
"""
166245

167246
product: EOProduct
247+
248+
# Location
249+
location: Optional[str]
250+
remote_location: Optional[str]
251+
252+
# File
168253
size: int
169254
filename: Optional[str]
170255
rel_path: str
171256

172257
def __init__(self, product: EOProduct, key: str, *args: Any, **kwargs: Any) -> None:
173258
self.product = product
174259
self.key = key
260+
self.location = None
261+
self.remote_location = None
175262
super(Asset, self).__init__(*args, **kwargs)
263+
self._update()
264+
265+
def __setitem__(self, key, item):
266+
super().__setitem__(key, item)
267+
self._update()
268+
269+
def _update(self):
270+
271+
title = self.get("title", None)
272+
if title is None:
273+
super().__setitem__("title", self.key)
274+
275+
# Order link behaviour require order:status state
276+
orderlink = self.get("order_link", None)
277+
orderstatus = self.get("order:status", None)
278+
if orderlink is not None and orderstatus is None:
279+
super().__setitem__("order:status", OFFLINE_STATUS)
280+
281+
href = self.get("href", None)
282+
if href is None:
283+
super().__setitem__("href", "")
284+
href = ""
285+
286+
if href != "":
287+
# Provide location and remote_location when undefined and href updated
288+
if self.location is None:
289+
self.location = href
290+
if self.remote_location is None:
291+
self.remote_location = href
292+
# With order behaviour, href can be fill later
293+
content_type = self.get("type", None)
294+
if content_type is None:
295+
super().__setitem__("type", guess_file_type(href))
176296

177297
def as_dict(self) -> dict[str, Any]:
178298
"""Builds a representation of Asset to enable its serialization

eodag/api/product/metadata_mapping.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from datetime import datetime, timedelta
2525
from string import Formatter
2626
from typing import TYPE_CHECKING, Any, AnyStr, Callable, Iterator, Optional, Union, cast
27+
from urllib.parse import unquote
2728

2829
import geojson
2930
import orjson
@@ -189,6 +190,8 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
189190
- ``to_rounded_wkt``: simplify the WKT of a geometry
190191
- ``to_title``: Convert a string to title case
191192
- ``to_upper``: Convert a string to uppercase
193+
- ``url_decode``: Convert a string url_encoded to decoded ones
194+
- ``round``: Convert a string number to another one without decimal part
192195
193196
:param search_param: The string to be formatted
194197
:param args: (optional) Additional arguments to use in the formatting process
@@ -782,6 +785,28 @@ def convert_to_upper(string: str) -> str:
782785
"""Convert a string to uppercase."""
783786
return string.upper()
784787

788+
@staticmethod
789+
def convert_url_decode(string: str):
790+
return unquote(string)
791+
792+
@staticmethod
793+
def convert_round(string: Any) -> str:
794+
"""Convert a number string to integer string"""
795+
if isinstance(string, float):
796+
return str(int(string))
797+
elif isinstance(string, int):
798+
return str(string)
799+
elif isinstance(string, str):
800+
formatted = ""
801+
for i in range(0, len(string)):
802+
char = string[i]
803+
if char == ".":
804+
break
805+
if "0123456789".find(char) >= 0:
806+
formatted += char
807+
return formatted
808+
return string
809+
785810
@staticmethod
786811
def convert_to_title(string: str) -> str:
787812
"""Convert a string to title case."""
@@ -1518,10 +1543,17 @@ def format_query_params(
15181543

15191544
if COMPLEX_QS_REGEX.match(provider_search_param):
15201545
parts = provider_search_param.split("=")
1546+
15211547
if len(parts) == 1:
1522-
formatted_query_param = format_metadata(
1523-
provider_search_param, collection, **query_dict
1524-
)
1548+
1549+
# If part contains something to interprete
1550+
if parts[0].strip("{}").find("{") >= 0:
1551+
formatted_query_param = format_metadata(
1552+
provider_search_param, collection, **query_dict
1553+
)
1554+
else:
1555+
formatted_query_param = "{" + parts[0].strip("{}") + "}"
1556+
15251557
formatted_query_param = formatted_query_param.replace("'", '"')
15261558
if "{{" in provider_search_param:
15271559
# retrieve values from hashes where keys are given in the param

eodag/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ class MetadataPreMapping(TypedDict, total=False):
580580

581581
def __or__(self, other: Union[Self, dict[str, Any]]) -> Self:
582582
"""Return a new PluginConfig with merged values."""
583-
new_config = self.__class__.from_mapping(self.__dict__)
583+
new_config: Self = self.__class__.from_mapping(self.__dict__)
584584
new_config.update(other)
585585
return new_config
586586

0 commit comments

Comments
 (0)