Repository: CS-SI/eodag

Issue:
percent variables parsing error in product location
If product location contains some percent variables, it may conflict with `register_downloader()` internal parsing mechanism and raise:
```
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/dist-packages/eodag/api/core.py", line 1474, in _do_search
    eo_product.register_downloader(
  File "/usr/local/lib/python3.8/dist-packages/eodag/api/product/_product.py", line 224, in register_downloader
    self.location = self.location % vars(self.downloader.config)
ValueError: unsupported format character 'B' (0x42) at index 72
2022-12-19 16:27:20,206 - eodag.core - ERROR - Error while searching on provider foo (ignored):
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/dist-packages/eodag/api/core.py", line 1474, in _do_search
    eo_product.register_downloader(
  File "/usr/local/lib/python3.8/dist-packages/eodag/api/product/_product.py", line 224, in register_downloader
    self.location = self.location % vars(self.downloader.config)
ValueError: unsupported format character 'B' (0x42) at index 72
```
Reproductible as:
```py
"https://example.com/%257B" % ({})
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [26], line 1
----> 1 "https://example.com/%257B" % ({})

ValueError: unsupported format character 'B' (0x42) at index 24
```

- [ ] Intercept error, log a warning and ignore this step if it occurs 

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

Results for commit a6388a0a. ± Comparison against base commit 3465afe0.

[test-results]:data:application/gzip;base64,H4sIABuToWMC/02MSw7CIBQAr9KwdsEn/LyMeSIkxLYYPivj3QUL2OXMJPNGzq82oetCLwtKxecJjxIh+7BXFJJXUVNukTEy6JaKMU1R/VdP/+qLQzjwaxV4ChtjiN3EsrenELTDWAoupzmOfPBp+OPzz4Rt87kCAsGUAgzgjNSEKKExBXsHMMQ5x2QtGLC06PMFBJYnbAUBAAA=

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

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `85%` | :white_check_mark: |
| eodag/api/product/\_product.py | `83%` | :white_check_mark: |
| tests/units/test\_eoproduct.py | `99%` | :white_check_mark: |

<strong> </strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `85%` | :white_check_mark: |
| eodag/api/product/\_product.py | `83%` | :white_check_mark: |
| tests/units/test\_eoproduct.py | `99%` | :white_check_mark: |
| eodag/api/product/\_product.py | `83%` | :white_check_mark: |
| tests/units/test\_eoproduct.py | `99%` | :white_check_mark: |

_Minimum allowed coverage is `70%`_

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

Patch:
diff --git a/eodag/api/product/_product.py b/eodag/api/product/_product.py
index 777b3026..a0a3d78c 100644
--- a/eodag/api/product/_product.py
+++ b/eodag/api/product/_product.py
@@ -221,15 +221,30 @@ class EOProduct(object):
         """
         self.downloader = downloader
         self.downloader_auth = authenticator
+
         # resolve locations and properties if needed with downloader configuration
-        self.location = self.location % vars(self.downloader.config)
-        self.remote_location = self.remote_location % vars(self.downloader.config)
+        location_attrs = ("location", "remote_location")
+        for location_attr in location_attrs:
+            try:
+                setattr(
+                    self,
+                    location_attr,
+                    getattr(self, location_attr) % vars(self.downloader.config),
+                )
+            except ValueError as e:
+                logger.debug(
+                    f"Could not resolve product.{location_attr} ({getattr(self, location_attr)})"
+                    f" in register_downloader: {str(e)}"
+                )
+
         for k, v in self.properties.items():
             if isinstance(v, str):
                 try:
                     self.properties[k] = v % vars(self.downloader.config)
-                except TypeError:
-                    pass
+                except (TypeError, ValueError) as e:
+                    logger.debug(
+                        f"Could not resolve {k} property ({v}) in register_downloader: {str(e)}"
+                    )
 
     def download(
         self,


Test patch:
diff --git a/tests/units/test_eoproduct.py b/tests/units/test_eoproduct.py
index ed716125..81bdd00f 100644
--- a/tests/units/test_eoproduct.py
+++ b/tests/units/test_eoproduct.py
@@ -453,3 +453,84 @@ class TestEOProduct(EODagTestCase):
         # progress bar finished
         self.assertEqual(progress_callback.n, progress_callback.total)
         self.assertGreater(progress_callback.total, 0)
+
+    def test_eoproduct_register_downloader(self):
+        """eoproduct.register_donwloader must set download and auth plugins"""
+        product = self._dummy_product()
+
+        self.assertIsNone(product.downloader)
+        self.assertIsNone(product.downloader_auth)
+
+        downloader = mock.MagicMock()
+        downloader_auth = mock.MagicMock()
+
+        product.register_downloader(downloader, downloader_auth)
+
+        self.assertEqual(product.downloader, downloader)
+        self.assertEqual(product.downloader_auth, downloader_auth)
+
+    def test_eoproduct_register_downloader_resolve_ok(self):
+        """eoproduct.register_donwloader must resolve locations and properties"""
+        downloadable_product = self._dummy_downloadable_product(
+            product=self._dummy_product(
+                properties=dict(
+                    self.eoproduct_props,
+                    **{
+                        "downloadLink": "%(base_uri)s/is/resolved",
+                        "otherProperty": "%(outputs_prefix)s/also/resolved",
+                    },
+                )
+            )
+        )
+        self.assertEqual(
+            downloadable_product.location,
+            f"{downloadable_product.downloader.config.base_uri}/is/resolved",
+        )
+        self.assertEqual(
+            downloadable_product.remote_location,
+            f"{downloadable_product.downloader.config.base_uri}/is/resolved",
+        )
+        self.assertEqual(
+            downloadable_product.properties["downloadLink"],
+            f"{downloadable_product.downloader.config.base_uri}/is/resolved",
+        )
+        self.assertEqual(
+            downloadable_product.properties["otherProperty"],
+            f"{downloadable_product.downloader.config.outputs_prefix}/also/resolved",
+        )
+
+    def test_eoproduct_register_downloader_resolve_ignored(self):
+        """eoproduct.register_donwloader must ignore unresolvable locations and properties"""
+        with self.assertLogs(level="DEBUG") as cm:
+            downloadable_product = self._dummy_downloadable_product(
+                product=self._dummy_product(
+                    properties=dict(
+                        self.eoproduct_props,
+                        **{
+                            "downloadLink": "%257B/cannot/be/resolved",
+                            "otherProperty": "%/%s/neither/resolved",
+                        },
+                    )
+                )
+            )
+            self.assertEqual(downloadable_product.location, "%257B/cannot/be/resolved")
+            self.assertEqual(
+                downloadable_product.remote_location, "%257B/cannot/be/resolved"
+            )
+            self.assertEqual(
+                downloadable_product.properties["downloadLink"],
+                "%257B/cannot/be/resolved",
+            )
+            self.assertEqual(
+                downloadable_product.properties["otherProperty"],
+                "%/%s/neither/resolved",
+            )
+
+            needed_logs = [
+                f"Could not resolve product.location ({downloadable_product.location})",
+                f"Could not resolve product.remote_location ({downloadable_product.remote_location})",
+                f"Could not resolve downloadLink property ({downloadable_product.properties['downloadLink']})",
+                f"Could not resolve otherProperty property ({downloadable_product.properties['otherProperty']})",
+            ]
+            for needed_log in needed_logs:
+                self.assertTrue(any(needed_log in log for log in cm.output))