Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions eodag/api/product/_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ def __init__(self, product: EOProduct, *args: Any, **kwargs: Any) -> None:
self.product = product
super(AssetsDict, self).__init__(*args, **kwargs)

def update(self, data: dict[str, Any]) -> None: # type: ignore
"""Update assets"""
super().update(data)

def __setitem__(self, key: str, value: dict[str, Any]) -> None:
super().__setitem__(key, Asset(self.product, key, value))

Expand Down
11 changes: 10 additions & 1 deletion eodag/api/product/_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
AssetsDict,
)
except ImportError:
from eodag.api.product._assets import AssetsDict
from ._assets import AssetsDict

from eodag.api.product.drivers import DRIVERS
from eodag.api.product.drivers.generic import GenericDriver
Expand All @@ -52,6 +52,7 @@
NOT_AVAILABLE,
NOT_MAPPED,
ONLINE_STATUS,
normalize_bands,
)
from eodag.utils import (
DEFAULT_DOWNLOAD_TIMEOUT,
Expand Down Expand Up @@ -424,6 +425,14 @@ def from_geojson(cls, feature: dict[str, Any]) -> EOProduct:
# https://gist.github.com/sgillies/2217756)
__geo_interface__ = property(as_dict)

def _normalize_bands(self) -> None:
"""Normalize bands in properties and each asset
from STAC 1.0 (``eo:bands`` / ``raster:bands``) to STAC 1.1 (``bands``), in place.
"""
normalize_bands(self.properties)
for key in self.assets:
normalize_bands(self.assets[key])

def __repr__(self) -> str:
try:
return "{}(id={}, provider={})".format(
Expand Down
115 changes: 115 additions & 0 deletions eodag/api/product/metadata_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from shapely.geometry import MultiPolygon, Polygon
from shapely.ops import transform

from eodag.api.product._assets import Asset
from eodag.types.queryables import Queryables
from eodag.utils import (
DEFAULT_PROJ,
Expand Down Expand Up @@ -1812,3 +1813,117 @@ def get_provider_queryable_key(
return ""
else:
return eodag_key


def normalize_bands(data: Union[dict, Asset]) -> Union[dict, Asset]:
"""Migrate ``eo:bands`` / ``raster:bands`` of ``data`` into a STAC 1.1
``bands`` array, in place. Returns ``data`` for convenience.

:param data: properties dict or Asset to migrate
:returns: the same data with migrated bands
"""
UNPREFIX_BAND_FIELDNAME = [
"name",
"description",
"data_type",
"nodata",
"unit",
"statistics",
]
EXCLUDE_MOVE_TO_PARENT_BAND_FIELDNAME = ["name", "eo:common_name"]

# https://github.com/radiantearth/stac-spec/blob/v1.1.0/best-practices.md#bands
# Migrate band STAC 1.0 to 1.1
if isinstance(data, dict) or isinstance(data, Asset):

# Gather eo:band et raster:bands
bands: dict[str, Any] = {"eo:bands": [], "raster:bands": []}
hasData = False
for fieldname in bands:
if fieldname in data:
if isinstance(data[fieldname], list):
bands[fieldname] = data[fieldname]
else:
bands[fieldname] = [data[fieldname]]
hasData = True
del data[fieldname]

if hasData:
processed_bands = []

# migrate eo:bands -> bands
if len(bands["eo:bands"]) > 0:
for item in bands["eo:bands"]:
band = {}
for key in item:
if key in UNPREFIX_BAND_FIELDNAME:
band[key] = item[key]
else:
band["eo:{}".format(key)] = item[key]
processed_bands.append(band)

# migrate raster:bands -> bands
if len(bands["raster:bands"]) > 0:
index = 0
for item in bands["raster:bands"]:
band = processed_bands[index]
for key in item:
if key in UNPREFIX_BAND_FIELDNAME:
band[key] = item[key]
else:
band["raster:{}".format(key)] = item[key]
if index < len(processed_bands):
processed_bands[index] = band
else:
processed_bands.append(band)
index += 1

# When a property has the same value for each band, move it in parent scope
if len(processed_bands) > 0:
field_values: dict[str, Any] = {}

# Lists each distinct value for a field of the same name on each band
for band in processed_bands:
for key in band:
if key not in field_values:
field_values[key] = []
if band[key] not in field_values[key]:
field_values[key].append(band[key])

# Move band fields from asset to parent if all fields shared same value
# (distinct values == 1)
remove_band_fields = []
for key in field_values:
if (
key in EXCLUDE_MOVE_TO_PARENT_BAND_FIELDNAME
or len(field_values[key]) != 1
):
continue
# Do not overwrite a value already set on the parent
# (e.g. an Asset's own `description`); keep the
# per-band value on the `bands` array instead.
if key in data and data[key] != field_values[key][0]:
continue
# All bands have same value
data[key] = field_values[key][0]
# Tag field "to remove" from assets
remove_band_fields.append(key)
del field_values

# Remove from assets field moved to parent
cleaned_bands = []
for band in processed_bands:
cleaned_band = {}
for key in band:
if key not in remove_band_fields:
cleaned_band[key] = band[key]
if len(list(cleaned_band.keys())) > 0:
cleaned_bands.append(cleaned_band)
processed_bands = cleaned_bands
del cleaned_bands

# Remap band field if contains at least one value
if len(processed_bands) > 0:
data["bands"] = processed_bands

return data
1 change: 1 addition & 0 deletions eodag/plugins/search/cop_marine.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ def _create_product(
assets.update(additional_assets)
product = EOProduct(self.provider, properties, collection=collection)
product.assets = AssetsDict(product, assets)
product._normalize_bands()
return product

def query(
Expand Down
23 changes: 12 additions & 11 deletions eodag/plugins/search/qssearch.py
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,7 @@ def normalize_results(
products: list[EOProduct] = []
asset_key_from_href = getattr(self.config, "asset_key_from_href", True)
product_kwargs = deepcopy(kwargs)

# collection alias as collection property for product
if alias := getattr(self.config, "collection_config", {}).get("alias"):
product_kwargs["collection"] = alias
Expand All @@ -1314,25 +1315,25 @@ def normalize_results(

additional_assets = self.get_assets_from_mapping(result)
product.assets.update(additional_assets)

# move assets from properties to product's attr, normalize keys & roles
for key, asset in product.properties.pop("assets", {}).items():
norm_key, norm_roles = product.driver.guess_asset_key_and_roles(
asset.get("href", "") if asset_key_from_href else key,
url = asset.get("href", "")
norm_key, roles = product.driver.guess_asset_key_and_roles(
url if asset_key_from_href else key,
product,
)
# Keep original key and roles if driver couldn't guess
# (e.g., filename without extension)
if norm_key is None:
norm_key = key
else:
asset["roles"] = norm_roles
if norm_key is not None:
asset["title"] = norm_key
if norm_key:
asset["roles"] = roles
product.assets[norm_key] = asset
# Set title if not already set
product.assets[norm_key].setdefault("title", norm_key)
else:
asset["title"] = asset.get("title", key)
product.assets[key] = asset

# sort assets
product.assets.data = dict(sorted(product.assets.data.items()))
product._normalize_bands()
products.append(product)
return products

Expand Down
2 changes: 2 additions & 0 deletions eodag/resources/providers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2814,6 +2814,8 @@
start_datetime: properties.datetime
platform: properties.platform
metadata_mapping:
created: '$.properties.created'
description: '$.properties.description'
grid:code:
- '{{"query":{{"s2:mgrs_tile":{{"eq":"{grid:code#replace_str("MGRS-","")}"}}}}}}'
- '{$.properties."s2:mgrs_tile"#replace_str(r"^T?(.*)$",r"MGRS-\1")}'
Expand Down
7 changes: 4 additions & 3 deletions eodag/utils/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,20 +557,21 @@ def update_assets_from_s3(
).get("Contents", [])
]

assets = {}
for asset_url in assets_urls:
out_of_zip_url = asset_url.split("!")[-1]
key, roles = product.driver.guess_asset_key_and_roles(
out_of_zip_url, product
)

if key and key not in product.assets:
product.assets[key] = {
assets[key] = {
"title": key, # Normalize title with key
"roles": roles,
"href": asset_url,
}
if mime_type := guess_file_type(asset_url):
product.assets[key]["type"] = mime_type
assets[key]["type"] = mime_type
product.assets.update(assets)

# sort assets
product.assets.data = dict(sorted(product.assets.data.items()))
Expand Down
Loading
Loading