From 6d70b2194b1f645d599bed3590d704350f6df754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Tue, 30 Jan 2024 20:07:45 +0000 Subject: [PATCH] resolve some bugs --- anilist-cmp/__init__.py | 51 ++++++++++++++++++++++++++++----- anilist-cmp/types_/responses.py | 23 ++++++++++++++- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/anilist-cmp/__init__.py b/anilist-cmp/__init__.py index c697f5a..43adb14 100644 --- a/anilist-cmp/__init__.py +++ b/anilist-cmp/__init__.py @@ -7,7 +7,7 @@ from fastapi.responses import Response if TYPE_CHECKING: - from .types_.responses import AnilistResponse, InnerMediaEntry, MediaEntry + from .types_.responses import AnilistError, AnilistErrorResponse, AnilistResponse, InnerMediaEntry, MediaEntry app = FastAPI(debug=False, title="Welcome!", version="0.0.1", openapi_url=None, redoc_url=None, docs_url=None) @@ -88,12 +88,18 @@ """ +class NoPlanningData(ValueError): + def __init__(self, user: int, *args: object) -> None: + self.user = user + super().__init__(*args) + + def format_entries_as_table(entries: dict[int, InnerMediaEntry]) -> str: rows = [ROW.format_map(entry) for entry in entries.values()] return TABLE.format(body="\n".join(rows)) -async def _fetch_user_entries(*usernames: str) -> AnilistResponse: +async def _fetch_user_entries(*usernames: str) -> AnilistResponse | AnilistErrorResponse: username1, username2 = usernames async with aiohttp.ClientSession() as session, session.post( @@ -108,11 +114,16 @@ def _restructure_entries(entries: list[MediaEntry]) -> dict[int, InnerMediaEntry def _get_common_planning(data: AnilistResponse) -> dict[int, InnerMediaEntry]: - user1_data = data["data"]["user1"] - user2_data = data["data"]["user2"] + user1_data = data["data"]["user1"]["lists"] + user2_data = data["data"]["user2"]["lists"] + + if not user1_data: + raise NoPlanningData(1) + elif not user2_data: + raise NoPlanningData(2) - user1_entries = _restructure_entries(user1_data["lists"][0]["entries"]) - user2_entries = _restructure_entries(user2_data["lists"][0]["entries"]) + user1_entries = _restructure_entries(user1_data[0]["entries"]) + user2_entries = _restructure_entries(user2_data[0]["entries"]) all_anime = user1_entries | user2_entries common_anime = user1_entries.keys() & user2_entries.keys() @@ -120,6 +131,19 @@ def _get_common_planning(data: AnilistResponse) -> dict[int, InnerMediaEntry]: return {id_: all_anime[id_] for id_ in common_anime} +def _handle_errors(errors: list[AnilistError], user1: str, user2: str) -> list[str]: + missing_users: list[str] = [] + for error in errors: + if error["message"] == "User not found": + for location in error["locations"]: + if location["column"] == 2: + missing_users.append(user1) + elif location["column"] == 17: + missing_users.append(user2) + + return missing_users + + @app.get("/") async def index() -> Response: return Response("Did you forget to add path parameters? Like /User1/User2?", media_type="text/plain") @@ -127,9 +151,22 @@ async def index() -> Response: @app.get("/{user1}/{user2}") async def get_matches(request: Request, user1: str, user2: str) -> Response: + if user1.casefold() == user2.casefold(): + return Response("Haha, you're really funny.", media_type="text/plain") + data = await _fetch_user_entries(user1.casefold(), user2.casefold()) - matching_items = _get_common_planning(data) + if errors := data.get("errors"): + errored_users = _handle_errors(errors, user1, user2) + + fmt = ", ".join(errored_users) + return Response(f"Sorry, it seems that user(s) {fmt} are not found.") + + try: + matching_items = _get_common_planning(data) # type: ignore # the type is resolved above. + except NoPlanningData as err: + errored_user = user1 if err.user == 1 else user2 + return Response(f"Sorry, but {errored_user} has no Planning entries!", media_type="text/plain") if not matching_items: return Response("No planning anime in common :(", status_code=405, media_type="text/plain") diff --git a/anilist-cmp/types_/responses.py b/anilist-cmp/types_/responses.py index d6b6e49..97336dc 100644 --- a/anilist-cmp/types_/responses.py +++ b/anilist-cmp/types_/responses.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TypedDict +from typing import Literal, TypedDict class LocalizedTitle(TypedDict): @@ -34,3 +34,24 @@ class MediaListCollectionResponse(TypedDict): class AnilistResponse(TypedDict): data: MediaListCollectionResponse + + +class AnilistErrorLocation(TypedDict): + line: int + column: int + + +class AnilistError(TypedDict): + message: str + status: int + locations: list[AnilistErrorLocation] + + +class UserEntryError(TypedDict): + user1: Literal[None] + user2: Literal[None] + + +class AnilistErrorResponse(TypedDict): + errors: list[AnilistError] + data: UserEntryError