Source code for eodag.api.search_result

# -*- coding: utf-8 -*-
# Copyright 2018, 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.
from __future__ import annotations

import logging
from collections import UserList
from typing import TYPE_CHECKING, Annotated, Any, Iterable, Optional, Union

from shapely.geometry import GeometryCollection, shape
from typing_extensions import Doc

from eodag.api.product import EOProduct, unregistered_product_from_item
from eodag.plugins.crunch.filter_date import FilterDate
from eodag.plugins.crunch.filter_latest_intersect import FilterLatestIntersect
from eodag.plugins.crunch.filter_latest_tpl_name import FilterLatestByName
from eodag.plugins.crunch.filter_overlap import FilterOverlap
from eodag.plugins.crunch.filter_property import FilterProperty
from eodag.utils import GENERIC_STAC_PROVIDER, STAC_SEARCH_PLUGINS
from eodag.utils.exceptions import MisconfiguredError

if TYPE_CHECKING:
    from shapely.geometry.base import BaseGeometry

    from eodag.plugins.crunch.base import Crunch
    from eodag.plugins.manager import PluginManager


logger = logging.getLogger("eodag.search_result")


[docs] class SearchResult(UserList[EOProduct]): """An object representing a collection of :class:`~eodag.api.product._product.EOProduct` resulting from a search. :param products: A list of products resulting from a search :param number_matched: (optional) the estimated total number of matching results :cvar data: List of products :ivar number_matched: Estimated total number of matching results """ errors: Annotated[ list[tuple[str, Exception]], Doc("Tuple of provider name, exception") ] def __init__( self, products: list[EOProduct], number_matched: Optional[int] = None, errors: Optional[list[tuple[str, Exception]]] = None, ) -> None: super().__init__(products) self.number_matched = number_matched self.errors = errors if errors is not None else []
[docs] def crunch(self, cruncher: Crunch, **search_params: Any) -> SearchResult: """Do some crunching with the underlying EO products. :param cruncher: The plugin instance to use to work on the products :param search_params: The criteria that have been used to produce this result :returns: The result of the application of the crunching method to the EO products """ crunched_results = cruncher.proceed(self.data, **search_params) return SearchResult(crunched_results)
[docs] def filter_date( self, start: Optional[str] = None, end: Optional[str] = None ) -> SearchResult: """ Apply :class:`~eodag.plugins.crunch.filter_date.FilterDate` crunch, check its documentation to know more. """ return self.crunch(FilterDate(dict(start=start, end=end)))
[docs] def filter_latest_intersect( self, geometry: Union[dict[str, Any], BaseGeometry, Any] ) -> SearchResult: """ Apply :class:`~eodag.plugins.crunch.filter_latest_intersect.FilterLatestIntersect` crunch, check its documentation to know more. """ return self.crunch(FilterLatestIntersect({}), geometry=geometry)
[docs] def filter_latest_by_name(self, name_pattern: str) -> SearchResult: """ Apply :class:`~eodag.plugins.crunch.filter_latest_tpl_name.FilterLatestByName` crunch, check its documentation to know more. """ return self.crunch(FilterLatestByName(dict(name_pattern=name_pattern)))
[docs] def filter_overlap( self, geometry: Any, minimum_overlap: int = 0, contains: bool = False, intersects: bool = False, within: bool = False, ) -> SearchResult: """ Apply :class:`~eodag.plugins.crunch.filter_overlap.FilterOverlap` crunch, check its documentation to know more. """ return self.crunch( FilterOverlap( dict( minimum_overlap=minimum_overlap, contains=contains, intersects=intersects, within=within, ) ), geometry=geometry, )
[docs] def filter_property( self, operator: str = "eq", **search_property: Any ) -> SearchResult: """ Apply :class:`~eodag.plugins.crunch.filter_property.FilterProperty` crunch, check its documentation to know more. """ return self.crunch(FilterProperty(dict(operator=operator, **search_property)))
[docs] def filter_online(self) -> SearchResult: """ Use cruncher :class:`~eodag.plugins.crunch.filter_property.FilterProperty`, filter for online products. """ return self.filter_property(storageStatus="ONLINE")
[docs] @staticmethod def from_geojson(feature_collection: dict[str, Any]) -> SearchResult: """Builds an :class:`~eodag.api.search_result.SearchResult` object from its representation as geojson :param feature_collection: A collection representing a search result. :returns: An eodag representation of a search result """ return SearchResult( [ EOProduct.from_geojson(feature) for feature in feature_collection["features"] ] )
[docs] def as_geojson_object(self) -> dict[str, Any]: """GeoJSON representation of SearchResult""" return { "type": "FeatureCollection", "features": [product.as_dict() for product in self], }
[docs] def as_shapely_geometry_object(self) -> GeometryCollection: """:class:`shapely.GeometryCollection` representation of SearchResult""" return GeometryCollection( [ shape(feature["geometry"]).buffer(0) for feature in self.as_geojson_object()["features"] ] )
[docs] def as_wkt_object(self) -> str: """WKT representation of SearchResult""" return self.as_shapely_geometry_object().wkt
@property def __geo_interface__(self) -> dict[str, Any]: """Implements the geo-interface protocol. See https://gist.github.com/sgillies/2217756 """ return self.as_geojson_object() def _repr_html_(self): total_count = f"/{self.number_matched}" if self.number_matched else "" return ( f"""<table> <thead><tr><td style='text-align: left; color: grey;'> {type(self).__name__}&ensp;({len(self)}{total_count}) </td></tr></thead> """ + "".join( [ f"""<tr><td style='text-align: left;'> <details><summary style='color: grey; font-family: monospace;'> {i}&ensp; {type(p).__name__}(id=<span style='color: black;'>{ p.properties["id"] }</span>, provider={p.provider}) </summary> {p._repr_html_()} </details> </td></tr> """ for i, p in enumerate(self) ] ) + "</table>" ) def extend(self, other: Iterable) -> None: """override extend method to include errors""" if isinstance(other, SearchResult): self.errors.extend(other.errors) return super().extend(other) @classmethod def _from_stac_item( cls, feature: dict[str, Any], plugins_manager: PluginManager ) -> SearchResult: """Create a SearchResult from a STAC item. :param feature: A STAC item as a dictionary :param plugins_manager: The EODAG plugin manager instance :returns: A SearchResult containing the EOProduct(s) created from the STAC item """ # Try importing from EODAG Server if results := _import_stac_item_from_eodag_server(feature, plugins_manager): return results # try importing from a known STAC provider if results := _import_stac_item_from_known_provider(feature, plugins_manager): return results # try importing from an unknown STAC provider return _import_stac_item_from_unknown_provider(feature, plugins_manager)
def _import_stac_item_from_eodag_server( feature: dict[str, Any], plugins_manager: PluginManager ) -> Optional[SearchResult]: """Import a STAC item from EODAG Server. :param feature: A STAC item as a dictionary :param plugins_manager: The EODAG plugin manager instance :returns: A SearchResult containing the EOProduct(s) created from the STAC item """ provider = None if backends := feature["properties"].get("federation:backends"): provider = backends[0] elif providers := feature["properties"].get("providers"): provider = providers[0].get("name") if provider is not None: logger.debug("Trying to import STAC item from EODAG Server") # assets coming from a STAC provider assets = { k: v["alternate"]["origin"] for k, v in feature.get("assets", {}).items() if k not in ("thumbnail", "downloadLink") and "origin" in v.get("alternate", {}) } if assets: updated_item = {**feature, **{"assets": assets}} else: # item coming from a non-STAC provider updated_item = {**feature} download_link = ( feature.get("assets", {}) .get("downloadLink", {}) .get("alternate", {}) .get("origin", {}) .get("href") ) if download_link: updated_item["assets"] = {} updated_item["links"] = [{"rel": "self", "href": download_link}] else: updated_item = {} try: eo_product = unregistered_product_from_item( updated_item, GENERIC_STAC_PROVIDER, plugins_manager ) except MisconfiguredError: eo_product = None if eo_product is not None: eo_product.provider = provider eo_product._register_downloader_from_manager(plugins_manager) return SearchResult([eo_product]) return None def _import_stac_item_from_known_provider( feature: dict[str, Any], plugins_manager: PluginManager ) -> Optional[SearchResult]: """Import a STAC item from an already-configured STAC provider. :param feature: A STAC item as a dictionary :param plugins_manager: The EODAG plugin manager instance :returns: A SearchResult containing the EOProduct(s) created from the STAC item """ item_hrefs = [f for f in feature.get("links", []) if f.get("rel") == "self"] item_href = item_hrefs[0]["href"] if len(item_hrefs) > 0 else None imported_products = SearchResult([]) for search_plugin in plugins_manager.get_search_plugins(): # only try STAC search plugins if ( search_plugin.config.type in STAC_SEARCH_PLUGINS and search_plugin.provider != GENERIC_STAC_PROVIDER and hasattr(search_plugin, "normalize_results") ): provider_base_url = search_plugin.config.api_endpoint.removesuffix( "/search" ) # compare the item href with the provider base URL if item_href and item_href.startswith(provider_base_url): products = search_plugin.normalize_results([feature]) if len(products) == 0 or len(products[0].assets) == 0: continue logger.debug( "Trying to import STAC item from %s", search_plugin.provider ) eo_product = products[0] configured_pts = [ k for k, v in search_plugin.config.products.items() if v.get("productType") == feature.get("collection") ] if len(configured_pts) > 0: eo_product.product_type = configured_pts[0] else: eo_product.product_type = feature.get("collection") eo_product._register_downloader_from_manager(plugins_manager) imported_products.append(eo_product) if len(imported_products) > 0: return imported_products return None def _import_stac_item_from_unknown_provider( feature: dict[str, Any], plugins_manager: PluginManager ) -> SearchResult: """Import a STAC item from an unknown STAC provider. :param feature: A STAC item as a dictionary :param plugins_manager: The EODAG plugin manager instance :returns: A SearchResult containing the EOProduct(s) created from the STAC item """ try: logger.debug("Trying to import STAC item from unknown provider") eo_product = unregistered_product_from_item( feature, GENERIC_STAC_PROVIDER, plugins_manager ) except MisconfiguredError: pass if eo_product is not None: eo_product.product_type = feature.get("collection") eo_product._register_downloader_from_manager(plugins_manager) return SearchResult([eo_product]) else: return SearchResult([]) class RawSearchResult(UserList[dict[str, Any]]): """An object representing a collection of raw/unparsed search results obtained from a provider. :param results: A list of raw/unparsed search results """ query_params: dict[str, Any] product_type_def_params: dict[str, Any] def __init__(self, results: list[Any]) -> None: super(RawSearchResult, self).__init__(results)