diff --git a/swh/deposit/cli/client.py b/swh/deposit/cli/client.py --- a/swh/deposit/cli/client.py +++ b/swh/deposit/cli/client.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2019 The Software Heritage developers +# Copyright (C) 2017-2020 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information @@ -14,7 +14,7 @@ import click import xmltodict -from swh.deposit.client import PublicApiDepositClient +from swh.deposit.client import PublicApiDepositClient, MaintenanceError from swh.deposit.cli import deposit @@ -161,7 +161,8 @@ errors are already dealt with by the underlying api client. Raises: - InputError explaining the issue + InputError explaining the user input related issue + MaintenanceError explaining the api status Returns: dict with the following keys: @@ -428,6 +429,9 @@ except InputError as e: logger.error("Problem during parsing options: %s", e) sys.exit(1) + except MaintenanceError as e: + logger.error(e) + sys.exit(1) if verbose: logger.info("Parsed configuration: %s" % (config,)) @@ -474,6 +478,9 @@ except InputError as e: logger.error("Problem during parsing options: %s", e) sys.exit(1) + except MaintenanceError as e: + logger.error(e) + sys.exit(1) print_result( client.deposit_status(collection=collection, deposit_id=deposit_id), diff --git a/swh/deposit/client.py b/swh/deposit/client.py --- a/swh/deposit/client.py +++ b/swh/deposit/client.py @@ -14,6 +14,7 @@ import logging from abc import ABCMeta, abstractmethod +from typing import Any, Dict from urllib.parse import urljoin from swh.core.config import SWHConfig @@ -22,6 +23,14 @@ logger = logging.getLogger(__name__) +class MaintenanceError(ValueError): + """Informational maintenance error exception + + """ + + pass + + def _parse(stream, encoding="utf-8"): """Given a xml stream, parse the result. @@ -277,9 +286,15 @@ """ return self.do(method, url) - def execute(self, *args, **kwargs): + def execute(self, *args, **kwargs) -> Dict[str, Any]: """Main endpoint to prepare and execute the http query to the api. + Raises: + MaintenanceError if some api maintenance is happening. + + Returns: + Dict of computed api data + """ url = self.compute_url(*args, **kwargs) method = self.compute_method(*args, **kwargs) @@ -304,6 +319,12 @@ error = self.parse_result_error(r.text) empty = self.empty_result error.update(empty) + if r.status_code == 503: + summary = error.get("summary") + detail = error.get("sword:verboseDescription") + # Maintenance error + if summary and detail: + raise MaintenanceError(f"{summary}: {detail}") error.update( {"status": r.status_code,} ) diff --git a/swh/deposit/tests/cli/test_client.py b/swh/deposit/tests/cli/test_client.py --- a/swh/deposit/tests/cli/test_client.py +++ b/swh/deposit/tests/cli/test_client.py @@ -12,7 +12,7 @@ from click.testing import CliRunner import pytest -from swh.deposit.client import PublicApiDepositClient +from swh.deposit.client import PublicApiDepositClient, MaintenanceError from swh.deposit.cli.client import generate_slug, _url, _client, _collection, InputError from swh.deposit.cli import deposit as cli from ..conftest import TEST_USER @@ -42,12 +42,16 @@ @pytest.fixture -def client_mock_down(mocker, slug): +def client_mock_api_down(mocker, slug): + """A mock client whose connection with api fails due to maintenance issue + + """ mocker.patch("swh.deposit.cli.client.generate_slug", return_value=slug) mock_client = MagicMock() mocker.patch("swh.deposit.cli.client._client", return_value=mock_client) - mock_client.service_document.return_value = EXAMPLE_SERVICE_DOCUMENT - mock_client.deposit_create.return_value = '{"foo": "bar"}' + mock_client.service_document.side_effect = MaintenanceError( + "Database backend maintenance: Temporarily unavailable, try again later." + ) return mock_client @@ -79,17 +83,19 @@ assert collection_name == "softcol" -def test_deposit_with_server_ok_backend_down( - sample_archive, mocker, caplog, client_mock, slug, tmp_path +def test_collection_ko_because_downtime(): + mock_client = MagicMock() + mock_client.service_document.side_effect = MaintenanceError("downtime") + with pytest.raises(MaintenanceError, match="downtime"): + _collection(mock_client) + + +def test_deposit_with_server_down_for_maintenance( + sample_archive, mocker, caplog, client_mock_api_down, slug, tmp_path ): - """ Deposit failure due to maintenance down time should be explicit in error msg - """ - metadata_path = os.path.join(tmp_path, "metadata.xml") - mocker.patch( - "swh.deposit.cli.client.tempfile.TemporaryDirectory", - return_value=contextlib.nullcontext(str(tmp_path)), - ) + """ Deposit failure due to maintenance down time should be explicit + """ runner = CliRunner() result = runner.invoke( cli, @@ -110,34 +116,17 @@ ], ) - assert result.exit_code == 0, result.output + assert result.exit_code == 1, result.output assert result.output == "" assert caplog.record_tuples == [ - ("swh.deposit.cli.client", logging.INFO, '{"foo": "bar"}'), + ( + "swh.deposit.cli.client", + logging.ERROR, + "Database backend maintenance: Temporarily unavailable, try again later.", + ) ] - client_mock.deposit_create.assert_called_once_with( - archive=sample_archive["path"], - collection="softcol", - in_progress=False, - metadata=metadata_path, - slug=slug, - ) - - with open(metadata_path) as fd: - assert ( - fd.read() - == f"""\ - - -\ttest-project -\t{slug} -\t -\t\tJane Doe -\t -""" - ) + client_mock_api_down.service_document.assert_called_once_with() def test_single_minimal_deposit(