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
117 changes: 117 additions & 0 deletions eodag/plugins/search/geodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# Copyright 2026, CS GROUP - France, https://www.csgroup.eu/
#
# This file is part of EODAG project
# https://www.github.com/CS-SI/EODAG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from typing import Any, List

from eodag.api.product import EOProduct # type: ignore
from eodag.api.product.metadata_mapping import OFFLINE_STATUS, ONLINE_STATUS
from eodag.api.search_result import RawSearchResult
from eodag.plugins.search import PreparedSearch
from eodag.plugins.search.qssearch import StacSearch

logger = logging.getLogger("eodag.search.geodes")


class GeodesSearch(StacSearch):
"""``GeodesSearch`` is an extension of :class:`~eodag.plugins.search.qssearch.StacSearch`.

It executes a Search on given STAC API endpoint and updates assets with content listed by the plugin using
``eodag:download_link`` :class:`~eodag.api.product._product.EOProduct` property.

:param provider: provider name
:param config: It has the same Search plugin configuration as :class:`~eodag.plugins.search.qssearch.StacSearch` and
one additional parameter:

* :attr:`~eodag.config.PluginConfig.s3_endpoint` (``str``): s3 endpoint if not hosted on AWS
"""

def __init__(self, provider, config):
super(GeodesSearch, self).__init__(provider, config)

def _get_availability(self, products: list[EOProduct]) -> dict[str, Any]:
"""Get availability information for the products from the provider's 'fastavailability' endpoint."""
body: dict[str, list] = {"availability": []}
for product in products:
download_link = product.properties.get("eodag:download_link")
endpoint_url = product.properties.get("geodes:endpoint_url")
if download_link and endpoint_url:
body["availability"].append(
{"href": download_link, "endpointURL": endpoint_url}
)

url = self.config.api_endpoint.replace("api/stac/search", "fastavailability")
prep = PreparedSearch(url=url)
prep.query_params = body

logger.debug("Get products availability information from %s", url)
resp = self._request(prep)

return resp.json()

def _set_availability(self, products: list[EOProduct]) -> None:
"""Set availability information on the products."""
availability_dict = self._get_availability(products)
updated = 0

for product in products:
download_link = product.properties.get("eodag:download_link")

# find matching product
product_availability_list = [
a
for a in availability_dict.get("products", [])
if a.get("id") in download_link
]
if len(product_availability_list) != 1:
continue
product_availability = product_availability_list[0]

# find matching asset
asset_availability_list = [
a.get("available")
for a in product_availability.get("files", {})
if a.get("checksum") in download_link
]
if len(asset_availability_list) != 1:
continue
asset_availability = asset_availability_list[0]

# set status
product.properties["order:status"] = (
ONLINE_STATUS if asset_availability else OFFLINE_STATUS
)

updated += 1

if updated < len(products):
logger.warning(
"Could not update availability for %d out of %d products",
len(products) - updated,
len(products),
)

def normalize_results(
self, results: RawSearchResult, **kwargs: Any
) -> List[EOProduct]:
"""Build EOProducts from provider results"""

products = super(GeodesSearch, self).normalize_results(results, **kwargs)

self._set_availability(products)

return products
16 changes: 14 additions & 2 deletions eodag/plugins/search/qssearch.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
REQ_RETRY_BACKOFF_FACTOR,
REQ_RETRY_STATUS_FORCELIST,
REQ_RETRY_TOTAL,
STAC_SEARCH_PLUGINS,
USER_AGENT,
copy_deepcopy,
deepcopy,
Expand Down Expand Up @@ -2102,8 +2103,19 @@ class StacSearch(PostJsonSearch):
"""

def __init__(self, provider: str, config: PluginConfig) -> None:
# backup results_entry overwritten by init
results_entry = config.results_entry
try:
# backup results_entry overwritten by init
results_entry = config.results_entry
except AttributeError:
plugin_name = self.__class__.__name__
if plugin_name not in STAC_SEARCH_PLUGINS:
raise MisconfiguredError(
"Missing results_entry in %s configuration. If %s is expected to be used as "
"a STAC plugin, it must be referenced in STAC_SEARCH_PLUGINS."
% (provider, plugin_name)
)
else:
raise

super(StacSearch, self).__init__(provider, config)

Expand Down
6 changes: 3 additions & 3 deletions eodag/resources/providers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5934,7 +5934,7 @@
description: French National Space Agency (CNES) Earth Observation portal
url: https://geodes.cnes.fr
search: !plugin
type: StacSearch
type: GeodesSearch
api_endpoint: https://geodes-portal.cnes.fr/api/stac/search
need_auth: false
asset_key_from_href: false
Expand Down Expand Up @@ -6017,8 +6017,8 @@
eodag:download_link: '$.assets[?(@.roles[0] == "data") & (@.type != "application/xml")].href'
eodag:quicklook: '$.assets[?(@.roles[0] == "overview")].href.`sub(/^(.*)$/, \\1?scope=gdh)`'
eodag:thumbnail: '$.assets[?(@.roles[0] == "overview")].href.`sub(/^(.*)$/, \\1?scope=gdh)`'
# order:status set to succeeded for consistency between providers
order:status: '{$.null#replace_str("Not Available","succeeded")}'
# order:status set to orderable by default and then updated from plugin
order:status: '{$.null#replace_str("Not Available","orderable")}'
products:
S1_SAR_OCN:
product:type: OCN
Expand Down
1 change: 1 addition & 0 deletions eodag/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@

#: List of known STAC search plugins. Required to complete plugin configuration with STAC plugins specific features.
STAC_SEARCH_PLUGINS = [
"GeodesSearch",
"StacSearch",
"StacListAssets",
"StaticStacSearch",
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ CreodiasS3Search = "eodag.plugins.search.creodias_s3:CreodiasS3Search"
CopMarineSearch = "eodag.plugins.search.cop_marine:CopMarineSearch"
StacListAssets = "eodag.plugins.search.stac_list_assets:StacListAssets"
CopGhslSearch = "eodag.plugins.search.cop_ghsl:CopGhslSearch"
GeodesSearch = "eodag.plugins.search.geodes:GeodesSearch"

[tool.setuptools]
include-package-data = true
Expand Down
5 changes: 2 additions & 3 deletions tests/integration/test_core_search_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,9 +389,8 @@ def test_core_serialize_deserialize_geodes_results(self, mock__request):
)
mock__request.return_value = mock.Mock()
with open(geodes_resp_file) as f:
mock__request.return_value.json.side_effect = [
json.load(f),
]
# mock search result, then availability
mock__request.return_value.json.side_effect = [json.load(f), {}]
prods = self.dag.search(
provider="geodes", collection="S2_MSI_L1C", raise_errors=True
)
Expand Down
Loading
Loading