Repository: CS-SI/eodag
Pull request #1705 (2025-06-26 14:23:05)

feat(cli): download STAC items from their urls
Download STAC items from their urls using `import_stac_items` method.

```sh
eodag download --stac-item https://example.com/stac/item1.json --stac-item /path/to/item2.json
```

github-actions[bot]: ## Test Results
    4 files  ± 0      4 suites  ±0   5m 53s ⏱️ +5s
  648 tests + 4    645 ✅ + 4    3 💤 ±0  0 ❌ ±0 
2 592 runs  +16  2 490 ✅ +16  102 💤 ±0  0 ❌ ±0 

Results for commit 3e46b265. ± Comparison against base commit 920312e4.

[test-results]:data:application/gzip;base64,H4sIAJVYXWgC/02MzQ6DIBAGX8Vw7gFXlkpfpgFcElJ/GoST6bsXrFKPM9/ubMz5kVb2aMStYWvyscKQgo5+mTN22GWRp1hGKfqTnmuydlf4Vy//Lj9VOO3HLHgVFMISDhPSXJqACg46myAUr+rXbHk9ukR3vjbtMk0+ZmAdCWlAogZwDonugmNPLRkORmplh94ogUazzxdc+dzDCQEAAA==


Patch:
diff --git a/docs/cli_user_guide.rst b/docs/cli_user_guide.rst
index 74c39a5f..88a52a87 100644
--- a/docs/cli_user_guide.rst
+++ b/docs/cli_user_guide.rst
@@ -102,6 +102,12 @@ Download
 
         eodag download --conf my_conf.yml --search-results my_search.geojson
 
+* To download STAC items from their URL or path:
+
+.. code-block:: console
+
+        eodag download --stac-item https://foo/collections/bar/items/item-1-id --stac-item /path/to/item2.json
+
 Product Types
 -------------
 
diff --git a/eodag/cli.py b/eodag/cli.py
index 275f76e1..450cb705 100755
--- a/eodag/cli.py
+++ b/eodag/cli.py
@@ -52,7 +52,7 @@ from typing import TYPE_CHECKING, Any, Mapping
 
 import click
 
-from eodag.api.core import EODataAccessGateway
+from eodag.api.core import EODataAccessGateway, SearchResult
 from eodag.utils import DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE, parse_qs
 from eodag.utils.exceptions import NoMatchingProductType, UnsupportedProvider
 from eodag.utils.logging import setup_logging
@@ -528,12 +528,26 @@ def discover_pt(ctx: Context, **kwargs: Any) -> None:
     click.echo("Results stored at '{}'".format(storage_filepath))
 
 
-@eodag.command(help="Download a list of products from a serialized search result")
+@eodag.command(
+    help="""Download a list of products from a serialized search result or STAC items URLs/paths
+
+Examples:
+
+  eodag download --search-results /path/to/search_results.geojson
+
+  eodag download --stac-item https://example.com/stac/item1.json --stac-item /path/to/item2.json
+""",
+)
 @click.option(
     "--search-results",
     type=click.Path(exists=True, dir_okay=False),
     help="Path to a serialized search result",
 )
