diff --git a/kassalappy/__init__.py b/kassalappy/__init__.py index 6da00bc..d0c6f1f 100644 --- a/kassalappy/__init__.py +++ b/kassalappy/__init__.py @@ -3,7 +3,7 @@ import asyncio import logging -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Literal import aiohttp import async_timeout @@ -47,11 +47,11 @@ async def execute( self, endpoint: str, method: str = aiohttp.hdrs.METH_GET, - params: dict[str, Any] | None = None, - data: dict[Any, Any] | None = None, + params: dict[str, any] | None = None, + data: dict[any, any] | None = None, timeout: int | None = None, map_to_model: bool = False, - ) -> dict[Any, Any] | KassalappResource | list[KassalappResource] | None: + ) -> dict[any, any] | KassalappResource | list[KassalappResource] | None: """"Execute a API request and return the data.""" timeout = timeout or self.timeout @@ -76,9 +76,14 @@ async def execute( _LOGGER.exception("Error connecting to kassalapp") raise + async def healthy(self): + """Check if the Kassalapp API is working.""" + result = await self.execute("health") + return result.get("message") == "Healthy" + async def get_shopping_lists(self): """Get shopping lists.""" - return await self.execute(f"shopping-lists") + return await self.execute("shopping-lists") async def get_shopping_list(self, list_id: int, include_items: bool = False): """Get a shopping list.""" diff --git a/kassalappy/const.py b/kassalappy/const.py index 54e5524..b00a78d 100644 --- a/kassalappy/const.py +++ b/kassalappy/const.py @@ -13,4 +13,8 @@ HTTPStatus.TOO_MANY_REQUESTS, HTTPStatus.PRECONDITION_REQUIRED, ] +HTTP_CODES_NO_ACCESS: Final = [ + HTTPStatus.UNAUTHORIZED, + HTTPStatus.FORBIDDEN, +] HTTP_CODES_FATAL: Final = [HTTPStatus.BAD_REQUEST] diff --git a/kassalappy/exceptions.py b/kassalappy/exceptions.py index 6d93bf8..60bf1f1 100644 --- a/kassalappy/exceptions.py +++ b/kassalappy/exceptions.py @@ -9,11 +9,11 @@ def __init__( self, status: int, message: str = "HTTP error", - extension_code: str = API_ERR_CODE_UNKNOWN, + errors: dict[str, str] | None = None, ): self.status = status self.message = message - self.extension_code = extension_code + self.errors = errors super().__init__(self.message) @@ -22,3 +22,6 @@ class FatalHttpException(HttpException): class RetryableHttpException(HttpException): """Exception raised for HTTP codes that are possible to retry.""" + +class AuthorizationError(FatalHttpException): + """Invalid login exception.""" diff --git a/kassalappy/models.py b/kassalappy/models.py index 1b3ce4a..9816dc6 100644 --- a/kassalappy/models.py +++ b/kassalappy/models.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import TYPE_CHECKING - +from enum import Enum from pydantic import BaseModel diff --git a/kassalappy/utils.py b/kassalappy/utils.py index 6a9010b..9d5db13 100644 --- a/kassalappy/utils.py +++ b/kassalappy/utils.py @@ -3,15 +3,21 @@ import logging import re - import aiohttp +from http import HTTPStatus + from .const import ( API_ERR_CODE_UNKNOWN, + HTTP_CODES_NO_ACCESS, HTTP_CODES_FATAL, HTTP_CODES_RETRYABLE, ) -from .exceptions import FatalHttpException, RetryableHttpException +from .exceptions import ( + AuthorizationError, + FatalHttpException, + RetryableHttpException, +) from .models import ( KassalappResource, ShoppingList, @@ -53,7 +59,7 @@ async def extract_response_data( result = await response.json() if response.ok: - data = result.get("data") + data = result.get("data") or result if map_to_model: model = path_to_model(response.url.path) if isinstance(data, list): @@ -61,22 +67,27 @@ async def extract_response_data( return model(**data) return data - if response.status in HTTP_CODES_RETRYABLE: - error_message = result.get("message") + error_message = result.get("message") + errors = result.get("errors", {}) - raise RetryableHttpException( - response.status, message=error_message - ) + if response.status == HTTPStatus.NOT_FOUND: + raise FatalHttpException(response.status, error_message) - if response.status in HTTP_CODES_FATAL: - error_message = result.get("message", "") - # if error_code == API_ERR_CODE_UNAUTH: - # raise InvalidLogin(response.status, error_message) + if response.status == HTTPStatus.UNPROCESSABLE_ENTITY: + raise FatalHttpException(response.status, error_message, errors) - raise FatalHttpException(response.status, error_message) + if response.status in HTTP_CODES_NO_ACCESS: + raise AuthorizationError(response.status, error_message, errors) + + if response.status in HTTP_CODES_RETRYABLE: + raise RetryableHttpException(response.status, error_message) + + if response.status in HTTP_CODES_FATAL: + raise FatalHttpException(response.status, error_message, errors) - error_message = result.get("mesasge", "") # if reached here the HTTP response code is not currently handled raise FatalHttpException( - response.status, f"Unhandled error: {error_message}" + response.status, + f"Unhandled error: {error_message}", + errors, ) diff --git a/pyproject.toml b/pyproject.toml index 6b3cf8f..673e414 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "kassalappy" -version = "0.6.0" +version = "0.7.0" description = "" authors = ["Bendik R. Brenne "] license = "MIT"