Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Habitica integration to habiticalib #131032

Merged
merged 27 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
migrate quest actions
  • Loading branch information
tr4nt0r committed Dec 17, 2024
commit 86b5a166d154412ccfc01479b9273e8ebc825fab
2 changes: 1 addition & 1 deletion homeassistant/components/habitica/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/habitica",
"iot_class": "cloud_polling",
"loggers": ["habitipy", "plumbum", "habiticalib"],
"requirements": ["habitipy==0.3.3", "habiticalib==0.2.0a0"]
"requirements": ["habitipy==0.3.3", "habiticalib==0.2.0a1"]
}
54 changes: 28 additions & 26 deletions homeassistant/components/habitica/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
from __future__ import annotations

from dataclasses import asdict
from http import HTTPStatus
import logging
from uuid import UUID

from aiohttp import ClientError, ClientResponseError
from aiohttp import ClientError
from habiticalib import (
Direction,
HabiticaException,
Expand Down Expand Up @@ -229,35 +228,38 @@ async def manage_quests(call: ServiceCall) -> ServiceResponse:
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
coordinator = entry.runtime_data

COMMAND_MAP = {
SERVICE_ABORT_QUEST: "abort",
SERVICE_ACCEPT_QUEST: "accept",
SERVICE_CANCEL_QUEST: "cancel",
SERVICE_LEAVE_QUEST: "leave",
SERVICE_REJECT_QUEST: "reject",
SERVICE_START_QUEST: "force-start",
FUNC_MAP = {
SERVICE_ABORT_QUEST: coordinator.habitica.abort_quest,
SERVICE_ACCEPT_QUEST: coordinator.habitica.accept_quest,
SERVICE_CANCEL_QUEST: coordinator.habitica.cancel_quest,
SERVICE_LEAVE_QUEST: coordinator.habitica.leave_quest,
SERVICE_REJECT_QUEST: coordinator.habitica.reject_quest,
SERVICE_START_QUEST: coordinator.habitica.start_quest,
}

func = FUNC_MAP[call.service]

try:
return await coordinator.api.groups.party.quests[
COMMAND_MAP[call.service]
].post()
except ClientResponseError as e:
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
) from e
if e.status == HTTPStatus.UNAUTHORIZED:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="quest_action_unallowed"
) from e
if e.status == HTTPStatus.NOT_FOUND:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="quest_not_found"
) from e
response = await func()
except TooManyRequestsError as e:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
) from e
except NotAuthorizedError as e:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="quest_action_unallowed"
) from e
except NotFoundError as e:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="quest_not_found"
) from e
except (HabiticaException, ClientError) as e:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="service_call_exception"
) from e
else:
return asdict(response.data)

for service in (
SERVICE_ABORT_QUEST,
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1088,7 +1088,7 @@ ha-iotawattpy==0.1.2
ha-philipsjs==3.2.2

# homeassistant.components.habitica
habiticalib==0.2.0a0
habiticalib==0.2.0a1

# homeassistant.components.habitica
habitipy==0.3.3
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,7 @@ ha-iotawattpy==0.1.2
ha-philipsjs==3.2.2

# homeassistant.components.habitica
habiticalib==0.2.0a0
habiticalib==0.2.0a1

# homeassistant.components.habitica
habitipy==0.3.3
Expand Down
12 changes: 12 additions & 0 deletions tests/components/habitica/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
NotFoundError,
TooManyRequestsError,
)
from habiticalib.types import HabiticaQuestResponse
import pytest
from yarl import URL

Expand Down Expand Up @@ -147,6 +148,17 @@ async def mock_habiticalib() -> Generator[AsyncMock]:
client.get_group_members.return_value = HabiticaGroupMembersResponse.from_json(
load_fixture("party_members.json", DOMAIN)
)
for func in (
"leave_quest",
"reject_quest",
"cancel_quest",
"abort_quest",
"start_quest",
"accept_quest",
):
getattr(client, func).return_value = HabiticaQuestResponse.from_json(
load_fixture("party_quest.json", DOMAIN)
)
yield client


