Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/cli_user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,19 @@ Collections

.. command-output:: eodag list --help

* To list all available collections and supported providers:
* To list all available collection identifiers, one per line:

.. code-block:: console

eodag list

* To include full collection information, use ``--json`` or ``--yaml``:

.. code-block:: console

eodag list --json
eodag list --yaml

* To list available collections on a specified supported provider:

.. code-block:: console
Expand Down
57 changes: 44 additions & 13 deletions eodag/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@
import functools
import json
import sys
import textwrap
from importlib.metadata import metadata
from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional
from urllib.parse import parse_qs

import click
import yaml

from eodag.utils import DEFAULT_LIMIT, DEFAULT_PAGE

Expand All @@ -64,6 +64,14 @@
]


class IndentedSafeDumper(yaml.SafeDumper):
"""PyYAML dumper that indents block sequence items under their mapping key."""

def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
"""Force indentation for block sequence items."""
return super().increase_indent(flow, False)


class MutuallyExclusiveOption(click.Option):
"""Mutually Exclusive Options for Click
from https://gist.github.com/jacobtolar/fb80d5552a9a9dfc32b12a829fa21c0c
Expand Down Expand Up @@ -438,6 +446,20 @@ def search_crunch(ctx: Context, **kwargs: Any) -> None:
@click.option(
"--no-fetch", is_flag=True, help="Do not fetch providers for new collections"
)
@click.option(
"--json",
is_flag=True,
cls=MutuallyExclusiveOption,
mutually_exclusive=["yaml"],
help="Print full collection information as JSON",
)
@click.option(
"--yaml",
is_flag=True,
cls=MutuallyExclusiveOption,
mutually_exclusive=["json"],
help="Print full collection information as YAML",
)
@click.pass_context
def list_col(ctx: Context, **kwargs: Any) -> None:
"""Print the list of supported collections"""
Expand All @@ -450,7 +472,8 @@ def list_col(ctx: Context, **kwargs: Any) -> None:
dag = EODataAccessGateway()
provider = kwargs.pop("provider")
fetch_providers = not kwargs.pop("no_fetch")
text_wrapper = textwrap.TextWrapper()
json_output = kwargs.pop("json")
yaml_output = kwargs.pop("yaml")
guessed_collections = CollectionsList([])
try:
guessed_collections = dag.guess_collection(
Expand Down Expand Up @@ -491,17 +514,25 @@ def list_col(ctx: Context, **kwargs: Any) -> None:
collections = dag.list_collections(
provider=provider, fetch_providers=fetch_providers
)
click.echo("Listing available collections:")
for collection in collections:
click.echo("\n* {}: ".format(collection.id))
for prop, value in collection.model_dump().items():
if prop != "id":
text_wrapper.initial_indent = " - {}: ".format(prop)
text_wrapper.subsequent_indent = " " * len(
text_wrapper.initial_indent
)
if value is not None:
click.echo(text_wrapper.fill(str(value)))
formatted_collections = {
collection.id: collection.model_dump(
mode="json", exclude_none=True, exclude={"id"}
)
for collection in collections
}
if json_output:
click.echo(json.dumps(formatted_collections))
elif yaml_output:
click.echo(
yaml.dump(
formatted_collections,
Dumper=IndentedSafeDumper,
sort_keys=False,
allow_unicode=True,
)
)
elif collections:
click.echo("\n".join(collection.id for collection in collections))
except UnsupportedProvider:
click.echo("Unsupported provider. You may have a typo")
click.echo("Available providers: {}".format(", ".join(dag.providers.names)))
Expand Down
73 changes: 62 additions & 11 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime as dt
import json
import logging
import os
import re
Expand All @@ -29,6 +30,7 @@
import click
import responses
import shapely
import yaml
from click.testing import CliRunner
from faker import Faker
from packaging import version
Expand Down Expand Up @@ -739,8 +741,8 @@ def test_eodag_list_collection_ok(self):
exit_code, output, error = self.eodag_command(["list", "--no-fetch"])
self.assertEqual(exit_code, 0)
self.assertIsNone(error)
for col in all_supported_collections:
self.assertIn(col, output)
self.assertNotIn("Listing available collections:", output)
self.assertCountEqual(output.splitlines(), all_supported_collections)

def test_eodag_list_collection_with_provider_ok(self):
"""Calling eodag list with provider should return all supported collections of specified provider""" # noqa
Expand All @@ -756,12 +758,9 @@ def test_eodag_list_collection_with_provider_ok(self):
)
self.assertEqual(exit_code, 0)
self.assertIsNone(error)
for col in provider_supported_collections:
self.assertIn(
col,
output,
f"{col} was not found in {provider} supported collections",
)
self.assertNotIn("Listing available collections:", output)
for collection in provider_supported_collections:
self.assertIn(collection, output)

def test_eodag_list_collection_with_provider_ko(self):
"""Calling eodag list with unsupported provider should fail and print a list of available providers""" # noqa
Expand All @@ -785,28 +784,80 @@ def test_eodag_list_collection_fetch(self, mock_fetch_collections_list):

exit_code, output, error = self.eodag_command(["list", "--no-fetch"])
self.assertEqual(exit_code, 0)
self.assertIn("Listing available collections:", output)
self.assertNotIn("Listing available collections:", output)
self.assertIsNone(error)

assert not mock_fetch_collections_list.called

exit_code, output, error = self.eodag_command(["list"])
self.assertEqual(exit_code, 0)
self.assertIn("Listing available collections:", output)
self.assertNotIn("Listing available collections:", output)
self.assertIsNone(error)

mock_fetch_collections_list.assert_called_once_with(mock.ANY, provider=None)

exit_code, output, error = self.eodag_command(["list", "-p", "cop_dataspace"])
self.assertEqual(exit_code, 0)
self.assertIn("Listing available collections:", output)
self.assertNotIn("Listing available collections:", output)
self.assertIsNone(error)

mock_fetch_collections_list.assert_called_with(
mock.ANY, provider="cop_dataspace"
)
self.assertEqual(mock_fetch_collections_list.call_count, 2)

@mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
def test_eodag_list_collection_formatted_output(self, dag):
"""Calling eodag list with a format option should return collection metadata."""
collections = CollectionsList(
[
Collection.create_with_dag(
dag=dag,
id="foo",
title="Foo collection",
description="Foo description",
keywords=["foo", "bar"],
),
Collection.create_with_dag(
dag=dag,
id="bar",
title="Bar collection",
description="Donn\u00e9es bar",
),
]
)
dag.return_value.list_collections.return_value = collections
dag.return_value.guess_collection.return_value = CollectionsList([])
expected_collections = {
collection.id: collection.model_dump(
mode="json", exclude_none=True, exclude={"id"}
)
for collection in collections
}

exit_code, output, error = self.eodag_command(["list", "--json", "--no-fetch"])
self.assertEqual(exit_code, 0)
self.assertIsNone(error)
self.assertEqual(json.loads(output), expected_collections)

exit_code, output, error = self.eodag_command(["list", "--yaml", "--no-fetch"])
self.assertEqual(exit_code, 0)
self.assertIsNone(error)
self.assertNotIn("Listing available collections:", output)
self.assertEqual(yaml.safe_load(output), expected_collections)
self.assertIn(" keywords:\n - foo\n - bar\n", output)
self.assertIn("Donn\u00e9es bar", output)
self.assertNotIn("\\xE9", output)

def test_eodag_list_collection_format_options_are_mutually_exclusive(self):
"""Calling eodag list with both output formats should fail."""
exit_code, output, error = self.eodag_command(
["list", "--json", "--yaml", "--no-fetch"]
)
self.assertNotEqual(exit_code, 0)
self.assertIsInstance(error, SystemExit)
self.assertIn("mutually exclusive", output)

@mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
def test_eodag_guess_collection_ok(self, dag):
"""Calling eodag list with one or several valid collection feature(s) should return
Expand Down
Loading