From e93904bb0d2b7bf6374a7c6ae6e2c1de0b43de3e Mon Sep 17 00:00:00 2001 From: 283375 Date: Tue, 1 Oct 2024 16:39:46 +0800 Subject: [PATCH] refactor: AndrealImageGenerator api data exporter --- .../external/andreal/__init__.py | 3 - .../external/andreal/account.py | 14 -- .../external/andreal/api_data.py | 98 ---------- .../external/exporters/andreal/__init__.py | 3 + .../external/exporters/andreal/api_data.py | 172 ++++++++++++++++++ .../external/exporters/andreal/definitions.py | 38 ++++ 6 files changed, 213 insertions(+), 115 deletions(-) delete mode 100644 src/arcaea_offline/external/andreal/__init__.py delete mode 100644 src/arcaea_offline/external/andreal/account.py delete mode 100644 src/arcaea_offline/external/andreal/api_data.py create mode 100644 src/arcaea_offline/external/exporters/andreal/__init__.py create mode 100644 src/arcaea_offline/external/exporters/andreal/api_data.py create mode 100644 src/arcaea_offline/external/exporters/andreal/definitions.py diff --git a/src/arcaea_offline/external/andreal/__init__.py b/src/arcaea_offline/external/andreal/__init__.py deleted file mode 100644 index ac1a0c0..0000000 --- a/src/arcaea_offline/external/andreal/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .api_data import AndrealImageGeneratorApiDataConverter - -__all__ = ["AndrealImageGeneratorApiDataConverter"] diff --git a/src/arcaea_offline/external/andreal/account.py b/src/arcaea_offline/external/andreal/account.py deleted file mode 100644 index 3d84a69..0000000 --- a/src/arcaea_offline/external/andreal/account.py +++ /dev/null @@ -1,14 +0,0 @@ -class AndrealImageGeneratorAccount: - def __init__( - self, - name: str = "Player", - code: int = 123456789, - rating: int = -1, - character: int = 5, - character_uncapped: bool = False, - ): - self.name = name - self.code = code - self.rating = rating - self.character = character - self.character_uncapped = character_uncapped diff --git a/src/arcaea_offline/external/andreal/api_data.py b/src/arcaea_offline/external/andreal/api_data.py deleted file mode 100644 index 272e399..0000000 --- a/src/arcaea_offline/external/andreal/api_data.py +++ /dev/null @@ -1,98 +0,0 @@ -from typing import Optional, Union - -from sqlalchemy import select -from sqlalchemy.orm import Session - -from ...models import CalculatedPotential, ScoreBest, ScoreCalculated -from .account import AndrealImageGeneratorAccount - - -class AndrealImageGeneratorApiDataConverter: - def __init__( - self, - session: Session, - account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(), - ): - self.session = session - self.account = account - - def account_info(self): - return { - "code": self.account.code, - "name": self.account.name, - "is_char_uncapped": self.account.character_uncapped, - "rating": self.account.rating, - "character": self.account.character, - } - - def score(self, score: Union[ScoreCalculated, ScoreBest]): - return { - "score": score.score, - "health": 75, - "rating": score.potential, - "song_id": score.song_id, - "modifier": score.modifier or 0, - "difficulty": score.rating_class, - "clear_type": score.clear_type or 1, - "best_clear_type": score.clear_type or 1, - "time_played": score.date * 1000 if score.date else 0, - "near_count": score.far, - "miss_count": score.lost, - "perfect_count": score.pure, - "shiny_perfect_count": score.shiny_pure, - } - - def user_info(self, score: Optional[ScoreCalculated] = None): - if not score: - score = self.session.scalar( - select(ScoreCalculated).order_by(ScoreCalculated.date.desc()).limit(1) - ) - if not score: - raise ValueError("No score available.") - - return { - "content": { - "account_info": self.account_info(), - "recent_score": [self.score(score)], - } - } - - def user_best(self, song_id: str, rating_class: int): - score = self.session.scalar( - select(ScoreBest).where( - (ScoreBest.song_id == song_id) - & (ScoreBest.rating_class == rating_class) - ) - ) - if not score: - raise ValueError("No score available.") - - return { - "content": { - "account_info": self.account_info(), - "record": self.score(score), - } - } - - def user_best30(self): - scores = list( - self.session.scalars( - select(ScoreBest).order_by(ScoreBest.potential.desc()).limit(40) - ) - ) - if not scores: - raise ValueError("No score available.") - best30_avg = self.session.scalar(select(CalculatedPotential.b30)) - - best30_overflow = ( - [self.score(score) for score in scores[30:40]] if len(scores) > 30 else [] - ) - - return { - "content": { - "account_info": self.account_info(), - "best30_avg": best30_avg, - "best30_list": [self.score(score) for score in scores[:30]], - "best30_overflow": best30_overflow, - } - } diff --git a/src/arcaea_offline/external/exporters/andreal/__init__.py b/src/arcaea_offline/external/exporters/andreal/__init__.py new file mode 100644 index 0000000..4991c41 --- /dev/null +++ b/src/arcaea_offline/external/exporters/andreal/__init__.py @@ -0,0 +1,3 @@ +from .api_data import AndrealImageGeneratorApiDataExporter + +__all__ = ["AndrealImageGeneratorApiDataExporter"] diff --git a/src/arcaea_offline/external/exporters/andreal/api_data.py b/src/arcaea_offline/external/exporters/andreal/api_data.py new file mode 100644 index 0000000..d0f401a --- /dev/null +++ b/src/arcaea_offline/external/exporters/andreal/api_data.py @@ -0,0 +1,172 @@ +import statistics +from dataclasses import dataclass +from typing import List, Optional, Union + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from arcaea_offline.constants.enums.arcaea import ArcaeaRatingClass +from arcaea_offline.database.models.v5 import ( + PlayResultBest, + PlayResultCalculated, +) + +from .definitions import ( + AndrealImageGeneratorApiDataAccountInfo, + AndrealImageGeneratorApiDataRoot, + AndrealImageGeneratorApiDataScoreItem, +) + + +@dataclass +class AndrealImageGeneratorAccount: + name: str = "Player" + code: int = 123456789 + rating: int = -1 + character: int = 5 + character_uncapped: bool = False + + +class AndrealImageGeneratorApiDataExporter: + @staticmethod + def craft_account_info( + account: AndrealImageGeneratorAccount, + ) -> AndrealImageGeneratorApiDataAccountInfo: + return { + "code": account.code, + "name": account.name, + "is_char_uncapped": account.character_uncapped, + "rating": account.rating, + "character": account.character, + } + + @staticmethod + def craft_score_item( + play_result: Union[PlayResultCalculated, PlayResultBest], + ) -> AndrealImageGeneratorApiDataScoreItem: + modifier = play_result.modifier.value if play_result.modifier else 0 + clear_type = play_result.clear_type.value if play_result.clear_type else 0 + + return { + "score": play_result.score, + "health": 75, + "rating": play_result.potential, + "song_id": play_result.song_id, + "modifier": modifier, + "difficulty": play_result.rating_class.value, + "clear_type": clear_type, + "best_clear_type": clear_type, + "time_played": int(play_result.date.timestamp() * 1000) + if play_result.date + else 0, + "near_count": play_result.far, + "miss_count": play_result.lost, + "perfect_count": play_result.pure, + "shiny_perfect_count": play_result.shiny_pure, + } + + @classmethod + def user_info( + cls, + play_result_calculated: PlayResultCalculated, + account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(), + ) -> AndrealImageGeneratorApiDataRoot: + return { + "content": { + "account_info": cls.craft_account_info(account), + "recent_score": [cls.craft_score_item(play_result_calculated)], + } + } + + @classmethod + def user_best( + cls, + play_result_best: PlayResultBest, + account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(), + ) -> AndrealImageGeneratorApiDataRoot: + return { + "content": { + "account_info": cls.craft_account_info(account), + "record": cls.craft_score_item(play_result_best), + } + } + + @classmethod + def user_best30( + cls, + play_results_best: List[PlayResultBest], + account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(), + ) -> AndrealImageGeneratorApiDataRoot: + play_results_best_sorted = sorted( + play_results_best, key=lambda it: it.potential, reverse=True + ) + + best30_list = play_results_best_sorted[:30] + best30_overflow = play_results_best_sorted[30:] + + best30_avg = statistics.fmean([it.potential for it in best30_list]) + + return { + "content": { + "account_info": cls.craft_account_info(account), + "best30_avg": best30_avg, + "best30_list": [cls.craft_score_item(it) for it in best30_list], + "best30_overflow": [cls.craft_score_item(it) for it in best30_overflow], + } + } + + @classmethod + def craft_user_info( + cls, + session: Session, + account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(), + ) -> Optional[AndrealImageGeneratorApiDataRoot]: + play_result_calculated = session.scalar( + select(PlayResultCalculated) + .order_by(PlayResultCalculated.date.desc()) + .limit(1) + ) + + if play_result_calculated is None: + return None + + return cls.user_info(play_result_calculated, account) + + @classmethod + def craft_user_best( + cls, + session: Session, + account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(), + *, + song_id: str, + rating_class: ArcaeaRatingClass, + ): + play_result_best = session.scalar( + select(PlayResultBest).where( + (PlayResultBest.song_id == song_id) + & (PlayResultBest.rating_class == rating_class) + ) + ) + + if play_result_best is None: + return None + + return cls.user_best(play_result_best, account) + + @classmethod + def craft( + cls, + session: Session, + account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(), + *, + limit: int = 40, + ) -> Optional[AndrealImageGeneratorApiDataRoot]: + play_results_best = list( + session.scalars( + select(PlayResultBest) + .order_by(PlayResultBest.potential.desc()) + .limit(limit) + ).all() + ) + + return cls.user_best30(play_results_best, account) diff --git a/src/arcaea_offline/external/exporters/andreal/definitions.py b/src/arcaea_offline/external/exporters/andreal/definitions.py new file mode 100644 index 0000000..a4e2058 --- /dev/null +++ b/src/arcaea_offline/external/exporters/andreal/definitions.py @@ -0,0 +1,38 @@ +from typing import List, Optional, TypedDict + + +class AndrealImageGeneratorApiDataAccountInfo(TypedDict): + name: str + code: int + rating: int + character: int + is_char_uncapped: bool + + +class AndrealImageGeneratorApiDataScoreItem(TypedDict): + score: int + health: int + rating: float + song_id: str + modifier: int + difficulty: int + clear_type: int + best_clear_type: int + time_played: int + near_count: Optional[int] + miss_count: Optional[int] + perfect_count: Optional[int] + shiny_perfect_count: Optional[int] + + +class AndrealImageGeneratorApiDataContent(TypedDict, total=False): + account_info: AndrealImageGeneratorApiDataAccountInfo + recent_score: List[AndrealImageGeneratorApiDataScoreItem] + record: AndrealImageGeneratorApiDataScoreItem + best30_avg: float + best30_list: List[AndrealImageGeneratorApiDataScoreItem] + best30_overflow: List[AndrealImageGeneratorApiDataScoreItem] + + +class AndrealImageGeneratorApiDataRoot(TypedDict): + content: AndrealImageGeneratorApiDataContent