feat(api): 支持 x-api-version (#29)
* feat(relationship): support legacy-compatible response format * feat(score): add support for legacy score response format in API * fix(score): avoid missing greenlet * fix(score): fix missing field for model validation * feat(user): apply legacy score format for user * feat(api): use `int` to hint `APIVersion`
This commit is contained in:
@@ -47,7 +47,7 @@ from .relationship import (
|
||||
)
|
||||
from .score_token import ScoreToken
|
||||
|
||||
from pydantic import field_serializer, field_validator
|
||||
from pydantic import BaseModel, field_serializer, field_validator
|
||||
from redis.asyncio import Redis
|
||||
from sqlalchemy import Boolean, Column, DateTime, TextClause
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||
@@ -194,6 +194,11 @@ class Score(ScoreBase, table=True):
|
||||
def is_perfect_combo(self) -> bool:
|
||||
return self.max_combo == self.beatmap.max_combo
|
||||
|
||||
async def to_resp(self, session: AsyncSession, api_version: int) -> "ScoreResp | LegacyScoreResp":
|
||||
if api_version >= 20220705:
|
||||
return await ScoreResp.from_db(session, self)
|
||||
return await LegacyScoreResp.from_db(session, self)
|
||||
|
||||
|
||||
class ScoreResp(ScoreBase):
|
||||
id: int
|
||||
@@ -334,6 +339,90 @@ class ScoreResp(ScoreBase):
|
||||
return s
|
||||
|
||||
|
||||
class LegacyStatistics(BaseModel):
|
||||
count_300: int
|
||||
count_100: int
|
||||
count_50: int
|
||||
count_miss: int
|
||||
count_geki: int | None = None
|
||||
count_katu: int | None = None
|
||||
|
||||
|
||||
class LegacyScoreResp(UTCBaseModel):
|
||||
accuracy: float
|
||||
best_id: int
|
||||
created_at: datetime
|
||||
id: int
|
||||
max_combo: int
|
||||
mode: GameMode
|
||||
mode_int: int
|
||||
mods: list[str] # acronym
|
||||
passed: bool
|
||||
perfect: bool = False
|
||||
pp: float
|
||||
rank: Rank
|
||||
replay: bool
|
||||
score: int
|
||||
statistics: LegacyStatistics
|
||||
type: str
|
||||
user_id: int
|
||||
current_user_attributes: CurrentUserAttributes
|
||||
user: UserResp
|
||||
beatmap: BeatmapResp
|
||||
rank_global: int | None = Field(default=None, exclude=True)
|
||||
|
||||
@classmethod
|
||||
async def from_db(cls, session: AsyncSession, score: Score) -> "LegacyScoreResp":
|
||||
await session.refresh(score)
|
||||
await score.awaitable_attrs.beatmap
|
||||
return cls(
|
||||
accuracy=score.accuracy,
|
||||
best_id=await get_best_id(session, score.id) or 0,
|
||||
created_at=score.started_at,
|
||||
id=score.id,
|
||||
max_combo=score.max_combo,
|
||||
mode=score.gamemode,
|
||||
mode_int=int(score.gamemode),
|
||||
mods=[m["acronym"] for m in score.mods],
|
||||
passed=score.passed,
|
||||
pp=score.pp,
|
||||
rank=score.rank,
|
||||
replay=score.has_replay,
|
||||
score=score.total_score,
|
||||
statistics=LegacyStatistics(
|
||||
count_300=score.n300,
|
||||
count_100=score.n100,
|
||||
count_50=score.n50,
|
||||
count_miss=score.nmiss,
|
||||
count_geki=score.ngeki or 0,
|
||||
count_katu=score.nkatu or 0,
|
||||
),
|
||||
type=score.type,
|
||||
user_id=score.user_id,
|
||||
current_user_attributes=CurrentUserAttributes(
|
||||
pin=PinAttributes(is_pinned=bool(score.pinned_order), score_id=score.id)
|
||||
),
|
||||
user=await UserResp.from_db(
|
||||
score.user,
|
||||
session,
|
||||
include=["statistics", "team", "daily_challenge_user_stats"],
|
||||
ruleset=score.gamemode,
|
||||
),
|
||||
beatmap=await BeatmapResp.from_db(score.beatmap),
|
||||
perfect=score.is_perfect_combo,
|
||||
rank_global=(
|
||||
await get_score_position_by_id(
|
||||
session,
|
||||
score.beatmap_id,
|
||||
score.id,
|
||||
mode=score.gamemode,
|
||||
user=score.user,
|
||||
)
|
||||
or None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class MultiplayerScores(RespWithCursor):
|
||||
scores: list[ScoreResp] = Field(default_factory=list)
|
||||
params: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
Reference in New Issue
Block a user