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: 7 additions & 2 deletions src/gitmojis/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
25 changes: 17 additions & 8 deletions src/gitmojis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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])

Expand Down
6 changes: 3 additions & 3 deletions src/gitmojis/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
15 changes: 15 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 11 additions & 3 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)