From 3f8baed220f82db5a5ae6b43f9a52f8d7d63782e Mon Sep 17 00:00:00 2001 From: Michael Manganiello Date: Sat, 7 Jun 2025 13:06:04 -0300 Subject: [PATCH 1/9] misc: Add typing to RetroAchievements API responses Include detailed type information for RetroAchievements API responses, based on the official API documentation. --- .../services/retroachievements_types.py | 164 ++++++++++++++++++ backend/handler/metadata/ra_handler.py | 46 +++-- 2 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 backend/adapters/services/retroachievements_types.py diff --git a/backend/adapters/services/retroachievements_types.py b/backend/adapters/services/retroachievements_types.py new file mode 100644 index 000000000..0f70d428c --- /dev/null +++ b/backend/adapters/services/retroachievements_types.py @@ -0,0 +1,164 @@ +import enum +from typing import NotRequired, TypedDict + + +# https://github.com/RetroAchievements/RAWeb/blob/master/app/Platform/Enums/AchievementType.php +class RAGameAchievementType(enum.StrEnum): + PROGRESSION = "progression" + WIN_CONDITION = "win_condition" + MISSABLE = "missable" + + +# https://github.com/RetroAchievements/RAWeb/blob/master/app/Platform/Enums/ReleasedAtGranularity.php +class RAGameReleasedAtGranularity(enum.StrEnum): + DAY = "day" + MONTH = "month" + YEAR = "year" + + +# https://github.com/RetroAchievements/RAWeb/blob/master/resources/js/common/components/AwardIndicator/AwardIndicator.tsx +class RAUserCompletionProgressKind(enum.StrEnum): + COMPLETED = "completed" + MASTERED = "mastered" + BEATEN_HARDCORE = "beaten-hardcore" + BEATEN_SOFTCORE = "beaten-softcore" + + +# https://api-docs.retroachievements.org/v1/get-game-extended.html#response +class RAGameExtendedDetailsAchievement(TypedDict): + ID: int + NumAwarded: int + NumAwardedHardcore: int + Title: str + Description: str + Points: int + TrueRatio: int + Author: str + AuthorULID: str + DateModified: str # ISO 8601 datetime format + DateCreated: str # ISO 8601 datetime format + BadgeName: str + DisplayOrder: int + MemAddr: str + type: RAGameAchievementType | None + + +# https://api-docs.retroachievements.org/v1/get-game-extended.html#response +class RAGameExtendedDetails(TypedDict): + ID: int + Title: str + ConsoleID: int + ForumTopicID: int | None + ImageIcon: str + ImageTitle: str + ImageIngame: str + ImageBoxArt: str + Publisher: str + Developer: str + Genre: str + Released: str # ISO 8601 date format + ReleasedAtGranularity: RAGameReleasedAtGranularity + RichPresencePatch: str + GuideURL: str | None + Updated: str # ISO 8601 datetime format + ConsoleName: str + ParentGameID: int | None + NumDistinctPlayers: int + NumAchievements: int + Achievements: dict[ + str, RAGameExtendedDetailsAchievement + ] # Key is achievement ID as string + NumDistinctPlayersCasual: int + NumDistinctPlayersHardcore: int + + +# https://api-docs.retroachievements.org/v1/get-user-completion-progress.html#response +class RAUserCompletionProgressResult(TypedDict): + GameID: int + Title: str + ImageIcon: str + ConsoleID: int + ConsoleName: str + MaxPossible: int + NumAwarded: int + NumAwardedHardcore: int + MostRecentAwardedDate: str # ISO 8601 datetime format + HighestAwardKind: RAUserCompletionProgressKind # e.g., "beaten-hardcore" + HighestAwardDate: str # ISO 8601 datetime format + + +# https://api-docs.retroachievements.org/v1/get-user-completion-progress.html#response +class RAUserCompletionProgress(TypedDict): + Count: int + Total: int + Results: list[RAUserCompletionProgressResult] + + +# https://api-docs.retroachievements.org/v1/get-game-info-and-user-progress.html#response +class RAGameInfoAndUserProgressAchievement(TypedDict): + ID: int + NumAwarded: int + NumAwardedHardcore: int + Title: str + Description: str + Points: int + TrueRatio: int + Author: str + AuthorULID: str + DateModified: str # ISO 8601 datetime format + DateCreated: str # ISO 8601 datetime format + BadgeName: str + DisplayOrder: int + MemAddr: str + type: RAGameAchievementType | None + DateEarnedHardcore: NotRequired[str] # ISO 8601 datetime format + DateEarned: NotRequired[str] # ISO 8601 datetime format + + +# https://api-docs.retroachievements.org/v1/get-game-info-and-user-progress.html#response +class RAGameInfoAndUserProgress(TypedDict): + ID: int + Title: str + ConsoleID: int + ForumTopicID: int | None + ImageIcon: str + ImageTitle: str + ImageIngame: str + ImageBoxArt: str + Publisher: str + Developer: str + Genre: str + Released: str # ISO 8601 date format + ReleasedAtGranularity: RAGameReleasedAtGranularity + RichPresencePatch: str + GuideURL: str | None + ConsoleName: str + ParentGameID: int | None + NumDistinctPlayers: int + NumAchievements: int + Achievements: dict[ + str, RAGameInfoAndUserProgressAchievement + ] # Key is achievement ID as string + NumAwardedToUser: int + NumAwardedToUserHardcore: int + NumDistinctPlayersCasual: int + NumDistinctPlayersHardcore: int + UserCompletion: str # e.g., "100.00%" + UserCompletionHardcore: str # e.g., "100.00%" + HighestAwardKind: NotRequired[RAUserCompletionProgressKind] + HighestAwardDate: NotRequired[str] # ISO 8601 datetime format + + +# https://api-docs.retroachievements.org/v1/get-game-list.html#response +class RAGameListItem(TypedDict): + Title: str + ID: int + ConsoleID: int + ConsoleName: str + ImageIcon: str + NumAchievements: int + NumLeaderboards: int + Points: int + DateModified: str # ISO 8601 datetime format + ForumTopicID: int | None + Hashes: NotRequired[list[str]] diff --git a/backend/handler/metadata/ra_handler.py b/backend/handler/metadata/ra_handler.py index f69b480e5..c42212941 100644 --- a/backend/handler/metadata/ra_handler.py +++ b/backend/handler/metadata/ra_handler.py @@ -3,10 +3,17 @@ import http import json import os import time -from typing import Final, NotRequired, TypedDict +from collections import defaultdict +from typing import Final, NotRequired, TypedDict, cast import httpx import yarl +from adapters.services.retroachievements_types import ( + RAGameExtendedDetails, + RAGameInfoAndUserProgress, + RAGameListItem, + RAUserCompletionProgress, +) from anyio import open_file from config import ( REFRESH_RETROACHIEVEMENTS_CACHE_DAYS, @@ -185,7 +192,7 @@ class RAHandler(MetadataHandler): async def _search_rom( self, platform: Platform, rom_id: int, hash: str - ) -> dict | None: + ) -> RAGameListItem | None: if not platform.ra_id: return None @@ -200,13 +207,14 @@ class RAHandler(MetadataHandler): ) # Fetch all hashes for specific platform + roms: list[RAGameListItem] if ( REFRESH_RETROACHIEVEMENTS_CACHE_DAYS <= self._days_since_last_cache_file_update(platform.id) or not self._exists_cache_file(platform.id) ): # Write the roms result to a JSON file if older than REFRESH_RETROACHIEVEMENTS_CACHE_DAYS days - roms = await self._request(str(url)) + roms = cast(list[RAGameListItem], await self._request(str(url))) async with await open_file( self._get_hashes_file_path(platform.id), "w", @@ -223,18 +231,17 @@ class RAHandler(MetadataHandler): roms = json.loads(await json_file.read()) for rom in roms: - if hash in rom["Hashes"]: + if hash in rom.get("Hashes", ()): return rom return None - async def _get_rom_details(self, ra_id: int) -> dict: + async def _get_rom_details(self, ra_id: int) -> RAGameExtendedDetails: url = yarl.URL(self.game_details_endpoint).with_query( i=[ra_id], y=[RETROACHIEVEMENTS_API_KEY], ) - details = await self._request(str(url)) - return details + return cast(RAGameExtendedDetails, await self._request(str(url))) def get_platform(self, slug: str) -> RAGamesPlatform: platform = SLUG_TO_RA_ID.get(slug.lower(), None) @@ -291,22 +298,25 @@ class RAHandler(MetadataHandler): url = yarl.URL(self.user_complete_progression_endpoint).with_query( u=[username], y=[RETROACHIEVEMENTS_API_KEY], c=[500] ) - user_complete_progression = await self._request(str(url)) + user_complete_progression = cast( + RAUserCompletionProgress, await self._request(str(url)) + ) roms_with_progression = user_complete_progression.get("Results", []) + rom_earned_achievements: dict[int, list[EarnedAchievement]] = defaultdict(list) for rom in roms_with_progression: - rom["EarnedAchievements"] = [] - if rom.get("GameID", None): + rom_game_id = rom.get("GameID") + if rom_game_id: url = yarl.URL(self.user_game_progression_endpoint).with_query( - g=[rom["GameID"]], + g=[rom_game_id], u=[username], y=[RETROACHIEVEMENTS_API_KEY], ) - result = await self._request(str(url)) + result = cast(RAGameInfoAndUserProgress, await self._request(str(url))) for achievement in result.get("Achievements", {}).values(): - if "DateEarned" in achievement.keys(): - rom["EarnedAchievements"].append( + if achievement.get("DateEarned") and achievement.get("BadgeName"): + rom_earned_achievements[rom_game_id].append( { - "id": achievement.get("BadgeName"), + "id": achievement["BadgeName"], "date": achievement["DateEarned"], } ) @@ -319,7 +329,11 @@ class RAHandler(MetadataHandler): max_possible=rom.get("MaxPossible", None), num_awarded=rom.get("NumAwarded", None), num_awarded_hardcore=rom.get("NumAwardedHardcore", None), - earned_achievements=rom.get("EarnedAchievements", []), + earned_achievements=( + rom_earned_achievements.get(rom["GameID"], []) + if rom.get("GameID") + else [] + ), ) for rom in roms_with_progression ], From d330dc754479e1fbbe0eeecd8f0e3224276b3502 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Sun, 8 Jun 2025 20:33:39 -0400 Subject: [PATCH 2/9] cleanup search-text-field --- .../src/components/Gallery/AppBar/Search/SearchTextField.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue b/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue index 9f75ca0a0..128873a86 100644 --- a/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue +++ b/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue @@ -5,17 +5,15 @@ import type { Events } from "@/types/emitter"; import type { Emitter } from "mitt"; import { inject, nextTick, onMounted, watch } from "vue"; import { storeToRefs } from "pinia"; -import { useDisplay } from "vuetify"; import { useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; import { debounce } from "lodash"; // Props -const { xs } = useDisplay(); const { t } = useI18n(); const router = useRouter(); const romsStore = storeRoms(); -const { fetchingRoms, initialSearch } = storeToRefs(romsStore); +const { initialSearch } = storeToRefs(romsStore); const emitter = inject>("emitter"); const galleryFilterStore = storeGalleryFilter(); const { searchTerm } = storeToRefs(galleryFilterStore); From 39ce329529b63ef69be465a91708ce9920391bfb Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Sun, 8 Jun 2025 20:41:36 -0400 Subject: [PATCH 3/9] Reset pagination when changing search field value --- .../src/components/Gallery/AppBar/Search/SearchTextField.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue b/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue index 128873a86..a4394b5ef 100644 --- a/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue +++ b/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue @@ -96,6 +96,7 @@ onMounted(() => { // Check if search term is set in the URL (empty string is ok) if (searchTerm.value !== null) { + romsStore.resetPagination(); fetchRoms(); } }); @@ -105,6 +106,7 @@ watch( (query) => { if (query.search !== undefined && query.search !== searchTerm.value) { searchTerm.value = query.search as string; + romsStore.resetPagination(); fetchRoms(); } }, From e885586edbefae565615fbd8e0f9606a2c708b31 Mon Sep 17 00:00:00 2001 From: Michael Manganiello Date: Sun, 8 Jun 2025 14:28:35 -0300 Subject: [PATCH 4/9] misc: Create RetroAchievements service adapter Add a new service adapter for the RetroAchievements API, to separate concerns with RomM's handler for metadata. This adapter is agnostic to the handler and only provides methods to interact with the API, and correctly return typed responses. The API authorization was also improved to be handled by a specific `httpx.Auth` class that sets the `y` parameter for each request. --- .../adapters/services/retroachievements.py | 187 ++++++++++++++++++ backend/handler/metadata/ra_handler.py | 58 ++---- 2 files changed, 203 insertions(+), 42 deletions(-) create mode 100644 backend/adapters/services/retroachievements.py diff --git a/backend/adapters/services/retroachievements.py b/backend/adapters/services/retroachievements.py new file mode 100644 index 000000000..1810aee86 --- /dev/null +++ b/backend/adapters/services/retroachievements.py @@ -0,0 +1,187 @@ +import asyncio +import http +from typing import Generator, cast + +import httpx +import yarl +from adapters.services.retroachievements_types import ( + RAGameExtendedDetails, + RAGameInfoAndUserProgress, + RAGameListItem, + RAUserCompletionProgress, +) +from config import RETROACHIEVEMENTS_API_KEY +from fastapi import HTTPException, status +from logger.logger import log +from utils.context import ctx_httpx_client + + +class RetroAchievementsAuth(httpx.Auth): + """RetroAchievements API authentication class. + + Reference: https://api-docs.retroachievements.org/getting-started.html#quick-start-http-requests + """ + + def __init__(self, token: str): + self.token = token + + def auth_flow( + self, request: httpx.Request + ) -> Generator[httpx.Request, httpx.Response, None]: + request.url = request.url.copy_merge_params({"y": self.token}) + yield request + + +class RetroAchievementsService: + """Service to interact with the RetroAchievements API. + + Reference: https://api-docs.retroachievements.org/ + """ + + def __init__( + self, + base_url: str | None = None, + token: str = RETROACHIEVEMENTS_API_KEY, + ) -> None: + self.url = yarl.URL(base_url or "https://retroachievements.org/API") + self.token = token + + @property + def _auth(self) -> httpx.Auth: + return RetroAchievementsAuth(token=self.token) + + async def _request(self, url: str, request_timeout: int = 120) -> dict: + httpx_client = ctx_httpx_client.get() + log.debug( + "API request: URL=%s, Timeout=%s", + url, + request_timeout, + ) + try: + res = await httpx_client.get(url, auth=self._auth, timeout=request_timeout) + res.raise_for_status() + return res.json() + except httpx.NetworkError as exc: + log.critical( + "Connection error: can't connect to RetroAchievements", exc_info=True + ) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Can't connect to RetroAchievements, check your internet connection", + ) from exc + except httpx.HTTPStatusError as err: + if err.response.status_code == http.HTTPStatus.TOO_MANY_REQUESTS: + # Retry after 2 seconds if rate limit hit + await asyncio.sleep(2) + else: + # Log the error and return an empty dict if the request fails with a different code + log.error(err) + return {} + except httpx.TimeoutException: + # Retry the request once if it times out + pass + + try: + log.debug( + "API request: URL=%s, Timeout=%s", + url, + request_timeout, + ) + res = await httpx_client.get(url, auth=self._auth, timeout=request_timeout) + res.raise_for_status() + except (httpx.HTTPStatusError, httpx.TimeoutException) as err: + if ( + isinstance(err, httpx.HTTPStatusError) + and err.response.status_code == http.HTTPStatus.UNAUTHORIZED + ): + return {} + + log.error(err) + return {} + + return res.json() + + async def get_game_extended_details(self, game_id: int) -> RAGameExtendedDetails: + """Retrieve extended metadata about a game, targeted via its unique ID. + + Reference: https://api-docs.retroachievements.org/v1/get-game-extended.html + """ + url = self.url.joinpath("API_GetGameExtended.php").with_query( + i=[game_id], + ) + response = await self._request(str(url)) + return cast(RAGameExtendedDetails, response) + + async def get_game_list( + self, + system_id: int, + *, + only_games_with_achievements: bool = False, + include_hashes: bool = False, + limit: int | None = None, + offset: int | None = None, + ) -> list[RAGameListItem]: + """Retrieve the complete list of games for a specified console on the site, targeted by the console ID. + + Reference: https://api-docs.retroachievements.org/v1/get-game-list.html + """ + params: dict[str, list[str]] = {"i": [str(system_id)]} + if only_games_with_achievements: + params["f"] = ["1"] + if include_hashes: + params["h"] = ["1"] + if limit is not None: + params["c"] = [str(limit)] + if offset is not None: + params["o"] = [str(offset)] + + url = self.url.joinpath("API_GetGameList.php").with_query(**params) + response = await self._request(str(url)) + return cast(list[RAGameListItem], response) + + async def get_user_completion_progress( + self, + username: str, + *, + limit: int | None = None, + offset: int | None = None, + ) -> RAUserCompletionProgress: + """Retrieve a given user's completion progress, targeted by their username. + + Reference: https://api-docs.retroachievements.org/v1/get-user-completion-progress.html + """ + params: dict[str, list[str]] = {"u": [username]} + if limit is not None: + params["c"] = [str(limit)] + if offset is not None: + params["o"] = [str(offset)] + + url = self.url.joinpath("API_GetUserCompletionProgress.php").with_query( + **params + ) + response = await self._request(str(url)) + return cast(RAUserCompletionProgress, response) + + async def get_user_game_progress( + self, + username: str, + game_id: int, + *, + include_award_metadata: bool = False, + ) -> RAGameInfoAndUserProgress: + """Retrieve extended metadata about a game, in addition to a user's progress about that game. + + Reference: https://api-docs.retroachievements.org/v1/get-game-info-and-user-progress.html + """ + params: dict[str, list[str]] = { + "u": [username], + "g": [str(game_id)], + } + if include_award_metadata: + params["a"] = ["1"] + + url = self.url.joinpath("API_GetGameInfoAndUserProgress.php").with_query( + **params + ) + response = await self._request(str(url)) + return cast(RAGameInfoAndUserProgress, response) diff --git a/backend/handler/metadata/ra_handler.py b/backend/handler/metadata/ra_handler.py index c42212941..b289824a9 100644 --- a/backend/handler/metadata/ra_handler.py +++ b/backend/handler/metadata/ra_handler.py @@ -4,16 +4,12 @@ import json import os import time from collections import defaultdict -from typing import Final, NotRequired, TypedDict, cast +from typing import Final, NotRequired, TypedDict import httpx import yarl -from adapters.services.retroachievements_types import ( - RAGameExtendedDetails, - RAGameInfoAndUserProgress, - RAGameListItem, - RAUserCompletionProgress, -) +from adapters.services.retroachievements import RetroAchievementsService +from adapters.services.retroachievements_types import RAGameListItem from anyio import open_file from config import ( REFRESH_RETROACHIEVEMENTS_CACHE_DAYS, @@ -83,15 +79,7 @@ class RAUserProgression(TypedDict): class RAHandler(MetadataHandler): def __init__(self) -> None: - self.BASE_URL = "https://retroachievements.org/API" - self.search_endpoint = f"{self.BASE_URL}/API_GetGameList.php" - self.game_details_endpoint = f"{self.BASE_URL}/API_GetGameExtended.php" - self.user_complete_progression_endpoint = ( - f"{self.BASE_URL}/API_GetUserCompletionProgress.php" - ) - self.user_game_progression_endpoint = ( - f"{self.BASE_URL}/API_GetGameInfoAndUserProgress.php" - ) + self.ra_service = RetroAchievementsService() self.HASHES_FILE_NAME = "ra_hashes.json" def _get_rom_base_path(self, platform_id: int, rom_id: int) -> str: @@ -199,13 +187,6 @@ class RAHandler(MetadataHandler): self._create_resources_path(platform.id, rom_id) - url = yarl.URL(self.search_endpoint).with_query( - i=[platform.ra_id], - f=["1"], # If 1, only return games that have achievements. Defaults to 0. - h=["1"], # If 1, also return supported hashes for games. Defaults to 0. - y=[RETROACHIEVEMENTS_API_KEY], - ) - # Fetch all hashes for specific platform roms: list[RAGameListItem] if ( @@ -214,7 +195,11 @@ class RAHandler(MetadataHandler): or not self._exists_cache_file(platform.id) ): # Write the roms result to a JSON file if older than REFRESH_RETROACHIEVEMENTS_CACHE_DAYS days - roms = cast(list[RAGameListItem], await self._request(str(url))) + roms = await self.ra_service.get_game_list( + system_id=platform.ra_id, + only_games_with_achievements=True, + include_hashes=True, + ) async with await open_file( self._get_hashes_file_path(platform.id), "w", @@ -236,13 +221,6 @@ class RAHandler(MetadataHandler): return None - async def _get_rom_details(self, ra_id: int) -> RAGameExtendedDetails: - url = yarl.URL(self.game_details_endpoint).with_query( - i=[ra_id], - y=[RETROACHIEVEMENTS_API_KEY], - ) - return cast(RAGameExtendedDetails, await self._request(str(url))) - def get_platform(self, slug: str) -> RAGamesPlatform: platform = SLUG_TO_RA_ID.get(slug.lower(), None) @@ -265,7 +243,7 @@ class RAHandler(MetadataHandler): return RAGameRom(ra_id=None) try: - rom_details = await self._get_rom_details(rom["ID"]) + rom_details = await self.ra_service.get_game_extended_details(rom["ID"]) return RAGameRom( ra_id=rom["ID"], ra_metadata=RAMetadata( @@ -295,23 +273,19 @@ class RAHandler(MetadataHandler): return RAGameRom(ra_id=None) async def get_user_progression(self, username: str) -> RAUserProgression: - url = yarl.URL(self.user_complete_progression_endpoint).with_query( - u=[username], y=[RETROACHIEVEMENTS_API_KEY], c=[500] - ) - user_complete_progression = cast( - RAUserCompletionProgress, await self._request(str(url)) + user_complete_progression = await self.ra_service.get_user_completion_progress( + username=username, + limit=500, ) roms_with_progression = user_complete_progression.get("Results", []) rom_earned_achievements: dict[int, list[EarnedAchievement]] = defaultdict(list) for rom in roms_with_progression: rom_game_id = rom.get("GameID") if rom_game_id: - url = yarl.URL(self.user_game_progression_endpoint).with_query( - g=[rom_game_id], - u=[username], - y=[RETROACHIEVEMENTS_API_KEY], + result = await self.ra_service.get_user_game_progress( + username=username, + game_id=rom_game_id, ) - result = cast(RAGameInfoAndUserProgress, await self._request(str(url))) for achievement in result.get("Achievements", {}).values(): if achievement.get("DateEarned") and achievement.get("BadgeName"): rom_earned_achievements[rom_game_id].append( From fe1a9ce2a7c6e25d65fcee71ed33a1fe56aaf04a Mon Sep 17 00:00:00 2001 From: Michael Manganiello Date: Mon, 9 Jun 2025 09:48:03 -0300 Subject: [PATCH 5/9] fix: Use aiohttp for RetroAchievements API calls This change replaces the `httpx` client with `aiohttp` for the RetroAchievements API service. The main reason for this change is that `httpx` has an unavoidable log line with `INFO` level, which includes the request full URL, containing the user's API key. `httpx` has had an [open discussion](https://github.com/encode/httpx/discussions/2765) regarding this security issue for almost two years. The change to `aiohttp` is painless, and would allow us to migrate more of the codebase to it in the future, to avoid leaking sensitive information in logs. --- .../adapters/services/retroachievements.py | 66 ++--- backend/main.py | 8 +- backend/utils/context.py | 17 +- poetry.lock | 273 +++++++++++++++++- pyproject.toml | 1 + 5 files changed, 324 insertions(+), 41 deletions(-) diff --git a/backend/adapters/services/retroachievements.py b/backend/adapters/services/retroachievements.py index 1810aee86..15fbbdda9 100644 --- a/backend/adapters/services/retroachievements.py +++ b/backend/adapters/services/retroachievements.py @@ -1,8 +1,8 @@ import asyncio import http -from typing import Generator, cast +from typing import cast -import httpx +import aiohttp import yarl from adapters.services.retroachievements_types import ( RAGameExtendedDetails, @@ -10,26 +10,22 @@ from adapters.services.retroachievements_types import ( RAGameListItem, RAUserCompletionProgress, ) +from aiohttp.client import ClientTimeout from config import RETROACHIEVEMENTS_API_KEY from fastapi import HTTPException, status from logger.logger import log -from utils.context import ctx_httpx_client +from utils.context import ctx_aiohttp_session -class RetroAchievementsAuth(httpx.Auth): - """RetroAchievements API authentication class. +async def auth_middleware( + req: aiohttp.ClientRequest, handler: aiohttp.ClientHandlerType +) -> aiohttp.ClientResponse: + """RetroAchievements API authentication mechanism. Reference: https://api-docs.retroachievements.org/getting-started.html#quick-start-http-requests """ - - def __init__(self, token: str): - self.token = token - - def auth_flow( - self, request: httpx.Request - ) -> Generator[httpx.Request, httpx.Response, None]: - request.url = request.url.copy_merge_params({"y": self.token}) - yield request + req.url = req.url.update_query({"y": RETROACHIEVEMENTS_API_KEY}) + return await handler(req) class RetroAchievementsService: @@ -41,27 +37,28 @@ class RetroAchievementsService: def __init__( self, base_url: str | None = None, - token: str = RETROACHIEVEMENTS_API_KEY, ) -> None: self.url = yarl.URL(base_url or "https://retroachievements.org/API") - self.token = token - - @property - def _auth(self) -> httpx.Auth: - return RetroAchievementsAuth(token=self.token) async def _request(self, url: str, request_timeout: int = 120) -> dict: - httpx_client = ctx_httpx_client.get() + aiohttp_session = ctx_aiohttp_session.get() log.debug( "API request: URL=%s, Timeout=%s", url, request_timeout, ) try: - res = await httpx_client.get(url, auth=self._auth, timeout=request_timeout) + res = await aiohttp_session.get( + url, + middlewares=(auth_middleware,), + timeout=ClientTimeout(total=request_timeout), + ) res.raise_for_status() - return res.json() - except httpx.NetworkError as exc: + return await res.json() + except aiohttp.ServerTimeoutError: + # Retry the request once if it times out + pass + except aiohttp.ClientConnectionError as exc: log.critical( "Connection error: can't connect to RetroAchievements", exc_info=True ) @@ -69,17 +66,14 @@ class RetroAchievementsService: status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Can't connect to RetroAchievements, check your internet connection", ) from exc - except httpx.HTTPStatusError as err: - if err.response.status_code == http.HTTPStatus.TOO_MANY_REQUESTS: + except aiohttp.ClientResponseError as err: + if err.status == http.HTTPStatus.TOO_MANY_REQUESTS: # Retry after 2 seconds if rate limit hit await asyncio.sleep(2) else: # Log the error and return an empty dict if the request fails with a different code log.error(err) return {} - except httpx.TimeoutException: - # Retry the request once if it times out - pass try: log.debug( @@ -87,19 +81,23 @@ class RetroAchievementsService: url, request_timeout, ) - res = await httpx_client.get(url, auth=self._auth, timeout=request_timeout) + res = await aiohttp_session.get( + url, + middlewares=(auth_middleware,), + timeout=ClientTimeout(total=request_timeout), + ) res.raise_for_status() - except (httpx.HTTPStatusError, httpx.TimeoutException) as err: + except (aiohttp.ClientResponseError, aiohttp.ServerTimeoutError) as err: if ( - isinstance(err, httpx.HTTPStatusError) - and err.response.status_code == http.HTTPStatus.UNAUTHORIZED + isinstance(err, aiohttp.ClientResponseError) + and err.status == http.HTTPStatus.UNAUTHORIZED ): return {} log.error(err) return {} - return res.json() + return await res.json() async def get_game_extended_details(self, game_id: int) -> RAGameExtendedDetails: """Retrieve extended metadata about a game, targeted via its unique ID. diff --git a/backend/main.py b/backend/main.py index 7636bc181..e55ba5d07 100644 --- a/backend/main.py +++ b/backend/main.py @@ -43,7 +43,12 @@ from handler.socket_handler import socket_handler from logger.log_middleware import LOGGING_CONFIG, CustomLoggingMiddleware from starlette.middleware.authentication import AuthenticationMiddleware from utils import get_version -from utils.context import ctx_httpx_client, initialize_context, set_context_middleware +from utils.context import ( + ctx_aiohttp_session, + ctx_httpx_client, + initialize_context, + set_context_middleware, +) logging.config.dictConfig(LOGGING_CONFIG) @@ -51,6 +56,7 @@ logging.config.dictConfig(LOGGING_CONFIG) @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: async with initialize_context(): + app.state.aiohttp_session = ctx_aiohttp_session.get() app.state.httpx_client = ctx_httpx_client.get() yield diff --git a/backend/utils/context.py b/backend/utils/context.py index 817de73ee..bc8835174 100644 --- a/backend/utils/context.py +++ b/backend/utils/context.py @@ -3,11 +3,13 @@ from contextlib import asynccontextmanager from contextvars import ContextVar, Token from typing import TypeVar +import aiohttp import httpx from fastapi import Request, Response _T = TypeVar("_T") +ctx_aiohttp_session: ContextVar[aiohttp.ClientSession] = ContextVar("aiohttp_session") ctx_httpx_client: ContextVar[httpx.AsyncClient] = ContextVar("httpx_client") @@ -24,9 +26,13 @@ async def set_context_var( @asynccontextmanager async def initialize_context() -> AsyncGenerator[None, None]: """Initialize context variables.""" - async with httpx.AsyncClient() as httpx_client: - async with set_context_var(ctx_httpx_client, httpx_client): - yield + async with ( + aiohttp.ClientSession() as aiohttp_session, + httpx.AsyncClient() as httpx_client, + set_context_var(ctx_aiohttp_session, aiohttp_session), + set_context_var(ctx_httpx_client, httpx_client), + ): + yield async def set_context_middleware( @@ -37,5 +43,8 @@ async def set_context_middleware( This middleware is needed because the context initialized during the lifespan process is not available in the request-response cycle. """ - async with set_context_var(ctx_httpx_client, request.app.state.httpx_client): + async with ( + set_context_var(ctx_aiohttp_session, request.app.state.aiohttp_session), + set_context_var(ctx_httpx_client, request.app.state.httpx_client), + ): return await call_next(request) diff --git a/poetry.lock b/poetry.lock index 0ecb76459..e649fbd25 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,139 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + +[[package]] +name = "aiohttp" +version = "3.12.11" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.12.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff576cb82b995ff213e58255bc776a06ebd5ebb94a587aab2fb5df8ee4e3f967"}, + {file = "aiohttp-3.12.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fe3a9ae8a7c93bec5b7cfacfbc781ed5ae501cf6a6113cf3339b193af991eaf9"}, + {file = "aiohttp-3.12.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:efafc6f8c7c49ff567e0f02133b4d50eef5183cf96d4b0f1c7858d478e9751f6"}, + {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6866da6869cc60d84921b55330d23cbac4f243aebfabd9da47bbc40550e6548"}, + {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:14aa6f41923324618687bec21adf1d5e8683264ccaa6266c38eb01aeaa404dea"}, + {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4aec7c3ccf2ed6b55db39e36eb00ad4e23f784fca2d38ea02e6514c485866dc"}, + {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efd174af34bd80aa07813a69fee000ce8745962e2d3807c560bdf4972b5748e4"}, + {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb02a172c073b0aaf792f0b78d02911f124879961d262d3163119a3e91eec31d"}, + {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcf5791dcd63e1fc39f5b0d4d16fe5e6f2b62f0f3b0f1899270fa4f949763317"}, + {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:47f7735b7e44965bd9c4bde62ca602b1614292278315e12fa5afbcc9f9180c28"}, + {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d211453930ab5995e99e3ffa7c5c33534852ad123a11761f1bf7810cd853d3d8"}, + {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:104f1f9135be00c8a71c5fc53ac7d49c293a8eb310379d2171f0e41172277a09"}, + {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e6cbaf3c02ef605b6f251d8bb71b06632ba24e365c262323a377b639bcfcbdae"}, + {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9d9922bc6cca3bc7a8f8b60a3435f6bca6e33c8f9490f6079a023cfb4ee65af0"}, + {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:554f4338611155e7d2f0dc01e71e71e5f6741464508cbc31a74eb35c9fb42982"}, + {file = "aiohttp-3.12.11-cp310-cp310-win32.whl", hash = "sha256:421ca03e2117d8756479e04890659f6b356d6399bbdf07af5a32d5c8b4ace5ac"}, + {file = "aiohttp-3.12.11-cp310-cp310-win_amd64.whl", hash = "sha256:cd58a0fae0d13a44456953d43706f9457b231879c4b3c9d0a1e0c6e2a4913d46"}, + {file = "aiohttp-3.12.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a7603f3998cd2893801d254072aaf1b5117183fcf5e726b6c27fc4239dc8c30a"}, + {file = "aiohttp-3.12.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:afe8c1860fb0df6e94725339376628e915b2b85e734eca4d14281ed5c11275b0"}, + {file = "aiohttp-3.12.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f014d909931e34f81b0080b289642d4fc4f4a700a161bd694a5cebdd77882ab5"}, + {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:734e64ceb8918b3d7099b2d000e174d8d944fb7d494de522cecb0fa45ffcb0cd"}, + {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4b603513b4596a8b80bfbedcb33e9f8ed93f44d3dfaac97db0bb9185a6d2c5c0"}, + {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:196fbd7951b89d9a4be3a09e1f49b3534eb0b764989df66b429e8685138f8d27"}, + {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1585fefa6a62a1140bf3e439f9648cb5bf360be2bbe76d057dddd175c030e30c"}, + {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e2874e665c771e6c87e81f8d4ac64d999da5e1a110b3ae0088b035529a08d5"}, + {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6563fa3bfb79f892a24d3f39ca246c7409cf3b01a3a84c686e548a69e4fc1bf"}, + {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f31bfeb53cfc5e028a0ade48ef76a3580016b92007ceb8311f5bd1b4472b7007"}, + {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fa806cdb0b7e99fb85daea0de0dda3895eea6a624f962f3800dfbbfc07f34fb6"}, + {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:210470f8078ecd1f596247a70f17d88c4e785ffa567ab909939746161f304444"}, + {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:cb9af1ce647cda1707d7b7e23b36eead3104ed959161f14f4ebc51d9b887d4a2"}, + {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ccef35cc9e96bb3fcd79f3ef9d6ae4f72c06585c2e818deafc4a499a220904a1"}, + {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e8ccb376eaf184bcecd77711697861095bc3352c912282e33d065222682460da"}, + {file = "aiohttp-3.12.11-cp311-cp311-win32.whl", hash = "sha256:7c345f7e7f10ac21a48ffd387c04a17da06f96bd087d55af30d1af238e9e164d"}, + {file = "aiohttp-3.12.11-cp311-cp311-win_amd64.whl", hash = "sha256:b461f7918c8042e927f629eccf7c120197135bd2eb14cc12fffa106b937d051b"}, + {file = "aiohttp-3.12.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3d222c693342ccca64320410ada8f06a47c4762ff82de390f3357a0e51ca102c"}, + {file = "aiohttp-3.12.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f50c10bd5799d82a9effe90d5d5840e055a2c94e208b76f9ed9e6373ca2426fe"}, + {file = "aiohttp-3.12.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a01a21975b0fd5160886d9f2cd6ed13cdfc8d59f2a51051708ed729afcc2a2fb"}, + {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39d29b6888ddd5a120dba1d52c78c0b45f5f34e227a23696cbece684872e62bd"}, + {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1df121c3ffcc5f7381cd4c84e8554ff121f558e92c318f48e049843b47ee9f1b"}, + {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:644f74197757e26266a5f57af23424f8cd506c1ef70d9b288e21244af69d6fdc"}, + {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726d9a15a1fd1058b2d27d094b1fec627e9fd92882ca990d90ded9b7c550bd21"}, + {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405a60b979da942cec2c26381683bc230f3bcca346bf23a59c1dfc397e44b17b"}, + {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27e75e96a4a747756c2f59334e81cbb9a398e015bc9e08b28f91090e5f3a85ef"}, + {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15e1da30ac8bf92fb3f8c245ff53ace3f0ea1325750cc2f597fb707140dfd950"}, + {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0329934d4df1500f13449c1db205d662123d9d0ee1c9d0c8c0cb997cdac75710"}, + {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2a06b2a031d6c828828317ee951f07d8a0455edc9cd4fc0e0432fd6a4dfd612d"}, + {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87ece62697b8792e595627c4179f0eca4b038f39b0b354e67a149fa6f83d9493"}, + {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5c981b7659379b5cb3b149e480295adfcdf557b5892a792519a56badbe9f33ef"}, + {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e6fb2170cb0b9abbe0bee2767b08bb4a3dbf01583880ecea97bca9f3f918ea78"}, + {file = "aiohttp-3.12.11-cp312-cp312-win32.whl", hash = "sha256:f20e4ec84a26f91adc8c54345a383095248d11851f257c816e8f1d853a6cef4c"}, + {file = "aiohttp-3.12.11-cp312-cp312-win_amd64.whl", hash = "sha256:b54d4c3cd77cf394e71a7ad5c3b8143a5bfe105a40fc693bcdfe472a286f1d95"}, + {file = "aiohttp-3.12.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5fadc4b67f972a701805aa501cd9d22cdbeda21f9c9ae85e60678f84b1727a16"}, + {file = "aiohttp-3.12.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:144d67c29ae36f052584fc45a363e92798441a5af5762d83037aade3e2aa9dc5"}, + {file = "aiohttp-3.12.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b73299e4bf37d14c6e4ca5ce7087b44914a8d9e1f40faedc271f28d64ec277e"}, + {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1226325e98e6d3cdfdaca639efdc3af8e82cd17287ae393626d1bd60626b0e93"}, + {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0ecae011f2f779271407f2959877230670de3c48f67e5db9fbafa9fddbfa3a"}, + {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8a711883eedcd55f2e1ba218d8224b9f20f1dfac90ffca28e78daf891667e3a"}, + {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2601c1fcd9b67e632548cfd3c760741b31490502f6f3e5e21287678c1c6fa1b2"}, + {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d5b11ea794ee54b33d0d817a1aec0ef0dd2026f070b493bc5a67b7e413b95d4"}, + {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:109b3544138ce8a5aca598d5e7ff958699e3e19ee3675d27d5ee9c2e30765a4a"}, + {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b795085d063d24c6d09300c85ddd6b9c49816d5c498b40b6899ca24584e936e4"}, + {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ebcbc113f40e4c9c0f8d2b6b31a2dd2a9768f3fa5f623b7e1285684e24f5159f"}, + {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:590e5d792150d75fa34029d0555b126e65ad50d66818a996303de4af52b65b32"}, + {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9c2a4dec596437b02f0c34f92ea799d6e300184a0304c1e54e462af52abeb0a8"}, + {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aace119abc495cc4ced8745e3faceb0c22e8202c60b55217405c5f389b569576"}, + {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd749731390520a2dc1ce215bcf0ee1018c3e2e3cd834f966a02c0e71ad7d637"}, + {file = "aiohttp-3.12.11-cp313-cp313-win32.whl", hash = "sha256:65952736356d1fbc9efdd17492dce36e2501f609a14ccb298156e392d3ad8b83"}, + {file = "aiohttp-3.12.11-cp313-cp313-win_amd64.whl", hash = "sha256:854132093e12dd77f5c07975581c42ae51a6a8868dcbbb509c77d1963c3713b7"}, + {file = "aiohttp-3.12.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4f1f92cde9d9a470121a0912566585cf989f0198718477d73f3ae447a6911644"}, + {file = "aiohttp-3.12.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f36958b508e03d6c5b2ed3562f517feb415d7cc3a9b2255f319dcedb1517561a"}, + {file = "aiohttp-3.12.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06e18aaa360d59dd25383f18454f79999915d063b7675cf0ac6e7146d1f19fd1"}, + {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:019d6075bc18fdc1e47e9dabaf339c9cc32a432aca4894b55e23536919640d87"}, + {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:063b0de9936ed9b9222aa9bdf34b1cc731d34138adfc4dbb1e4bbde1ab686778"}, + {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8437e3d8041d4a0d73a48c563188d5821067228d521805906e92f25576076f95"}, + {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:340ee38cecd533b48f1fe580aa4eddfb9c77af2a80c58d9ff853b9675adde416"}, + {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f672d8dbca49e9cf9e43de934ee9fd6716740263a7e37c1a3155d6195cdef285"}, + {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4a36ae8bebb71276f1aaadb0c08230276fdadad88fef35efab11d17f46b9885"}, + {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b63b3b5381791f96b07debbf9e2c4e909c87ecbebe4fea9dcdc82789c7366234"}, + {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:8d353c5396964a79b505450e8efbfd468b0a042b676536505e8445d9ab1ef9ae"}, + {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8ddd775457180d149ca0dbc4ebff5616948c09fa914b66785e5f23227fec5a05"}, + {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:29f642b386daf2fadccbcd2bc8a3d6541a945c0b436f975c3ce0ec318b55ad6e"}, + {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:cb907dcd8899084a56bb13a74e9fdb49070aed06229ae73395f49a9ecddbd9b1"}, + {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:760846271518d649be968cee1b245b84d348afe896792279312ca758511d798f"}, + {file = "aiohttp-3.12.11-cp39-cp39-win32.whl", hash = "sha256:d28f7d2b68f4ef4006ca92baea02aa2dce2b8160cf471e4c3566811125f5c8b9"}, + {file = "aiohttp-3.12.11-cp39-cp39-win_amd64.whl", hash = "sha256:2af98debfdfcc52cae5713bbfbfe3328fc8591c6f18c93cf3b61749de75f6ef2"}, + {file = "aiohttp-3.12.11.tar.gz", hash = "sha256:a5149ae1b11ce4cf8b122846bfa3d7c5f29fe3bfe6745ab21b3eea9615bc5564"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.1.2" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.3.2" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, + {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" [[package]] name = "alembic" @@ -84,6 +219,26 @@ files = [ astroid = ["astroid (>=2,<4)"] test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] + [[package]] name = "authlib" version = "1.4.1" @@ -666,6 +821,120 @@ files = [ [package.dependencies] python-dateutil = ">=2.7" +[[package]] +name = "frozenlist" +version = "1.6.2" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "frozenlist-1.6.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:92836b9903e52f787f4f4bfc6cf3b03cf19de4cbc09f5969e58806f876d8647f"}, + {file = "frozenlist-1.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3af419982432a13a997451e611ff7681a4fbf81dca04f70b08fc51106335ff0"}, + {file = "frozenlist-1.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1570ba58f0852a6e6158d4ad92de13b9aba3474677c3dee827ba18dcf439b1d8"}, + {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0de575df0135949c4049ae42db714c43d1693c590732abc78c47a04228fc1efb"}, + {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b6eaba27ec2b3c0af7845619a425eeae8d510d5cc83fb3ef80569129238153b"}, + {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af1ee5188d2f63b4f09b67cf0c60b8cdacbd1e8d24669eac238e247d8b157581"}, + {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9179c5186eb996c0dd7e4c828858ade4d7a8d1d12dd67320675a6ae7401f2647"}, + {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38814ebc3c6bb01dc3bb4d6cffd0e64c19f4f2d03e649978aeae8e12b81bdf43"}, + {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dbcab0531318fc9ca58517865fae63a2fe786d5e2d8f3a56058c29831e49f13"}, + {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7472e477dc5d6a000945f45b6e38cbb1093fdec189dc1e98e57f8ab53f8aa246"}, + {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:17c230586d47332774332af86cc1e69ee095731ec70c27e5698dfebb9db167a0"}, + {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:946a41e095592cf1c88a1fcdd154c13d0ef6317b371b817dc2b19b3d93ca0811"}, + {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d90c9b36c669eb481de605d3c2da02ea98cba6a3f5e93b3fe5881303026b2f14"}, + {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8651dd2d762d6eefebe8450ec0696cf3706b0eb5e46463138931f70c667ba612"}, + {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:48400e6a09e217346949c034105b0df516a1b3c5aa546913b70b71b646caa9f5"}, + {file = "frozenlist-1.6.2-cp310-cp310-win32.whl", hash = "sha256:56354f09082262217f837d91106f1cc204dd29ac895f9bbab33244e2fa948bd7"}, + {file = "frozenlist-1.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3016ff03a332cdd2800f0eed81ca40a2699b2f62f23626e8cf81a2993867978a"}, + {file = "frozenlist-1.6.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb66c5d48b89701b93d58c31a48eb64e15d6968315a9ccc7dfbb2d6dc2c62ab7"}, + {file = "frozenlist-1.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8fb9aee4f7b495044b868d7e74fb110d8996e8fddc0bfe86409c7fc7bd5692f0"}, + {file = "frozenlist-1.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:48dde536fc4d8198fad4e211f977b1a5f070e6292801decf2d6bc77b805b0430"}, + {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91dd2fb760f4a2c04b3330e0191787c3437283f9241f0b379017d4b13cea8f5e"}, + {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f01f34f8a5c7b4d74a1c65227678822e69801dcf68edd4c11417a7c83828ff6f"}, + {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f43f872cc4cfc46d9805d0e71302e9c39c755d5ad7572198cd2ceb3a291176cc"}, + {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f96cc8ab3a73d42bcdb6d9d41c3dceffa8da8273ac54b71304b891e32de8b13"}, + {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c0b257123320832cce9bea9935c860e4fa625b0e58b10db49fdfef70087df81"}, + {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23dc4def97ccc0232f491836050ae664d3d2352bb43ad4cd34cd3399ad8d1fc8"}, + {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3663463c040315f025bd6a5f88b3748082cfe111e90fd422f71668c65de52"}, + {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:16b9e7b59ea6eef876a8a5fac084c95fd4bac687c790c4d48c0d53c6bcde54d1"}, + {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:308b40d32a98a8d0d09bc28e4cbc13a0b803a0351041d4548564f28f6b148b05"}, + {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:baf585d8968eaad6c1aae99456c40978a9fa822ccbdb36fd4746b581ef338192"}, + {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4dfdbdb671a6af6ea1a363b210373c8233df3925d9a7fb99beaa3824f6b99656"}, + {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:94916e3acaeb8374d5aea9c37db777c9f0a2b9be46561f5de30064cbbbfae54a"}, + {file = "frozenlist-1.6.2-cp311-cp311-win32.whl", hash = "sha256:0453e3d2d12616949cb2581068942a0808c7255f2abab0676d2da7db30f9ea11"}, + {file = "frozenlist-1.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:fb512753c4bbf0af03f6b9c7cc5ecc9bbac2e198a94f61aaabd26c3cf3229c8c"}, + {file = "frozenlist-1.6.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:48544d07404d7fcfccb6cc091922ae10de4d9e512c537c710c063ae8f5662b85"}, + {file = "frozenlist-1.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ee0cf89e7638de515c0bb2e8be30e8e2e48f3be9b6c2f7127bca4a1f35dff45"}, + {file = "frozenlist-1.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e084d838693d73c0fe87d212b91af80c18068c95c3d877e294f165056cedfa58"}, + {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d918b01781c6ebb5b776c18a87dd3016ff979eb78626aaca928bae69a640c3"}, + {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2892d9ab060a847f20fab83fdb886404d0f213f648bdeaebbe76a6134f0973d"}, + {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbd2225d7218e7d386f4953d11484b0e38e5d134e85c91f0a6b0f30fb6ae25c4"}, + {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b679187cba0a99f1162c7ec1b525e34bdc5ca246857544d16c1ed234562df80"}, + {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bceb7bd48849d4b76eac070a6d508aa3a529963f5d9b0a6840fd41fb381d5a09"}, + {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b1b79ae86fdacc4bf842a4e0456540947abba64a84e61b5ae24c87adb089db"}, + {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c5c3c575148aa7308a38709906842039d7056bf225da6284b7a11cf9275ac5d"}, + {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:16263bd677a31fe1a5dc2b803b564e349c96f804a81706a62b8698dd14dbba50"}, + {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2e51b2054886ff7db71caf68285c2cd936eb7a145a509965165a2aae715c92a7"}, + {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ae1785b76f641cce4efd7e6f49ca4ae456aa230383af5ab0d4d3922a7e37e763"}, + {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:30155cc481f73f92f47ab1e858a7998f7b1207f9b5cf3b3cba90ec65a7f224f5"}, + {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e1a1d82f2eb3d2875a8d139ae3f5026f7797f9de5dce44f53811ab0a883e85e7"}, + {file = "frozenlist-1.6.2-cp312-cp312-win32.whl", hash = "sha256:84105cb0f3479dfa20b85f459fb2db3b0ee52e2f84e86d447ea8b0de1fb7acdd"}, + {file = "frozenlist-1.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:eecc861bd30bc5ee3b04a1e6ebf74ed0451f596d91606843f3edbd2f273e2fe3"}, + {file = "frozenlist-1.6.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2ad8851ae1f6695d735f8646bf1e68675871789756f7f7e8dc8224a74eabb9d0"}, + {file = "frozenlist-1.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cd2d5abc0ccd99a2a5b437987f3b1e9c265c1044d2855a09ac68f09bbb8082ca"}, + {file = "frozenlist-1.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15c33f665faa9b8f8e525b987eeaae6641816e0f6873e8a9c4d224338cebbb55"}, + {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e6c0681783723bb472b6b8304e61ecfcb4c2b11cf7f243d923813c21ae5d2a"}, + {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:61bae4d345a26550d0ed9f2c9910ea060f89dbfc642b7b96e9510a95c3a33b3c"}, + {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90e5a84016d0d2fb828f770ede085b5d89155fcb9629b8a3237c960c41c120c3"}, + {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55dc289a064c04819d669e6e8a85a1c0416e6c601782093bdc749ae14a2f39da"}, + {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b79bcf97ca03c95b044532a4fef6e5ae106a2dd863875b75fde64c553e3f4820"}, + {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e5e7564d232a782baa3089b25a0d979e2e4d6572d3c7231fcceacc5c22bf0f7"}, + {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6fcd8d56880dccdd376afb18f483ab55a0e24036adc9a83c914d4b7bb5729d4e"}, + {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4fbce985c7fe7bafb4d9bf647c835dbe415b465a897b0c79d1bdf0f3fae5fe50"}, + {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3bd12d727cd616387d50fe283abebb2db93300c98f8ff1084b68460acd551926"}, + {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:38544cae535ed697960891131731b33bb865b7d197ad62dc380d2dbb1bceff48"}, + {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:47396898f98fae5c9b9bb409c3d2cf6106e409730f35a0926aad09dd7acf1ef5"}, + {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10d835f8ce8571fd555db42d3aef325af903535dad7e6faa7b9c8abe191bffc"}, + {file = "frozenlist-1.6.2-cp313-cp313-win32.whl", hash = "sha256:a400fe775a41b6d7a3fef00d88f10cbae4f0074c9804e282013d7797671ba58d"}, + {file = "frozenlist-1.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:cc8b25b321863ed46992558a29bb09b766c41e25f31461666d501be0f893bada"}, + {file = "frozenlist-1.6.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:56de277a0e0ad26a1dcdc99802b4f5becd7fd890807b68e3ecff8ced01d58132"}, + {file = "frozenlist-1.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9cb386dd69ae91be586aa15cb6f39a19b5f79ffc1511371eca8ff162721c4867"}, + {file = "frozenlist-1.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53835d8a6929c2f16e02616f8b727bd140ce8bf0aeddeafdb290a67c136ca8ad"}, + {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc49f2277e8173abf028d744f8b7d69fe8cc26bffc2de97d47a3b529599fbf50"}, + {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:65eb9e8a973161bdac5fa06ea6bd261057947adc4f47a7a6ef3d6db30c78c5b4"}, + {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:301eb2f898d863031f8c5a56c88a6c5d976ba11a4a08a1438b96ee3acb5aea80"}, + {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:207f717fd5e65fddb77d33361ab8fa939f6d89195f11307e073066886b33f2b8"}, + {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f83992722642ee0db0333b1dbf205b1a38f97d51a7382eb304ba414d8c3d1e05"}, + {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12af99e6023851b36578e5bcc60618b5b30f4650340e29e565cd1936326dbea7"}, + {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6f01620444a674eaad900a3263574418e99c49e2a5d6e5330753857363b5d59f"}, + {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:82b94c8948341512306ca8ccc702771600b442c6abe5f8ee017e00e452a209e8"}, + {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:324a4cf4c220ddb3db1f46ade01e48432c63fa8c26812c710006e7f6cfba4a08"}, + {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:695284e51458dabb89af7f7dc95c470aa51fd259207aba5378b187909297feef"}, + {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:9ccbeb1c8dda4f42d0678076aa5cbde941a232be71c67b9d8ca89fbaf395807c"}, + {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cbbdf62fcc1864912c592a1ec748fee94f294c6b23215d5e8e9569becb7723ee"}, + {file = "frozenlist-1.6.2-cp313-cp313t-win32.whl", hash = "sha256:76857098ee17258df1a61f934f2bae052b8542c9ea6b187684a737b2e3383a65"}, + {file = "frozenlist-1.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c06a88daba7e891add42f9278cdf7506a49bc04df9b1648be54da1bf1c79b4c6"}, + {file = "frozenlist-1.6.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99119fa5ae292ac1d3e73336ecbe3301dbb2a7f5b4e6a4594d3a6b2e240c31c1"}, + {file = "frozenlist-1.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af923dbcfd382554e960328133c2a8151706673d1280f55552b1bb914d276267"}, + {file = "frozenlist-1.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69e85175df4cc35f2cef8cb60a8bad6c5fc50e91524cd7018d73dd2fcbc70f5d"}, + {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97dcdffe18c0e35ce57b3d7c1352893a3608e7578b814abb3b2a3cc15907e682"}, + {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cc228faf4533327e5f1d153217ab598648a2cd5f6b1036d82e63034f079a5861"}, + {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ee53aba5d0768e2c5c6185ec56a94bab782ef002429f293497ec5c5a3b94bdf"}, + {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d3214738024afd53434614ee52aa74353a562414cd48b1771fa82fd982cb1edb"}, + {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5628e6a6f74ef1693adbe25c0bce312eb9aee82e58abe370d287794aff632d0f"}, + {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7678d3e32cb3884879f10c679804c08f768df55078436fb56668f3e13e2a5e"}, + {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b776ab5217e2bf99c84b2cbccf4d30407789c0653f72d1653b5f8af60403d28f"}, + {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:b1e162a99405cb62d338f747b8625d6bd7b6794383e193335668295fb89b75fb"}, + {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2de1ddeb9dd8a07383f6939996217f0f1b2ce07f6a01d74c9adb1db89999d006"}, + {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2dcabe4e7aac889d41316c1698df0eb2565ed233b66fab6bc4a5c5b7769cad4c"}, + {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:06e28cd2ac31797e12ec8c65aa462a89116323f045e8b1930127aba9486aab24"}, + {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:86f908b70043c3517f862247bdc621bd91420d40c3e90ede1701a75f025fcd5f"}, + {file = "frozenlist-1.6.2-cp39-cp39-win32.whl", hash = "sha256:2647a3d11f10014a5f9f2ca38c7fadd0dd28f5b1b5e9ce9c9d194aa5d0351c7e"}, + {file = "frozenlist-1.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:e2cbef30ba27a1d9f3e3c6aa84a60f53d907d955969cd0103b004056e28bca08"}, + {file = "frozenlist-1.6.2-py3-none-any.whl", hash = "sha256:947abfcc8c42a329bbda6df97a4b9c9cdb4e12c85153b3b57b9d2f02aa5877dc"}, + {file = "frozenlist-1.6.2.tar.gz", hash = "sha256:effc641518696471cf4962e8e32050133bc1f7b2851ae8fd0cb8797dd70dc202"}, +] + [[package]] name = "greenlet" version = "3.1.1" @@ -4141,4 +4410,4 @@ test = ["fakeredis", "pytest", "pytest-asyncio", "pytest-env", "pytest-mock", "p [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "705faca66bf1ccae73f82741ba3b53305705da34391a2d795f7befe772b2fce1" +content-hash = "7a7637e281585aefed156c07b7841b4369186178744b0496aface64473be98b6" diff --git a/pyproject.toml b/pyproject.toml index adffd5d5a..91f749eeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "PyYAML == 6.0.1", "SQLAlchemy[mariadb-connector,mysql-connector,postgresql-psycopg] ~= 2.0", "Unidecode == 1.3.8", + "aiohttp ~= 3.12", "alembic == 1.13.1", "anyio ~= 4.4", "authlib ~= 1.3", From 0abb41d9db69b3fd2c328b4b6615b5406e6a4246 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Mon, 9 Jun 2025 09:51:55 -0400 Subject: [PATCH 6/9] Use correct ID for genesis/metadrive --- backend/adapters/services/rahasher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/adapters/services/rahasher.py b/backend/adapters/services/rahasher.py index f913a0458..0b9c35eb0 100644 --- a/backend/adapters/services/rahasher.py +++ b/backend/adapters/services/rahasher.py @@ -37,7 +37,7 @@ PLATFORM_SLUG_TO_RETROACHIEVEMENTS_ID: dict[str, int] = { "gamecube": 16, "ngc": 14, "genesis": 1, - "genesis-slash-megadrive": 16, + "genesis-slash-megadrive": 1, "intellivision": 45, "jaguar": 17, "lynx": 13, From d18f85b943c35c3090ca7eb33930d157ea7e9050 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Mon, 9 Jun 2025 10:18:50 -0400 Subject: [PATCH 7/9] Fix and uupdate more rahasher IDs --- backend/adapters/services/rahasher.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/adapters/services/rahasher.py b/backend/adapters/services/rahasher.py index 0b9c35eb0..6b8f20dc5 100644 --- a/backend/adapters/services/rahasher.py +++ b/backend/adapters/services/rahasher.py @@ -26,6 +26,8 @@ PLATFORM_SLUG_TO_RETROACHIEVEMENTS_ID: dict[str, int] = { "colecovision": 44, "dreamcast": 40, "dc": 40, + "elektor": 75, + "fairchild-channel-f": 57, "gameboy": 4, "gb": 4, "gameboy-advance": 5, @@ -35,10 +37,11 @@ PLATFORM_SLUG_TO_RETROACHIEVEMENTS_ID: dict[str, int] = { "game-gear": 15, "gamegear": 15, "gamecube": 16, - "ngc": 14, + "ngc": 16, "genesis": 1, "genesis-slash-megadrive": 1, "intellivision": 45, + "interton-vc-4000": 74, "jaguar": 17, "lynx": 13, "msx": 29, @@ -53,6 +56,7 @@ PLATFORM_SLUG_TO_RETROACHIEVEMENTS_ID: dict[str, int] = { "nds": 18, "nintendo-dsi": 78, "odyssey-2": 23, + "win": 102, "pc-8000": 47, "pc-8800-series": 47, "pc-fx": 49, @@ -70,13 +74,16 @@ PLATFORM_SLUG_TO_RETROACHIEVEMENTS_ID: dict[str, int] = { "sms": 11, "sg-1000": 33, "snes": 3, + "sfam": 3, "turbografx-cd": 76, "turbografx-16-slash-pc-engine-cd": 76, "turbo-grafx": 8, "turbografx16--1": 8, - "vectrex": 26, + "uzebox": 80, + "vectrex": 46, "virtual-boy": 28, "virtualboy": 28, + "wasm-4": 72, "watara-slash-quickshot-supervision": 63, "wonderswan": 53, "wonderswan-color": 53, From 15ac30cdc0978ef55184e0f6efe3c476a0b514af Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Mon, 9 Jun 2025 11:30:14 -0400 Subject: [PATCH 8/9] Action bar content should always be white --- frontend/src/components/common/Game/Card/ActionBar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/Game/Card/ActionBar.vue b/frontend/src/components/common/Game/Card/ActionBar.vue index b5e3b88ae..f7fff5f9d 100644 --- a/frontend/src/components/common/Game/Card/ActionBar.vue +++ b/frontend/src/components/common/Game/Card/ActionBar.vue @@ -57,7 +57,7 @@ watch(menuOpen, (val) => {