+@click.option(
+    "--stac-item",
+    multiple=True,
+    help="URL/path of a STAC item to download (multiple values accepted)",
+)
 @click.option(
     "-f",
     "--conf",
@@ -550,9 +564,10 @@ def discover_pt(ctx: Context, **kwargs: Any) -> None:
 def download(ctx: Context, **kwargs: Any) -> None:
     """Download a bunch of products from a serialized search result"""
     search_result_path = kwargs.pop("search_results")
-    if not search_result_path:
+    stac_items = kwargs.pop("stac_item")
+    if not search_result_path and not stac_items:
         with click.Context(download) as ctx:
-            click.echo("Nothing to do (no search results file provided)")
+            click.echo("Nothing to do (no search results file or stac item provided)")
             click.echo(download.get_help(ctx))
         sys.exit(1)
     setup_logging(verbose=ctx.obj["verbosity"])
@@ -561,24 +576,21 @@ def download(ctx: Context, **kwargs: Any) -> None:
         conf_file = click.format_filename(conf_file)
 
     satim_api = EODataAccessGateway(user_conf_file_path=conf_file)
-    search_results = satim_api.deserialize(search_result_path)
+
+    search_results = SearchResult([])
+    if search_result_path:
+        search_results.extend(satim_api.deserialize_and_register(search_result_path))
+    if stac_items:
+        search_results.extend(satim_api.import_stac_items(list(stac_items)))
 
     get_quicklooks = kwargs.pop("quicklooks")
     if get_quicklooks:
+        # Download only quicklooks
         click.echo(
             "Flag 'quicklooks' specified, downloading only quicklooks of products"
         )
 
         for idx, product in enumerate(search_results):
-            if product.downloader is None:
-                downloader = satim_api._plugins_manager.get_download_plugin(product)
-                auth = product.downloader_auth
-                if auth is None:
-                    auth = satim_api._plugins_manager.get_auth_plugin(
-                        downloader, product
-                    )
-                search_results[idx].register_downloader(downloader, auth)
-
             downloaded_file = product.get_quicklook()
             if not downloaded_file:
                 click.echo(
@@ -589,17 +601,7 @@ def download(ctx: Context, **kwargs: Any) -> None:
                 click.echo("Downloaded {}".format(downloaded_file))
 
     else:
-        # register downloader
-        for idx, product in enumerate(search_results):
-            if product.downloader is None:
-                downloader = satim_api._plugins_manager.get_download_plugin(product)
-                auth = product.downloader_auth
-                if auth is None:
-                    auth = satim_api._plugins_manager.get_auth_plugin(
-                        downloader, product
-                    )
-                search_results[idx].register_downloader(downloader, auth)
-
+        # Download products
         downloaded_files = satim_api.download_all(search_results)
         if downloaded_files and len(downloaded_files) > 0:
             for downloaded_file in downloaded_files:


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 902cc0bd..ba4ea44b 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -755,7 +755,7 @@ class TestEodagCli(unittest.TestCase):
                 no_blanks(
                     "".join(
                         (
-                            "Nothing to do (no search results file provided)",
+                            "Nothing to do (no search results file or stac item provided)",
                             ctx.get_help(),
                         )
                     )
@@ -776,7 +776,9 @@ class TestEodagCli(unittest.TestCase):
             ["download", "--search-results", search_results_path, "-f", config_path],
         )
         dag.assert_called_once_with(user_conf_file_path=config_path)
-        dag.return_value.deserialize.assert_called_once_with(search_results_path)
+        dag.return_value.deserialize_and_register.assert_called_once_with(
+            search_results_path
+        )
         self.assertEqual(dag.return_value.download_all.call_count, 1)
         self.assertEqual("Downloaded /fake_path\n", result.output)
 
@@ -807,6 +809,29 @@ class TestEodagCli(unittest.TestCase):
             result.output,
         )
 
+    @mock.patch("eodag.cli.EODataAccessGateway", autospec=True)
+    def test_eodag_download_stac_items(self, dag):
+        """Calling eodag download with --stac-item argument"""
+        fake_result = SearchResult([mock.MagicMock() * 2], 2)
+        dag.return_value.import_stac_items.return_value = fake_result
+        with self.user_conf() as conf_file:
+            self.runner.invoke(
+                eodag,
+                [
+                    "download",
+                    "--conf",
+                    conf_file,
+                    "--stac-item",
+                    "foo",
+                    "--stac-item",
+                    "bar",
+                ],
+            )
+            dag.return_value.import_stac_items.assert_called_once_with(
+                ["foo", "bar"],
+            )
+            dag.return_value.download_all.assert_called_once_with(fake_result)
+
     def test_eodag_download_missingcredentials(self):
         """Calling eodag download with missing credentials must raise MisconfiguredError"""
         search_results_path = os.path.join(