Repository: CS-SI/eodag

Issue:
Add keywords to discovered product types
Automatically add default keywords to discovered product types to help finding them using [guess_product_type()](https://eodag.readthedocs.io/en/stable/api_reference/core.html#eodag.api.core.EODataAccessGateway.guess_product_type). 
See also [Guess a product type documentation](https://eodag.readthedocs.io/en/stable/notebooks/api_user_guide/4_search.html#Guess-a-product-type)

Discussion:
github-actions[bot]: ## Unit Test Results
    2 files  ±0      2 suites  ±0   11m 46s [:stopwatch:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "duration of all tests") + 2m 0s
333 tests +1  330 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") +1  3 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests") ±0  0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") ±0 
666 runs  +2  660 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") +2  6 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests") ±0  0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") ±0 

Results for commit b9d4eacc. ± Comparison against base commit 870580e4.

[test-results]:data:application/gzip;base64,H4sIAB99pGMC/02OQQ6DIBBFr2JYd4FCx9rLNDAMCalKg7AyvXvBKrp8b35eZmXWjbSwZ9PdGrYkFyuYFFR0fs7Yc8gin2I5CiEOei0JcVP8VG/3KaoKq9yYxbmgEHzYTUhzaQLADkcSgFfzL9bFJbjxtYd+mlzMwPRgJClEKfPDWraiax9IHAm4JDsYbam3d+Ds+wPf2302BQEAAA==

github-actions[bot]: <strong> </strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `85%` | :white_check_mark: |
| eodag/api/product/metadata\_mapping.py | `80%` | :white_check_mark: |
| eodag/plugins/search/qssearch.py | `77%` | :white_check_mark: |

<strong> </strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `85%` | :white_check_mark: |
| eodag/api/product/metadata\_mapping.py | `80%` | :white_check_mark: |
| eodag/plugins/search/qssearch.py | `77%` | :white_check_mark: |
| eodag/api/product/metadata\_mapping.py | `79%` | :white_check_mark: |
| eodag/plugins/search/qssearch.py | `77%` | :white_check_mark: |

_Minimum allowed coverage is `70%`_

<p align="right">Generated by :monkey: cobertura-action against b9d4eacc44333b413218ce0ce604ef9dbfe7f560 </p>

Patch:
diff --git a/eodag/api/product/metadata_mapping.py b/eodag/api/product/metadata_mapping.py
index e449a643..3f014290 100644
--- a/eodag/api/product/metadata_mapping.py
+++ b/eodag/api/product/metadata_mapping.py
@@ -302,7 +302,10 @@ def format_metadata(search_param, *args, **kwargs):
 
         @staticmethod
         def convert_csv_list(values_list):
-            return ",".join([str(x) for x in values_list])
+            if isinstance(values_list, list):
+                return ",".join([str(x) for x in values_list])
+            else:
+                return values_list
 
         @staticmethod
         def convert_remove_extension(string):
diff --git a/eodag/plugins/search/qssearch.py b/eodag/plugins/search/qssearch.py
index 6595d429..dda336da 100644
--- a/eodag/plugins/search/qssearch.py
+++ b/eodag/plugins/search/qssearch.py
@@ -33,6 +33,7 @@ from eodag.api.product.metadata_mapping import (
     get_metadata_path,
     get_metadata_path_value,
     get_search_param,
+    mtd_cfg_as_jsonpath,
     properties_from_json,
     properties_from_xml,
 )
@@ -214,19 +215,19 @@ class QueryStringSearch(Search):
 
                     for product_type_result in result:
                         # providers_config extraction
-                        mapping_config = {
-                            k: (None, cached_parse(v))
-                            for k, v in self.config.discover_product_types[
-                                "generic_product_type_parsable_properties"
-                            ].items()
-                        }
-                        mapping_config["generic_product_type_id"] = (
-                            None,
-                            cached_parse(
+                        mapping_config = {}
+                        mapping_config = mtd_cfg_as_jsonpath(
+                            dict(
                                 self.config.discover_product_types[
-                                    "generic_product_type_id"
-                                ]
+                                    "generic_product_type_parsable_properties"
+                                ],
+                                **{
+                                    "generic_product_type_id": self.config.discover_product_types[
+                                        "generic_product_type_id"
+                                    ]
+                                },
                             ),
+                            mapping_config,
                         )
 
                         extracted_mapping = properties_from_json(
@@ -244,15 +245,52 @@ class QueryStringSearch(Search):
                             ),
                         )
                         # product_types_config extraction
