diff --git a/eodag/api/core.py b/eodag/api/core.py index e1a53d265..7c35b41a6 100644 --- a/eodag/api/core.py +++ b/eodag/api/core.py @@ -288,6 +288,7 @@ def build_index(self) -> None: missionStartDate=fields.ID, missionEndDate=fields.ID, keywords=fields.KEYWORD(analyzer=kw_analyzer), + stacCollection=fields.STORED, ) self._product_types_index = create_in(index_dir, product_types_schema) ix_writer = self._product_types_index.writer() diff --git a/eodag/rest/core.py b/eodag/rest/core.py index 37cd2d06d..fd33b5835 100644 --- a/eodag/rest/core.py +++ b/eodag/rest/core.py @@ -689,6 +689,7 @@ def crunch_products( def eodag_api_init() -> None: """Init EODataAccessGateway server instance, pre-running all time consuming tasks""" eodag_api.fetch_product_types_list() + StacCollection.fetch_external_stac_collections(eodag_api) # pre-build search plugins for provider in eodag_api.available_providers(): diff --git a/eodag/rest/stac.py b/eodag/rest/stac.py index aae31ade9..5cd0beae5 100644 --- a/eodag/rest/stac.py +++ b/eodag/rest/stac.py @@ -36,6 +36,7 @@ from eodag.api.product.metadata_mapping import ( DEFAULT_METADATA_MAPPING, + NOT_AVAILABLE, format_metadata, get_metadata_path, ) @@ -53,8 +54,11 @@ from eodag.utils.exceptions import ( NoMatchingProductType, NotAvailableError, + RequestError, + TimeOutError, ValidationError, ) +from eodag.utils.requests import fetch_json if TYPE_CHECKING: from eodag.api.core import EODataAccessGateway @@ -610,6 +614,34 @@ class StacCollection(StacCommon): :type root: str """ + # External STAC collections + ext_stac_collections: Dict[str, Dict[str, Any]] = dict() + + @classmethod + def fetch_external_stac_collections(cls, eodag_api: EODataAccessGateway) -> None: + """Load external STAC collections + + :param eodag_api: EODAG python API instance + :type eodag_api: :class:`eodag.api.core.EODataAccessGateway` + """ + list_product_types = eodag_api.list_product_types(fetch_providers=False) + for product_type in list_product_types: + ext_stac_collection_path = product_type.get("stacCollection") + if not ext_stac_collection_path: + continue + logger.info(f"Fetching external STAC collection for {product_type['ID']}") + + try: + ext_stac_collection = fetch_json(ext_stac_collection_path) + except (RequestError, TimeOutError) as e: + logger.debug(e) + logger.warning( + f"Could not read remote external STAC collection from {ext_stac_collection_path}", + ) + ext_stac_collection = {} + + cls.ext_stac_collections[product_type["ID"]] = ext_stac_collection + def __init__( self, url: str, @@ -717,6 +749,19 @@ def __get_collection_list( "providers": providers_models, }, ) + # override EODAG's collection with the external collection + product_type_id = product_type.get("_id", None) or product_type["ID"] + ext_stac_collection = self.ext_stac_collections.get(product_type_id, {}) + # merge "keywords" list + merged_keywords = product_type_collection.get("keywords", []) + if "keywords" in ext_stac_collection: + new_keywords = ext_stac_collection["keywords"] + for v in new_keywords: + if v != NOT_AVAILABLE and v not in merged_keywords: + merged_keywords.append(v) + product_type_collection.update(ext_stac_collection) + if merged_keywords: + product_type_collection["keywords"] = merged_keywords # parse f-strings format_args = deepcopy(self.stac_config) format_args["collection"] = dict( diff --git a/tests/units/test_stac_utils.py b/tests/units/test_stac_utils.py index ecef201b1..8945fcfdd 100644 --- a/tests/units/test_stac_utils.py +++ b/tests/units/test_stac_utils.py @@ -27,6 +27,7 @@ from pygeofilter.values import Geometry import eodag.rest.utils.rfc3339 as rfc3339 +from eodag.rest.stac import StacCollection from eodag.rest.types.stac_search import SearchPostRequest from eodag.rest.utils.cql_evaluate import EodagEvaluator from eodag.utils.exceptions import ValidationError @@ -54,6 +55,12 @@ def setUpClass(cls): cls.rest_utils = rest_utils + import eodag.rest.core as rest_core + + importlib.reload(rest_core) + + cls.rest_core = rest_core + search_results_file = os.path.join( TEST_RESOURCES_PATH, "eodag_search_result_peps.geojson" ) @@ -246,6 +253,42 @@ def test_get_datetime(self): self.assertEqual(dtstart, start) self.assertEqual(dtend, end) + def test_fetch_external_stac_collections(self): + """Load external STAC collections""" + external_json = """{ + "new_field":"New Value", + "title":"A different title for Sentinel 2 MSI Level 1C", + "keywords":["New Keyword"] + }""" + product_type_conf = self.rest_core.eodag_api.product_types_config["S2_MSI_L1C"] + ext_stac_collection_path = "/path/to/external/stac/collections/S2_MSI_L1C.json" + product_type_conf["stacCollection"] = ext_stac_collection_path + + with mock.patch( + "eodag.rest.stac.fetch_json", + autospec=True, + return_value=json.loads(external_json), + ) as mock_fetch_json: + # Check if the returned STAC collection contains updated data + StacCollection.fetch_external_stac_collections(self.rest_core.eodag_api) + stac_coll = self.rest_core.get_stac_collection_by_id( + url="", root="", collection_id="S2_MSI_L1C" + ) + mock_fetch_json.assert_called_with(ext_stac_collection_path) + # New field + self.assertIn("new_field", stac_coll) + # Merge keywords + self.assertListEqual( + ["MSI", "SENTINEL2", "S2A,S2B", "L1", "OPTICAL", "New Keyword"], + stac_coll["keywords"], + ) + # Override existing fields + self.assertEqual( + "A different title for Sentinel 2 MSI Level 1C", stac_coll["title"] + ) + # Restore previous state + StacCollection.ext_stac_collections.clear() + class TestEodagCql2jsonEvaluator(unittest.TestCase): def setUp(self):