diff --git a/src/gitmojis/cli/__init__.py b/src/gitmojis/cli/__init__.py index 792bf95..037d678 100644 --- a/src/gitmojis/cli/__init__.py +++ b/src/gitmojis/cli/__init__.py @@ -18,15 +18,20 @@ def get_commands() -> list[click.Command]: name="gitmojis", commands=get_commands(), ) +@click.option( + "--use-backup", + is_flag=True, + help="Use the backup to fetch data if the API request fails.", +) @click.version_option( package_name="python-gitmojis", prog_name="gitmojis", ) @click.pass_context -def gitmojis_cli(context: click.Context) -> None: +def gitmojis_cli(context: click.Context, use_backup: bool) -> None: """Command-line interface for managing the official Gitmoji guide.""" # Initialize the context object context.ensure_object(dict) # Pass the current state of the Gitmoji guide to the group context - context.obj["guide"] = fetch_guide() + context.obj["guide"] = fetch_guide(use_backup=use_backup) diff --git a/src/gitmojis/core.py b/src/gitmojis/core.py index b3b2291..4d265ba 100644 --- a/src/gitmojis/core.py +++ b/src/gitmojis/core.py @@ -3,24 +3,30 @@ import requests from . import defaults -from .exceptions import ResponseJsonError +from .exceptions import ApiRequestError, ResponseJsonError from .model import Gitmoji, Guide -def fetch_guide() -> Guide: +def fetch_guide(*, use_backup: bool = False) -> Guide: """Fetch the Gitmoji guide from the official Gitmoji API. This function sends a GET request to the Gitmoji API to retrieve the current state of the Gitmoji guide. If the request is successful and contains the expected JSON data, a `Guide` object is returned. If the expected JSON data is not present, a - `ResponseJsonError` is raised. In case of an HTTP error during the request (e.g., - connection error, timeout), the function falls back to loading a local copy of the - Gitmoji guide. + `ResponseJsonError` is raised. In case of an HTTP error during the request + (e.g., connection error, timeout), and if `use_backup` is set to `True`, the function + falls back to loading a local copy of the Gitmoji guide. Otherwise, it raises an + `ApiRequestError`. + + Args: + use_backup: A flag indicating whether to use a local backup in case of a request + failure. Defaults to `False`. Returns: A `Guide` object representing the current state of the Gitmoji API. Raises: + ApiRequestError: If the API request fails and `use_backup` is `False`. ResponseJsonError: If the API response doesn't contain the expected JSON data or if there is an error loading the local backup of the Gitmoji guide. """ @@ -29,9 +35,12 @@ def fetch_guide() -> Guide: if (gitmojis_json := response.json().get(defaults.GITMOJI_API_KEY)) is None: raise ResponseJsonError - except requests.RequestException: - with defaults.GITMOJI_API_PATH.open(encoding="UTF-8") as f: - gitmojis_json = json.load(f) + except requests.RequestException as exc_info: + if use_backup: + with defaults.GITMOJI_API_PATH.open(encoding="UTF-8") as f: + gitmojis_json = json.load(f) + else: + raise ApiRequestError from exc_info guide = Guide(gitmojis=[Gitmoji(**gitmoji_json) for gitmoji_json in gitmojis_json]) diff --git a/src/gitmojis/exceptions.py b/src/gitmojis/exceptions.py index 75f4ad3..2954406 100644 --- a/src/gitmojis/exceptions.py +++ b/src/gitmojis/exceptions.py @@ -5,9 +5,9 @@ def __init__(self, message: str | None = None) -> None: super().__init__(message or getattr(self.__class__, "message", "")) -class ApiError(GitmojisException): - pass +class ApiRequestError(GitmojisException): + message = "request to get the Gitmoji data from the API failed" -class ResponseJsonError(ApiError): +class ResponseJsonError(ApiRequestError): message = "unsupported format of the JSON data returned by the API" diff --git a/tests/test_cli.py b/tests/test_cli.py index f91d47c..c00fc99 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -48,6 +48,21 @@ def command(context): assert result.exit_code == 0 +def test_gitmojis_cli_passes_use_backup_option_to_fetch_guide(mocker, cli_runner): + fetch_guide = mocker.patch("gitmojis.cli.fetch_guide", return_value=Guide()) + + @click.command() + @click.pass_context + def command(context): + pass + + gitmojis_cli.add_command(command) + + cli_runner.invoke(gitmojis_cli, ["--use-backup", "command"]) + + assert fetch_guide.call_args.kwargs == {"use_backup": True} + + def test_sync_command_dumps_api_data_to_backup_file(tmp_path, mocker, cli_runner): # Mock the backup file as empty file gitmoji_api_path = tmp_path / "gitmojis.json" diff --git a/tests/test_core.py b/tests/test_core.py index c4a21ce..47a6bd1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,7 +3,7 @@ from gitmojis import defaults from gitmojis.core import fetch_guide -from gitmojis.exceptions import ResponseJsonError +from gitmojis.exceptions import ApiRequestError, ResponseJsonError from gitmojis.model import Guide @@ -31,13 +31,21 @@ def test_fetch_guide_raises_error_if_gitmoji_api_key_not_in_response_json(mocker fetch_guide() -def test_fetch_guide_fall_back_to_backup_data_if_request_error(mocker): +def test_fetch_guide_falls_back_to_backup_data_if_request_error_and_using_backup(mocker): # fmt: skip mocker.patch("pathlib.Path.open", mocker.mock_open(read_data="[]")) mocker.patch("requests.get", side_effect=requests.RequestException) json_load = mocker.patch("json.load") - guide = fetch_guide() + guide = fetch_guide(use_backup=True) assert json_load.called assert guide == Guide(gitmojis=[]) + + +def test_fetch_guide_raises_error_if_request_error_and_not_using_backup(mocker): + mocker.patch("requests.get", side_effect=requests.RequestException) + + with pytest.raises(ApiRequestError) as exc_info: + fetch_guide(use_backup=False) + assert isinstance(exc_info.value.__cause__, requests.RequestException)