-                        mapping_config = {
-                            k: (None, cached_parse(v))
-                            for k, v in self.config.discover_product_types[
+                        mapping_config = {}
+                        mapping_config = mtd_cfg_as_jsonpath(
+                            self.config.discover_product_types[
                                 "generic_product_type_parsable_metadata"
-                            ].items()
-                        }
+                            ],
+                            mapping_config,
+                        )
                         conf_update_dict["product_types_config"][
                             generic_product_type_id
                         ] = properties_from_json(product_type_result, mapping_config)
+
+                        # update keywords
+                        keywords_fields = [
+                            "instrument",
+                            "platform",
+                            "platformSerialIdentifier",
+                            "processingLevel",
+                            "keywords",
+                        ]
+                        keywords_values_str = ",".join(
+                            [generic_product_type_id]
+                            + [
+                                str(
+                                    conf_update_dict["product_types_config"][
+                                        generic_product_type_id
+                                    ][kf]
+                                )
+                                for kf in keywords_fields
+                                if conf_update_dict["product_types_config"][
+                                    generic_product_type_id
+                                ][kf]
+                                != NOT_AVAILABLE
+                            ]
+                        )
+                        keywords_values_str = keywords_values_str.replace(
+                            ", ", ","
+                        ).replace(" ", "-")
+                        keywords_values_str = re.sub(
+                            r"[\[\]'\"]", "", keywords_values_str
+                        )
+                        keywords_values_str = ",".join(
+                            set(keywords_values_str.split(","))
+                        )
+                        conf_update_dict["product_types_config"][
+                            generic_product_type_id
+                        ]["keywords"] = keywords_values_str
             except KeyError as e:
                 logger.warning(
                     "Incomplete %s discover_product_types configuration: %s",
diff --git a/eodag/resources/stac_provider.yml b/eodag/resources/stac_provider.yml
index 3018f52c..22981b82 100644
--- a/eodag/resources/stac_provider.yml
+++ b/eodag/resources/stac_provider.yml
@@ -38,11 +38,11 @@ search:
         productType: '$.id'
       generic_product_type_parsable_metadata:
         abstract: '$.description'
-        instrument: '$.summaries.instruments'
-        platform: '$.summaries.constellation'
-        platformSerialIdentifier: '$.summaries.platform'
+        instrument: '{$.summaries.instruments#csv_list}'
+        platform: '{$.summaries.constellation#csv_list}'
+        platformSerialIdentifier: '{$.summaries.platform#csv_list}'
         processingLevel: '$.summaries."processing:level"'
-        keywords: '$.keywords'
+        keywords: '{$.keywords#csv_list}'
         license: '$.license'
         title: '$.title'
         missionStartDate: '$.extent.temporal.interval[0][0]'


Test patch:
diff --git a/tests/units/test_search_plugins.py b/tests/units/test_search_plugins.py
index ca6c2456..b25b7ea7 100644
--- a/tests/units/test_search_plugins.py
+++ b/tests/units/test_search_plugins.py
@@ -187,6 +187,50 @@ class TestSearchPluginQueryStringSearch(BaseSearchPluginTest):
             "The FOO collection",
         )
 
+    @mock.patch(
+        "eodag.plugins.search.qssearch.QueryStringSearch._request", autospec=True
+    )
+    def test_plugins_search_querystringseach_discover_product_types_keywords(
+        self, mock__request
+    ):
+        """QueryStringSearch.discover_product_types must return a dict with well formatted keywords"""
+        # One of the providers that has a QueryStringSearch Search plugin and keywords configured
+        provider = "astraea_eod"
+        search_plugin = self.get_search_plugin(self.product_type, provider)
+
+        mock__request.return_value = mock.Mock()
+        mock__request.return_value.json.return_value = {
+            "collections": [
+                {
+                    "id": "foo_collection",
+                    "keywords": ["foo", "bar"],
+                    "summaries": {
+                        "instruments": ["baz"],
+                        "constellation": "qux,foo",
+                        "platform": ["quux", "corge", "bar"],
+                        "processing:level": "grault",
+                    },
+                },
+            ]
+        }
+        conf_update_dict = search_plugin.discover_product_types()
+        keywords_list = conf_update_dict["product_types_config"]["foo_collection"][
+            "keywords"
+        ].split(",")
+
+        self.assertEqual(len(keywords_list), 8)
+        for k in [
+            "foo_collection",
+            "foo",
+            "bar",
+            "baz",
+            "qux",
+            "quux",
+            "corge",
+            "grault",
+        ]:
+            self.assertIn(k, keywords_list)
+
 
 class TestSearchPluginPostJsonSearch(BaseSearchPluginTest):
     def setUp(self):