Merge pull request #5 from GooGuTeam/score-database-model
Score database model and /beatmap/score API endpoint
This commit is contained in:
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -3,7 +3,7 @@
|
|||||||
<component name="Black">
|
<component name="Black">
|
||||||
<option name="sdkName" value="osu_lazer_api" />
|
<option name="sdkName" value="osu_lazer_api" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="osu_lazer_api" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="uv (osu_lazer_api)" project-jdk-type="Python SDK" />
|
||||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||||
<option name="version" value="3" />
|
<option name="version" value="3" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
2
.idea/osu_lazer_api.iml
generated
2
.idea/osu_lazer_api.iml
generated
@@ -4,7 +4,7 @@
|
|||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="osu_lazer_api" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="uv (osu_lazer_api)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PyDocumentationSettings">
|
<component name="PyDocumentationSettings">
|
||||||
|
|||||||
110
app/database/score.py
Normal file
110
app/database/score.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# ruff: noqa: I002
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import math
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from app.models.score import Rank
|
||||||
|
|
||||||
|
from .beatmap import Beatmap, BeatmapResp
|
||||||
|
from .beatmapset import Beatmapset, BeatmapsetResp
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import Column, DateTime
|
||||||
|
from sqlmodel import BigInteger, Field, Relationship, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreBase(SQLModel):
|
||||||
|
# 基本字段
|
||||||
|
accuracy: float
|
||||||
|
map_md5: str = Field(max_length=32, index=True)
|
||||||
|
best_id: int | None = Field(default=None)
|
||||||
|
build_id: int | None = Field(default=None)
|
||||||
|
classic_total_score: int | None = Field(
|
||||||
|
default=0, sa_column=Column(BigInteger)
|
||||||
|
) # solo_score
|
||||||
|
ended_at: datetime = Field(sa_column=Column(DateTime))
|
||||||
|
has_replay: bool
|
||||||
|
max_combo: int
|
||||||
|
mods: int = Field(index=True)
|
||||||
|
passed: bool
|
||||||
|
playlist_item_id: int | None = Field(default=None) # multiplayer
|
||||||
|
pp: float
|
||||||
|
preserve: bool = Field(default=True)
|
||||||
|
rank: Rank
|
||||||
|
room_id: int | None = Field(default=None) # multiplayer
|
||||||
|
ruleset_id: Literal[0, 1, 2, 3] = Field(index=True)
|
||||||
|
started_at: datetime = Field(sa_column=Column(DateTime))
|
||||||
|
total_score: int = Field(default=0, sa_column=Column(BigInteger))
|
||||||
|
type: str
|
||||||
|
|
||||||
|
# optional
|
||||||
|
# TODO: current_user_attributes
|
||||||
|
position: int | None = Field(default=None) # multiplayer
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreStatistics(BaseModel):
|
||||||
|
count_miss: int
|
||||||
|
count_50: int
|
||||||
|
count_100: int
|
||||||
|
count_300: int
|
||||||
|
count_geki: int
|
||||||
|
count_katu: int
|
||||||
|
count_large_tick_miss: int | None = None
|
||||||
|
count_slider_tail_hit: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Score(ScoreBase, table=True):
|
||||||
|
__tablename__ = "scores" # pyright: ignore[reportAssignmentType]
|
||||||
|
id: int = Field(primary_key=True)
|
||||||
|
beatmap_id: int = Field(index=True, foreign_key="beatmap.id")
|
||||||
|
user_id: int = Field(foreign_key="user.id", index=True)
|
||||||
|
# ScoreStatistics
|
||||||
|
n300: int = Field(exclude=True)
|
||||||
|
n100: int = Field(exclude=True)
|
||||||
|
n50: int = Field(exclude=True)
|
||||||
|
nmiss: int = Field(exclude=True)
|
||||||
|
ngeki: int = Field(exclude=True)
|
||||||
|
nkatu: int = Field(exclude=True)
|
||||||
|
nlarge_tick_miss: int | None = Field(default=None, exclude=True)
|
||||||
|
nslider_tail_hit: int | None = Field(default=None, exclude=True)
|
||||||
|
|
||||||
|
# optional
|
||||||
|
beatmap: "Beatmap" = Relationship(back_populates="scores")
|
||||||
|
beatmapset: "Beatmapset" = Relationship(back_populates="scores")
|
||||||
|
# FIXME: user: "User" = Relationship(back_populates="scores")
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreResp(ScoreBase):
|
||||||
|
id: int
|
||||||
|
is_perfect_combo: bool = False
|
||||||
|
legacy_perfect: bool = False
|
||||||
|
legacy_total_score: int = 0 # FIXME
|
||||||
|
processed: bool = False # solo_score
|
||||||
|
weight: float = 0.0
|
||||||
|
beatmap: BeatmapResp | None = None
|
||||||
|
beatmapset: BeatmapsetResp | None = None
|
||||||
|
# FIXME: user: APIUser | None = None
|
||||||
|
statistics: ScoreStatistics | None = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_db(cls, score: Score) -> "ScoreResp":
|
||||||
|
s = cls.model_validate(score)
|
||||||
|
s.beatmap = BeatmapResp.from_db(score.beatmap)
|
||||||
|
s.beatmapset = BeatmapsetResp.from_db(score.beatmap.beatmapset)
|
||||||
|
s.is_perfect_combo = s.max_combo == s.beatmap.max_combo
|
||||||
|
s.legacy_perfect = s.max_combo == s.beatmap.max_combo
|
||||||
|
if score.best_id:
|
||||||
|
# https://osu.ppy.sh/wiki/Performance_points/Weighting_system
|
||||||
|
s.weight = math.pow(0.95, score.best_id)
|
||||||
|
s.statistics = ScoreStatistics(
|
||||||
|
count_miss=score.nmiss,
|
||||||
|
count_50=score.n50,
|
||||||
|
count_100=score.n100,
|
||||||
|
count_300=score.n300,
|
||||||
|
count_geki=score.ngeki,
|
||||||
|
count_katu=score.nkatu,
|
||||||
|
count_large_tick_miss=score.nlarge_tick_miss,
|
||||||
|
count_slider_tail_hit=score.nslider_tail_hit,
|
||||||
|
)
|
||||||
|
return s
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
# ruff: noqa: I002
|
# ruff: noqa: I002
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -95,7 +97,6 @@ class User(SQLModel, table=True):
|
|||||||
back_populates="user"
|
back_populates="user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Lazer API 专用表模型
|
# Lazer API 专用表模型
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -19,6 +19,19 @@ MODE_TO_INT = {
|
|||||||
GameMode.FRUITS: 2,
|
GameMode.FRUITS: 2,
|
||||||
GameMode.MANIA: 3,
|
GameMode.MANIA: 3,
|
||||||
}
|
}
|
||||||
|
INT_TO_MODE = {v: k for k, v in MODE_TO_INT.items()}
|
||||||
|
|
||||||
|
|
||||||
|
class Rank(str, Enum):
|
||||||
|
X = "ss"
|
||||||
|
XH = "ssh"
|
||||||
|
S = "s"
|
||||||
|
SH = "sh"
|
||||||
|
A = "a"
|
||||||
|
B = "b"
|
||||||
|
C = "c"
|
||||||
|
D = "d"
|
||||||
|
F = "f"
|
||||||
|
|
||||||
|
|
||||||
class APIMod(BaseModel):
|
class APIMod(BaseModel):
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
fastapi==0.104.1
|
fastapi~=0.116.1
|
||||||
uvicorn[standard]==0.24.0
|
uvicorn[standard]==0.24.0
|
||||||
sqlalchemy==2.0.23
|
sqlalchemy~=2.0.41
|
||||||
alembic==1.12.1
|
alembic==1.12.1
|
||||||
|
pymysql~=1.1.1
|
||||||
cryptography==41.0.7
|
cryptography==41.0.7
|
||||||
redis==5.0.1
|
redis~=6.2.0
|
||||||
python-jose[cryptography]==3.3.0
|
python-jose[cryptography]~=3.5.0
|
||||||
passlib[bcrypt]==1.7.4
|
passlib[bcrypt]==1.7.4
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
pydantic[email]==2.5.0
|
pydantic[email]~=2.11.7
|
||||||
python-dotenv==1.0.0
|
python-dotenv~=1.1.1
|
||||||
bcrypt==4.1.2
|
bcrypt~=4.3.0
|
||||||
|
msgpack~=1.1.1
|
||||||
|
sqlmodel~=0.0.24
|
||||||
|
starlette~=0.47.2
|
||||||
aiomysql==0.2.0
|
aiomysql==0.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user