Expand Down
31 changes: 31 additions & 0 deletions tests/components/habitica/fixtures/party_quest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"success": true,
"data": {
"progress": {
"collect": {},
"hp": 100
},
"key": "dustbunnies",
"active": true,
"leader": "a380546a-94be-4b8e-8a0b-23e0d5c03303",
"members": {
"a380546a-94be-4b8e-8a0b-23e0d5c03303": true
},
"extra": {}
},
"notifications": [
{
"id": "3f59313f-6d7c-4fff-a8c4-3b153d828c6f",
"type": "NEW_CHAT_MESSAGE",
"data": {
"group": {
"id": "94cd398c-2240-4320-956e-6d345cf2c0de",
"name": "tests Party"
}
},
"seen": false
}
],
"userV": 287,
"appVersion": "5.29.2"
}
67 changes: 30 additions & 37 deletions tests/components/habitica/test_services.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Test Habitica actions."""

from collections.abc import Generator
from http import HTTPStatus
from typing import Any
from unittest.mock import AsyncMock, patch
from uuid import UUID
Expand All @@ -16,7 +15,6 @@
ATTR_SKILL,
ATTR_TARGET,
ATTR_TASK,
DEFAULT_URL,
DOMAIN,
SERVICE_ABORT_QUEST,
SERVICE_ACCEPT_QUEST,
Expand All @@ -38,7 +36,6 @@
ERROR_NOT_AUTHORIZED,
ERROR_NOT_FOUND,
ERROR_TOO_MANY_REQUESTS,
mock_called_with,
)

from tests.common import MockConfigEntry
Expand Down Expand Up @@ -307,31 +304,24 @@ async def test_get_config_entry(


@pytest.mark.parametrize(
("service", "command"),
"service",
[
(SERVICE_ABORT_QUEST, "abort"),
(SERVICE_ACCEPT_QUEST, "accept"),
(SERVICE_CANCEL_QUEST, "cancel"),
(SERVICE_LEAVE_QUEST, "leave"),
(SERVICE_REJECT_QUEST, "reject"),
(SERVICE_START_QUEST, "force-start"),
SERVICE_ABORT_QUEST,
SERVICE_ACCEPT_QUEST,
SERVICE_CANCEL_QUEST,
SERVICE_LEAVE_QUEST,
SERVICE_REJECT_QUEST,
SERVICE_START_QUEST,
],
ids=[],
)
async def test_handle_quests(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
habitica: AsyncMock,
service: str,
command: str,
) -> None:
"""Test Habitica actions for quest handling."""

mock_habitica.post(
f"{DEFAULT_URL}/api/v3/groups/party/quests/{command}",
json={"success": True, "data": {}},
)

await hass.services.async_call(
DOMAIN,
service,
Expand All @@ -340,62 +330,65 @@ async def test_handle_quests(
blocking=True,
)

assert mock_called_with(
mock_habitica,
"post",
f"{DEFAULT_URL}/api/v3/groups/party/quests/{command}",
)
getattr(habitica, service).assert_awaited_once()


@pytest.mark.parametrize(
(
"http_status",
"raise_exception",
"expected_exception",
"expected_exception_msg",
),
[
(
HTTPStatus.TOO_MANY_REQUESTS,
ERROR_TOO_MANY_REQUESTS,
ServiceValidationError,
RATE_LIMIT_EXCEPTION_MSG,
),
(
HTTPStatus.NOT_FOUND,
ERROR_NOT_FOUND,
ServiceValidationError,
"Unable to complete action, quest or group not found",
),
(
HTTPStatus.UNAUTHORIZED,
ERROR_NOT_AUTHORIZED,
ServiceValidationError,
"Action not allowed, only quest leader or group leader can perform this action",
),
(
HTTPStatus.BAD_REQUEST,
ERROR_BAD_REQUEST,
HomeAssistantError,
REQUEST_EXCEPTION_MSG,
),
],
)
@pytest.mark.parametrize(
"service",
[
SERVICE_ACCEPT_QUEST,
SERVICE_ABORT_QUEST,
SERVICE_CANCEL_QUEST,
SERVICE_LEAVE_QUEST,
SERVICE_REJECT_QUEST,
SERVICE_START_QUEST,
],
)
async def test_handle_quests_exceptions(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
http_status: HTTPStatus,
habitica: AsyncMock,
raise_exception: Exception,
service: str,
expected_exception: Exception,
expected_exception_msg: str,
) -> None:
"""Test Habitica handle quests action exceptions."""

mock_habitica.post(
f"{DEFAULT_URL}/api/v3/groups/party/quests/accept",
json={"success": True, "data": {}},
status=http_status,
)

getattr(habitica, service).side_effect = raise_exception
with pytest.raises(expected_exception, match=expected_exception_msg):
await hass.services.async_call(
DOMAIN,
SERVICE_ACCEPT_QUEST,
service,
service_data={ATTR_CONFIG_ENTRY: config_entry.entry_id},
return_response=True,
blocking=True,
Expand Down