Skip to content

Commit

Permalink
Add public user stats API
Browse files Browse the repository at this point in the history
  • Loading branch information
tsunyoku committed Jul 7, 2024
1 parent 20fd2a2 commit 7a5ae6f
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 2 deletions.
2 changes: 2 additions & 0 deletions app/api/public/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from . import authentication
from . import users
from . import user_stats

public_router = APIRouter()

public_router.include_router(authentication.router)
public_router.include_router(users.router)
public_router.include_router(user_stats.router)
46 changes: 46 additions & 0 deletions app/api/public/user_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from fastapi import APIRouter
from fastapi import Response
from fastapi import Query

from app.api.responses import JSONResponse
from app.errors import Error
from app.errors import ErrorCode
from app.common_types import GameMode
from app.common_types import RelaxMode
from app.common_types import Mode
from app.usecases import user_stats

router = APIRouter(tags=["(Public) User Stats API"])

def map_error_code_to_http_status_code(error_code: ErrorCode) -> int:
return _error_code_to_http_status_code_map[error_code]


_error_code_to_http_status_code_map: dict[ErrorCode, int] = {
ErrorCode.INCORRECT_CREDENTIALS: 401,
ErrorCode.INSUFFICIENT_PRIVILEGES: 401,
ErrorCode.PENDING_VERIFICATION: 401,
ErrorCode.NOT_FOUND: 404,
ErrorCode.INTERNAL_SERVER_ERROR: 500,
}

@router.get("/public/api/v1/users/{user_id}/stats")
async def get_user_stats(
user_id: int,
game_mode: GameMode = Query(...),
relax_mode: RelaxMode = Query(...),
) -> Response:
mode = Mode.from_game_mode_and_relax_mode(game_mode, relax_mode)

response = await user_stats.fetch_one_by_user_id_and_mode(user_id, mode)
if isinstance(response, Error):
return JSONResponse(
content=response.model_dump(),
status_code=map_error_code_to_http_status_code(response.error_code),
)

return JSONResponse(
content=response.model_dump(),
status_code=200,
)

36 changes: 34 additions & 2 deletions app/common_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from enum import IntEnum
from enum import IntFlag

from enum import IntEnum

class UserPrivileges(IntFlag):
USER_PUBLIC = 1 << 0
Expand Down Expand Up @@ -46,3 +45,36 @@ class GameMode(IntEnum):
TAIKO = 1
CATCH = 2
MANIA = 3


class RelaxMode(IntEnum):
VANILLA = 0
RELAX = 1
AUTOPILOT = 2


class Mode(IntEnum):
OSU = 0
TAIKO = 1
CATCH = 2
MANIA = 3

RELAX_OSU = 4
RELAX_TAIKO = 5
RELAX_CATCH = 6

AUTOPILOT_OSU = 8

@staticmethod
def from_game_mode_and_relax_mode(
game_mode: GameMode,
relax_mode: RelaxMode,
) -> "Mode":
if relax_mode is RelaxMode.VANILLA:
return Mode(game_mode.value)
elif relax_mode is RelaxMode.RELAX and game_mode is not GameMode.MANIA:
return Mode(game_mode.value + 4)
elif relax_mode is RelaxMode.AUTOPILOT and game_mode is GameMode.OSU:
return Mode.AUTOPILOT_OSU
else:
raise ValueError("Unknown game_mode and relax_mode combo")
20 changes: 20 additions & 0 deletions app/models/user_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pydantic import BaseModel

class UserStats(BaseModel):
ranked_score: int
total_score: int
play_count: int
replays_watched: int
total_hits: int
accuracy: float
pp: int
play_time: int
ssh_count: int
ss_count: int
sh_count: int
s_count: int
a_count: int
b_count: int
c_count: int
d_count: int
max_combo: int
67 changes: 67 additions & 0 deletions app/repositories/user_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from pydantic import BaseModel

from app.common_types import Mode

import app.state


class UserStats(BaseModel):
ranked_score: int
total_score: int
playcount: int
replays_watched: int
total_hits: int
avg_accuracy: float
pp: int
playtime: int
xh_count: int
x_count: int
sh_count: int
s_count: int
a_count: int
b_count: int
c_count: int
d_count: int
max_combo: int


READ_PARAMS = """\
ranked_score, total_score, playcount, replays_watched, total_hits, avg_accuracy, pp, playtime,
xh_count, x_count, sh_count, s_count, a_count, b_count, c_count, d_count, max_combo
"""

async def fetch_one_by_user_id_and_mode(
user_id: int,
mode: Mode,
) -> UserStats | None:
query = f"""
SELECT {READ_PARAMS}
FROM user_stats
WHERE user_id = :user_id
AND mode = :mode
"""
params = {"user_id": user_id, "mode": mode.value}

user_stats = await app.state.database.fetch_one(query, params)
if user_stats is None:
return None

return UserStats(
ranked_score=user_stats["ranked_score"],
total_score=user_stats["total_score"],
playcount=user_stats["playcount"],
replays_watched=user_stats["replays_watched"],
total_hits=user_stats["total_hits"],
avg_accuracy=user_stats["avg_accuracy"],
pp=user_stats["pp"],
playtime=user_stats["playtime"],
xh_count=user_stats["xh_count"],
x_count=user_stats["x_count"],
sh_count=user_stats["sh_count"],
s_count=user_stats["s_count"],
a_count=user_stats["a_count"],
b_count=user_stats["b_count"],
c_count=user_stats["c_count"],
d_count=user_stats["d_count"],
max_combo=user_stats["max_combo"],
)
33 changes: 33 additions & 0 deletions app/usecases/user_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from app.common_types import Mode
from app.models.user_stats import UserStats
from app.errors import Error
from app.errors import ErrorCode
from app.repositories import user_stats

async def fetch_one_by_user_id_and_mode(user_id: int, mode: Mode) -> UserStats | Error:
stats = await user_stats.fetch_one_by_user_id_and_mode(user_id, mode)
if stats is None:
return Error(
error_code=ErrorCode.NOT_FOUND,
user_feedback="User statistics not found.",
)

return UserStats(
ranked_score=stats.ranked_score,
total_score=stats.total_score,
play_count=stats.playcount,
replays_watched=stats.replays_watched,
total_hits=stats.total_hits,
accuracy=stats.avg_accuracy,
pp=stats.pp,
play_time=stats.playtime,
ssh_count=stats.xh_count,
ss_count=stats.x_count,
sh_count=stats.sh_count,
s_count=stats.s_count,
a_count=stats.a_count,
b_count=stats.b_count,
c_count=stats.c_count,
d_count=stats.d_count,
max_combo=stats.max_combo,
)

0 comments on commit 7a5ae6f

Please sign in to comment.