chore(merge): merge branch 'main' into feat/multiplayer-api

This commit is contained in:
MingxuanGame
2025-07-30 06:34:29 +00:00
34 changed files with 1971 additions and 1017 deletions

View File

@@ -1,56 +1,71 @@
{
"name": "OSU Lazer API",
"dockerComposeFile": "docker-compose.yml",
"service": "devcontainer",
"shutdownAction": "stopCompose",
"workspaceFolder": "/workspaces/osu_lazer_api",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"detachhead.basedpyright",
"charliermarsh.ruff",
"ms-python.debugpy",
"ms-vscode.vscode-json",
"redhat.vscode-yaml",
"ms-vscode.docker"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.terminal.activateEnvironment": true,
"python.linting.enabled": true,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": false,
"python.formatting.provider": "none",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"ruff.enable": true,
"ruff.lint.enable": true,
"ruff.format.enable": true,
"ruff.importStrategy": "fromEnvironment",
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true
},
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"python.testing.pytestArgs": [
"."
],
"terminal.integrated.defaultProfile.linux": "bash"
}
}
},
"features": {
"ghcr.io/va-h/devcontainers-features/uv:1": {}
},
"forwardPorts": [8000, 3306, 6379],
"postCreateCommand": "uv sync --dev && uv run pre-commit install",
"remoteUser": "vscode"
}
{
"name": "OSU Lazer API",
"dockerComposeFile": "docker-compose.yml",
"service": "devcontainer",
"shutdownAction": "stopCompose",
"workspaceFolder": "/workspaces/osu_lazer_api",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"detachhead.basedpyright",
"charliermarsh.ruff",
"ms-python.debugpy",
"ms-vscode.vscode-json",
"redhat.vscode-yaml",
"ms-vscode.docker",
"rust-lang.rust-analyzer"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.terminal.activateEnvironment": true,
"python.linting.enabled": true,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": false,
"python.formatting.provider": "none",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"ruff.enable": true,
"ruff.lint.enable": true,
"ruff.format.enable": true,
"ruff.importStrategy": "fromEnvironment",
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true
},
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"python.testing.pytestArgs": [
"."
],
"terminal.integrated.defaultProfile.linux": "bash",
"rust-analyzer.server.path": "rust-analyzer",
"rust-analyzer.cargo.buildScripts.enable": true,
"rust-analyzer.procMacro.enable": true,
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true
}
}
}
},
"features": {
"ghcr.io/va-h/devcontainers-features/uv:1": {},
"ghcr.io/devcontainers/features/rust:1": {
"version": "latest",
"profile": "default"
}
},
"forwardPorts": [
8000,
3306,
6379
],
"postCreateCommand": "uv sync --dev && uv run pre-commit install && cd packages/msgpack_lazer_api && cargo check",
"remoteUser": "vscode"
}

104
app/calculator.py Normal file
View File

@@ -0,0 +1,104 @@
from __future__ import annotations
import math
from typing import TYPE_CHECKING
from app.models.beatmap import BeatmapAttributes
from app.models.mods import APIMod
from app.models.score import GameMode
import rosu_pp_py as rosu
if TYPE_CHECKING:
from app.database.score import Score
def clamp[T: int | float](n: T, min_value: T, max_value: T) -> T:
if n < min_value:
return min_value
elif n > max_value:
return max_value
else:
return n
def calculate_beatmap_attribute(
beatmap: str,
gamemode: GameMode | None = None,
mods: int | list[APIMod] | list[str] = 0,
) -> BeatmapAttributes:
map = rosu.Beatmap(content=beatmap)
if gamemode is not None:
map.convert(gamemode.to_rosu(), mods) # pyright: ignore[reportArgumentType]
diff = rosu.Difficulty(mods=mods).calculate(map)
return BeatmapAttributes(
star_rating=diff.stars,
max_combo=diff.max_combo,
aim_difficulty=diff.aim,
aim_difficult_slider_count=diff.aim_difficult_slider_count,
speed_difficulty=diff.speed,
speed_note_count=diff.speed_note_count,
slider_factor=diff.slider_factor,
aim_difficult_strain_count=diff.aim_difficult_strain_count,
speed_difficult_strain_count=diff.speed_difficult_strain_count,
mono_stamina_factor=diff.stamina,
)
def calculate_pp(
score: "Score",
beatmap: str,
) -> float:
map = rosu.Beatmap(content=beatmap)
map.convert(score.gamemode.to_rosu(), score.mods) # pyright: ignore[reportArgumentType]
if map.is_suspicious():
return 0.0
perf = rosu.Performance(
mods=score.mods,
lazer=True,
accuracy=score.accuracy,
combo=score.max_combo,
large_tick_hits=score.nlarge_tick_hit or 0,
slider_end_hits=score.nslider_tail_hit or 0,
small_tick_hits=score.nsmall_tick_hit or 0,
n_geki=score.ngeki,
n_katu=score.nkatu,
n300=score.n300,
n100=score.n100,
n50=score.n50,
misses=score.nmiss,
hitresult_priority=rosu.HitResultPriority.Fastest,
)
attrs = perf.calculate(map)
return attrs.pp
# https://osu.ppy.sh/wiki/Gameplay/Score/Total_score
def calculate_level_to_score(level: int) -> float:
if level <= 100:
# 55 = 4^3 - 3^2
return 5000 / 3 * (55 - level) + 1.25 * math.pow(1.8, level - 60)
else:
return 26_931_190_827 + 99_999_999_999 * (level - 100)
def calculate_score_to_level(score: float) -> int:
if score < 5000:
return int(55 - (3 * score / 5000)) # 55 = 4^3 - 3^2
elif score < 26_931_190_827:
return int(60 + math.log(score / 1.25, 1.8))
else:
return int((score - 26_931_190_827) / 99_999_999_999 + 100)
# https://osu.ppy.sh/wiki/Performance_points/Weighting_system
def calculate_pp_weight(index: int) -> float:
return math.pow(0.95, index)
def calculate_weighted_pp(pp: float, index: int) -> float:
return calculate_pp_weight(index) * pp if pp > 0 else 0.0
def calculate_weighted_acc(acc: float, index: int) -> float:
return calculate_pp_weight(index) * acc if acc > 0 else 0.0

View File

@@ -7,6 +7,7 @@ from .beatmapset import (
Beatmapset as Beatmapset,
BeatmapsetResp as BeatmapsetResp,
)
from .best_score import BestScore
from .legacy import LegacyOAuthToken, LegacyUserStatistics
from .relationship import Relationship, RelationshipResp, RelationshipType
from .score import (
@@ -44,6 +45,7 @@ __all__ = [
"BeatmapResp",
"Beatmapset",
"BeatmapsetResp",
"BestScore",
"DailyChallengeStats",
"LazerUserAchievement",
"LazerUserBadge",

View File

@@ -0,0 +1,41 @@
from typing import TYPE_CHECKING
from app.models.score import GameMode
from .user import User
from sqlmodel import (
BigInteger,
Column,
Field,
Float,
ForeignKey,
Relationship,
SQLModel,
)
if TYPE_CHECKING:
from .beatmap import Beatmap
from .score import Score
class BestScore(SQLModel, table=True):
__tablename__ = "best_scores" # pyright: ignore[reportAssignmentType]
user_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("users.id"), index=True)
)
score_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("scores.id"), primary_key=True)
)
beatmap_id: int = Field(foreign_key="beatmaps.id", index=True)
gamemode: GameMode = Field(index=True)
pp: float = Field(
sa_column=Column(Float, default=0),
)
acc: float = Field(
sa_column=Column(Float, default=0),
)
user: User = Relationship()
score: "Score" = Relationship()
beatmap: "Beatmap" = Relationship()

View File

@@ -1,6 +1,8 @@
from enum import Enum
from .user import User
from app.models.user import User as APIUser
from .user import User as DBUser
from pydantic import BaseModel
from sqlmodel import (
@@ -41,14 +43,14 @@ class Relationship(SQLModel, table=True):
),
)
type: RelationshipType = Field(default=RelationshipType.FOLLOW, nullable=False)
target: "User" = SQLRelationship(
target: DBUser = SQLRelationship(
sa_relationship_kwargs={"foreign_keys": "[Relationship.target_id]"}
)
class RelationshipResp(BaseModel):
target_id: int
# FIXME: target: User
target: APIUser
mutual: bool = False
type: RelationshipType
@@ -56,6 +58,8 @@ class RelationshipResp(BaseModel):
async def from_db(
cls, session: AsyncSession, relationship: Relationship
) -> "RelationshipResp":
from app.utils import convert_db_user_to_api_user
target_relationship = (
await session.exec(
select(Relationship).where(
@@ -71,7 +75,7 @@ class RelationshipResp(BaseModel):
)
return cls(
target_id=relationship.target_id,
# target=relationship.target,
target=await convert_db_user_to_api_user(relationship.target),
mutual=mutual,
type=relationship.type,
)

View File

@@ -1,21 +1,38 @@
from datetime import datetime
import asyncio
from collections.abc import Sequence
from datetime import UTC, datetime
import math
from typing import TYPE_CHECKING
from app.database.user import User
from app.calculator import (
calculate_pp,
calculate_pp_weight,
calculate_score_to_level,
calculate_weighted_acc,
calculate_weighted_pp,
clamp,
)
from app.database.score_token import ScoreToken
from app.database.user import LazerUserStatistics, User
from app.models.beatmap import BeatmapRankStatus
from app.models.mods import APIMod
from app.models.mods import APIMod, mods_can_get_pp
from app.models.score import (
INT_TO_MODE,
MODE_TO_INT,
GameMode,
HitResult,
LeaderboardType,
Rank,
ScoreStatistics,
SoloScoreSubmissionInfo,
)
from app.models.user import User as APIUser
from .beatmap import Beatmap, BeatmapResp
from .beatmapset import Beatmapset, BeatmapsetResp
from .best_score import BestScore
from redis import Redis
from sqlalchemy import Column, ColumnExpressionArgument, DateTime
from sqlalchemy.orm import aliased, joinedload
from sqlmodel import (
@@ -33,12 +50,14 @@ from sqlmodel import (
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlmodel.sql._expression_select_cls import SelectOfScalar
if TYPE_CHECKING:
from app.fetcher import Fetcher
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)
@@ -49,7 +68,7 @@ class ScoreBase(SQLModel):
mods: list[APIMod] = Field(sa_column=Column(JSON))
passed: bool
playlist_item_id: int | None = Field(default=None) # multiplayer
pp: float
pp: float = Field(default=0.0)
preserve: bool = Field(default=True)
rank: Rank
room_id: int | None = Field(default=None) # multiplayer
@@ -87,7 +106,9 @@ class Score(ScoreBase, table=True):
ngeki: int = Field(exclude=True)
nkatu: int = Field(exclude=True)
nlarge_tick_miss: int | None = Field(default=None, exclude=True)
nlarge_tick_hit: int | None = Field(default=None, exclude=True)
nslider_tail_hit: int | None = Field(default=None, exclude=True)
nsmall_tick_hit: int | None = Field(default=None, exclude=True)
gamemode: GameMode = Field(index=True)
# optional
@@ -99,15 +120,19 @@ class Score(ScoreBase, table=True):
return self.max_combo == self.beatmap.max_combo
@staticmethod
def select_clause() -> SelectOfScalar["Score"]:
return select(Score).options(
def select_clause(with_user: bool = True) -> SelectOfScalar["Score"]:
clause = select(Score).options(
joinedload(Score.beatmap) # pyright: ignore[reportArgumentType]
.joinedload(Beatmap.beatmapset) # pyright: ignore[reportArgumentType]
.selectinload(
Beatmapset.beatmaps # pyright: ignore[reportArgumentType]
),
joinedload(Score.user).joinedload(User.lazer_profile), # pyright: ignore[reportArgumentType]
)
if with_user:
return clause.options(
joinedload(Score.user).options(*User.all_select_option()) # pyright: ignore[reportArgumentType]
)
return clause
@staticmethod
def select_clause_unique(
@@ -131,7 +156,7 @@ class Score(ScoreBase, table=True):
.selectinload(
Beatmapset.beatmaps # pyright: ignore[reportArgumentType]
),
joinedload(best.user).joinedload(User.lazer_profile), # pyright: ignore[reportArgumentType]
joinedload(best.user).options(*User.all_select_option()), # pyright: ignore[reportArgumentType]
)
)
@@ -144,16 +169,22 @@ class ScoreResp(ScoreBase):
legacy_total_score: int = 0 # FIXME
processed: bool = False # solo_score
weight: float = 0.0
best_id: int | None = None
ruleset_id: int | None = None
beatmap: BeatmapResp | None = None
beatmapset: BeatmapsetResp | None = None
# FIXME: user: APIUser | None = None
user: APIUser | None = None
statistics: ScoreStatistics | None = None
maximum_statistics: ScoreStatistics | None = None
rank_global: int | None = None
rank_country: int | None = None
@classmethod
async def from_db(cls, session: AsyncSession, score: Score) -> "ScoreResp":
async def from_db(
cls, session: AsyncSession, score: Score, user: User | None = None
) -> "ScoreResp":
from app.utils import convert_db_user_to_api_user
s = cls.model_validate(score.model_dump())
assert score.id
s.beatmap = BeatmapResp.from_db(score.beatmap)
@@ -161,9 +192,10 @@ class ScoreResp(ScoreBase):
s.is_perfect_combo = s.max_combo == s.beatmap.max_combo
s.legacy_perfect = s.max_combo == s.beatmap.max_combo
s.ruleset_id = MODE_TO_INT[score.gamemode]
if score.best_id:
# https://osu.ppy.sh/wiki/Performance_points/Weighting_system
s.weight = math.pow(0.95, score.best_id)
best_id = await get_best_id(session, score.id)
if best_id:
s.best_id = best_id
s.weight = calculate_pp_weight(best_id - 1)
s.statistics = {
HitResult.MISS: score.nmiss,
HitResult.MEH: score.n50,
@@ -176,14 +208,27 @@ class ScoreResp(ScoreBase):
s.statistics[HitResult.LARGE_TICK_MISS] = score.nlarge_tick_miss
if score.nslider_tail_hit is not None:
s.statistics[HitResult.SLIDER_TAIL_HIT] = score.nslider_tail_hit
# s.user = await convert_db_user_to_api_user(score.user)
if score.nsmall_tick_hit is not None:
s.statistics[HitResult.SMALL_TICK_HIT] = score.nsmall_tick_hit
if score.nlarge_tick_hit is not None:
s.statistics[HitResult.LARGE_TICK_HIT] = score.nlarge_tick_hit
if score.gamemode == GameMode.MANIA:
s.maximum_statistics = {
HitResult.PERFECT: score.beatmap.max_combo,
}
else:
s.maximum_statistics = {
HitResult.GREAT: score.beatmap.max_combo,
}
if user:
s.user = await convert_db_user_to_api_user(user)
s.rank_global = (
await get_score_position_by_id(
session,
score.map_md5,
score.id,
mode=score.gamemode,
user=score.user,
user=user or score.user,
)
or None
)
@@ -193,13 +238,25 @@ class ScoreResp(ScoreBase):
score.map_md5,
score.id,
score.gamemode,
score.user,
user or score.user,
)
or None
)
return s
async def get_best_id(session: AsyncSession, score_id: int) -> None:
rownum = (
func.row_number()
.over(partition_by=col(BestScore.user_id), order_by=col(BestScore.pp).desc())
.label("rn")
)
subq = select(BestScore, rownum).subquery()
stmt = select(subq.c.rn).where(subq.c.score_id == score_id)
result = await session.exec(stmt)
return result.one_or_none()
async def get_leaderboard(
session: AsyncSession,
beatmap_md5: str,
@@ -381,3 +438,207 @@ async def get_score_position_by_id(
result = await session.exec(stmt)
s = result.one_or_none()
return s if s else 0
async def get_user_best_score_in_beatmap(
session: AsyncSession,
beatmap: int,
user: int,
mode: GameMode | None = None,
) -> Score | None:
return (
await session.exec(
Score.select_clause(False)
.where(
Score.gamemode == mode if mode is not None else True,
Score.beatmap_id == beatmap,
Score.user_id == user,
)
.order_by(col(Score.total_score).desc())
)
).first()
async def get_user_best_pp_in_beatmap(
session: AsyncSession,
beatmap: int,
user: int,
mode: GameMode,
) -> BestScore | None:
return (
await session.exec(
select(BestScore).where(
BestScore.beatmap_id == beatmap,
BestScore.user_id == user,
BestScore.gamemode == mode,
)
)
).first()
async def get_user_best_pp(
session: AsyncSession,
user: int,
limit: int = 200,
) -> Sequence[BestScore]:
return (
await session.exec(
select(BestScore)
.where(BestScore.user_id == user)
.order_by(col(BestScore.pp).desc())
.limit(limit)
)
).all()
async def process_user(
session: AsyncSession, user: User, score: Score, ranked: bool = False
):
previous_score_best = await get_user_best_score_in_beatmap(
session, score.beatmap_id, user.id, score.gamemode
)
statistics = None
add_to_db = False
for i in user.lazer_statistics:
if i.mode == score.gamemode.value:
statistics = i
break
if statistics is None:
statistics = LazerUserStatistics(
mode=score.gamemode.value,
user_id=user.id,
)
add_to_db = True
# pc, pt, tth, tts
statistics.total_score += score.total_score
difference = (
score.total_score - previous_score_best.total_score
if previous_score_best and previous_score_best.id != score.id
else score.total_score
)
if difference > 0 and score.passed and ranked:
match score.rank:
case Rank.X:
statistics.grade_ss += 1
case Rank.XH:
statistics.grade_ssh += 1
case Rank.S:
statistics.grade_s += 1
case Rank.SH:
statistics.grade_sh += 1
case Rank.A:
statistics.grade_a += 1
if previous_score_best is not None:
match previous_score_best.rank:
case Rank.X:
statistics.grade_ss -= 1
case Rank.XH:
statistics.grade_ssh -= 1
case Rank.S:
statistics.grade_s -= 1
case Rank.SH:
statistics.grade_sh -= 1
case Rank.A:
statistics.grade_a -= 1
statistics.ranked_score += difference
statistics.level_current = calculate_score_to_level(statistics.ranked_score)
statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo)
statistics.play_count += 1
statistics.play_time += int((score.ended_at - score.started_at).total_seconds())
statistics.total_hits += (
score.n300 + score.n100 + score.n50 + score.ngeki + score.nkatu
)
if score.passed and ranked:
best_pp_scores = await get_user_best_pp(session, user.id)
pp_sum = 0.0
acc_sum = 0.0
for i, bp in enumerate(best_pp_scores):
pp_sum += calculate_weighted_pp(bp.pp, i)
acc_sum += calculate_weighted_acc(bp.acc, i)
if len(best_pp_scores):
# https://github.com/ppy/osu-queue-score-statistics/blob/c538ae/osu.Server.Queues.ScoreStatisticsProcessor/Helpers/UserTotalPerformanceAggregateHelper.cs#L41-L45
acc_sum *= 100 / (20 * (1 - math.pow(0.95, len(best_pp_scores))))
acc_sum = clamp(acc_sum, 0.0, 100.0)
statistics.pp = pp_sum
statistics.hit_accuracy = acc_sum
statistics.updated_at = datetime.now(UTC)
if add_to_db:
session.add(statistics)
await session.commit()
await session.refresh(user)
async def process_score(
user: User,
beatmap_id: int,
ranked: bool,
score_token: ScoreToken,
info: SoloScoreSubmissionInfo,
fetcher: "Fetcher",
session: AsyncSession,
redis: Redis,
) -> Score:
score = Score(
accuracy=info.accuracy,
max_combo=info.max_combo,
# maximum_statistics=info.maximum_statistics,
mods=info.mods,
passed=info.passed,
rank=info.rank,
total_score=info.total_score,
total_score_without_mods=info.total_score_without_mods,
beatmap_id=beatmap_id,
ended_at=datetime.now(UTC),
gamemode=INT_TO_MODE[info.ruleset_id],
started_at=score_token.created_at,
user_id=user.id,
preserve=info.passed,
map_md5=score_token.beatmap.checksum,
has_replay=False,
type="solo",
n300=info.statistics.get(HitResult.GREAT, 0),
n100=info.statistics.get(HitResult.OK, 0),
n50=info.statistics.get(HitResult.MEH, 0),
nmiss=info.statistics.get(HitResult.MISS, 0),
ngeki=info.statistics.get(HitResult.PERFECT, 0),
nkatu=info.statistics.get(HitResult.GOOD, 0),
nlarge_tick_miss=info.statistics.get(HitResult.LARGE_TICK_MISS, 0),
nsmall_tick_hit=info.statistics.get(HitResult.SMALL_TICK_HIT, 0),
nlarge_tick_hit=info.statistics.get(HitResult.LARGE_TICK_HIT, 0),
nslider_tail_hit=info.statistics.get(HitResult.SLIDER_TAIL_HIT, 0),
)
if info.passed and ranked and mods_can_get_pp(info.ruleset_id, info.mods):
beatmap_raw = await fetcher.get_or_fetch_beatmap_raw(redis, beatmap_id)
pp = await asyncio.get_event_loop().run_in_executor(
None, calculate_pp, score, beatmap_raw
)
score.pp = pp
session.add(score)
user_id = user.id
await session.commit()
await session.refresh(score)
if score.passed and ranked:
previous_pp_best = await get_user_best_pp_in_beatmap(
session, beatmap_id, user_id, score.gamemode
)
if previous_pp_best is None or score.pp > previous_pp_best.pp:
assert score.id
best_score = BestScore(
user_id=user_id,
score_id=score.id,
beatmap_id=beatmap_id,
gamemode=score.gamemode,
pp=score.pp,
acc=score.accuracy,
)
session.add(best_score)
session.delete(previous_pp_best) if previous_pp_best else None
await session.commit()
await session.refresh(score)
await session.refresh(score_token)
await session.refresh(user)
return score

View File

@@ -102,8 +102,8 @@ class User(SQLModel, table=True):
)
@classmethod
def all_select_clause(cls):
return select(cls).options(
def all_select_option(cls):
return (
joinedload(cls.lazer_profile), # pyright: ignore[reportArgumentType]
joinedload(cls.lazer_counts), # pyright: ignore[reportArgumentType]
joinedload(cls.daily_challenge_stats), # pyright: ignore[reportArgumentType]
@@ -121,6 +121,10 @@ class User(SQLModel, table=True):
selectinload(cls.lazer_replays_watched), # pyright: ignore[reportArgumentType]
)
@classmethod
def all_select_clause(cls):
return select(cls).options(*cls.all_select_option())
# ============================================
# Lazer API 专用表模型

View File

@@ -4,6 +4,7 @@ from ._base import BaseFetcher
from httpx import AsyncClient
from loguru import logger
import redis
class OsuDotDirectFetcher(BaseFetcher):
@@ -17,3 +18,12 @@ class OsuDotDirectFetcher(BaseFetcher):
)
response.raise_for_status()
return response.text
async def get_or_fetch_beatmap_raw(
self, redis: redis.Redis, beatmap_id: int
) -> str:
if redis.exists(f"beatmap:{beatmap_id}:raw"):
return redis.get(f"beatmap:{beatmap_id}:raw") # pyright: ignore[reportReturnType]
raw = await self.get_beatmap_raw(beatmap_id)
redis.set(f"beatmap:{beatmap_id}:raw", raw, ex=60 * 60 * 24)
return raw

View File

@@ -105,3 +105,65 @@ def mods_to_int(mods: list[APIMod]) -> int:
for mod in mods:
sum_ |= API_MOD_TO_LEGACY.get(mod["acronym"], 0)
return sum_
NO_CHECK = "DO_NO_CHECK"
# FIXME: 这里为空表示了两种情况mod 没有配置项;任何时候都可以获得 pp
# 如果是后者,则 mod 更新的时候可能会误判。
COMMON_CONFIG: dict[str, dict] = {
"EZ": {"retries": 2},
"NF": {},
"HT": {"speed_change": 0.75, "adjust_pitch": NO_CHECK},
"DC": {"speed_change": 0.75},
"HR": {},
"SD": {},
"PF": {},
"HD": {},
"DT": {"speed_change": 1.5, "adjust_pitch": NO_CHECK},
"NC": {"speed_change": 1.5},
"FL": {"size_multiplier": 1.0, "combo_based_size": True},
"AC": {},
"MU": {},
"TD": {},
}
RANKED_MODS: dict[int, dict[str, dict]] = {
0: COMMON_CONFIG,
1: COMMON_CONFIG,
2: COMMON_CONFIG,
3: COMMON_CONFIG,
}
# osu
RANKED_MODS[0]["HD"]["only_fade_approach_circles"] = False
RANKED_MODS[0]["FL"]["follow_delay"] = 1.0
RANKED_MODS[0]["BL"] = {}
RANKED_MODS[0]["NS"] = {}
RANKED_MODS[0]["SO"] = {}
RANKED_MODS[0]["TC"] = {}
# taiko
del RANKED_MODS[1]["EZ"]["retries"]
# catch
RANKED_MODS[2]["NS"] = {}
# mania
del RANKED_MODS[3]["HR"]
RANKED_MODS[3]["FL"]["combo_based_size"] = False
RANKED_MODS[3]["MR"] = {}
for i in range(4, 10):
RANKED_MODS[3][f"{i}K"] = {}
def mods_can_get_pp(ruleset_id: int, mods: list[APIMod]) -> bool:
ranked_mods = RANKED_MODS[ruleset_id]
for mod in mods:
mod["settings"] = mod.get("settings", {})
if (settings := ranked_mods.get(mod["acronym"])) is None:
return False
if settings == {}:
continue
for setting, value in mod["settings"].items():
if (expected_value := settings.get(setting)) is None:
return False
if expected_value != NO_CHECK and value != expected_value:
return False
return True

View File

@@ -44,6 +44,16 @@ class Rank(str, Enum):
D = "D"
F = "F"
@property
def in_statisctics(self):
return self in {
Rank.X,
Rank.XH,
Rank.S,
Rank.SH,
Rank.A,
}
# https://github.com/ppy/osu/blob/master/osu.Game/Rulesets/Scoring/HitResult.cs
class HitResult(str, Enum):

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import datetime
from typing import Any, get_origin
import msgpack
from pydantic import (
BaseModel,
ConfigDict,
@@ -24,11 +23,11 @@ def serialize_to_list(value: BaseModel) -> list[Any]:
elif anno and issubclass(anno, list):
data.append(
TypeAdapter(
info.annotation,
info.annotation, config=ConfigDict(arbitrary_types_allowed=True)
).dump_python(v)
)
elif isinstance(v, datetime.datetime):
data.append([msgpack.ext.Timestamp.from_datetime(v), 0])
data.append([v, 0])
else:
data.append(v)
return data

View File

@@ -11,15 +11,8 @@ from .score import (
)
from .signalr import MessagePackArrayModel, UserState
import msgpack
from pydantic import BaseModel, Field, field_validator
class APIMod(MessagePackArrayModel):
acronym: str
settings: dict[str, Any] | list = Field(
default_factory=dict
) # FIXME: with settings
from msgpack_lazer_api import APIMod
from pydantic import BaseModel, ConfigDict, Field, field_validator
class SpectatedUserState(IntEnum):
@@ -32,6 +25,8 @@ class SpectatedUserState(IntEnum):
class SpectatorState(MessagePackArrayModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
beatmap_id: int | None = None
ruleset_id: int | None = None # 0,1,2,3
mods: list[APIMod] = Field(default_factory=list)
@@ -58,6 +53,8 @@ class ScoreProcessorStatistics(MessagePackArrayModel):
class FrameHeader(MessagePackArrayModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
total_score: int
acc: float
combo: int
@@ -70,10 +67,8 @@ class FrameHeader(MessagePackArrayModel):
@field_validator("received_time", mode="before")
@classmethod
def validate_timestamp(cls, v: Any) -> datetime.datetime:
if isinstance(v, msgpack.ext.Timestamp):
return v.to_datetime()
if isinstance(v, list):
return v[0].to_datetime()
return v[0]
if isinstance(v, datetime.datetime):
return v
if isinstance(v, int | float):
@@ -111,6 +106,8 @@ class APIUser(BaseModel):
class ScoreInfo(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
mods: list[APIMod]
user: APIUser
ruleset: int

View File

@@ -2,16 +2,15 @@ from __future__ import annotations
from datetime import datetime
from enum import Enum
from app.database import (
LazerUserAchievement,
Team as Team,
)
from typing import TYPE_CHECKING
from .score import GameMode
from pydantic import BaseModel
if TYPE_CHECKING:
from app.database import LazerUserAchievement, Team
class PlayStyle(str, Enum):
MOUSE = "mouse"
@@ -83,7 +82,11 @@ class UserAchievement(BaseModel):
achievement_id: int
# 添加数据库模型转换方法
def to_db_model(self, user_id: int) -> LazerUserAchievement:
def to_db_model(self, user_id: int) -> "LazerUserAchievement":
from app.database import (
LazerUserAchievement,
)
return LazerUserAchievement(
user_id=user_id,
achievement_id=self.achievement_id,
@@ -207,7 +210,7 @@ class User(BaseModel):
rank_history: RankHistory | None = None
rankHistory: RankHistory | None = None # 兼容性别名
replays_watched_counts: list[dict] = []
team: Team | None = None
team: "Team | None" = None
user_achievements: list[UserAchievement] = []

View File

@@ -4,6 +4,7 @@ import asyncio
import hashlib
import json
from app.calculator import calculate_beatmap_attribute
from app.database import (
Beatmap,
BeatmapResp,
@@ -20,7 +21,6 @@ from app.models.score import (
INT_TO_MODE,
GameMode,
)
from app.utils import calculate_beatmap_attribute
from .api_router import router
@@ -157,7 +157,7 @@ async def get_beatmap_attributes(
return BeatmapAttributes.model_validate_json(redis.get(key)) # pyright: ignore[reportArgumentType]
try:
resp = await fetcher.get_beatmap_raw(beatmap)
resp = await fetcher.get_or_fetch_beatmap_raw(redis, beatmap)
try:
attr = await asyncio.get_event_loop().run_in_executor(
None, calculate_beatmap_attribute, resp, ruleset, mods_

View File

@@ -8,6 +8,7 @@ from app.dependencies.user import get_current_user
from .api_router import router
from fastapi import Depends, HTTPException, Query, Request
from sqlalchemy.orm import joinedload
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
@@ -25,7 +26,9 @@ async def get_relationship(
else RelationshipType.BLOCK
)
relationships = await db.exec(
select(Relationship).where(
select(Relationship)
.options(joinedload(Relationship.target).options(*DBUser.all_select_option())) # pyright: ignore[reportArgumentType]
.where(
Relationship.user_id == current_user.id,
Relationship.type == relationship_type,
)
@@ -67,7 +70,8 @@ async def add_relationship(
type=relationship_type,
)
db.add(relationship)
if relationship.type == RelationshipType.BLOCK:
origin_type = relationship.type
if origin_type == RelationshipType.BLOCK:
target_relationship = (
await db.exec(
select(Relationship).where(
@@ -78,9 +82,22 @@ async def add_relationship(
).first()
if target_relationship and target_relationship.type == RelationshipType.FOLLOW:
await db.delete(target_relationship)
current_user_id = current_user.id
await db.commit()
await db.refresh(relationship)
if relationship.type == RelationshipType.FOLLOW:
if origin_type == RelationshipType.FOLLOW:
relationship = (
await db.exec(
select(Relationship)
.where(
Relationship.user_id == current_user_id,
Relationship.target_id == target,
)
.options(
joinedload(Relationship.target).options(*DBUser.all_select_option()) # pyright: ignore[reportArgumentType]
)
)
).first()
assert relationship, "Relationship should exist after commit"
return await RelationshipResp.from_db(db, relationship)

View File

@@ -1,18 +1,18 @@
from __future__ import annotations
import datetime
from app.database import (
User as DBUser,
)
from app.database.score import Score, ScoreResp
from app.database.beatmap import Beatmap
from app.database.score import Score, ScoreResp, process_score, process_user
from app.database.score_token import ScoreToken, ScoreTokenResp
from app.dependencies.database import get_db
from app.dependencies.database import get_db, get_redis
from app.dependencies.fetcher import get_fetcher
from app.dependencies.user import get_current_user
from app.models.beatmap import BeatmapRankStatus
from app.models.score import (
INT_TO_MODE,
GameMode,
HitResult,
Rank,
SoloScoreSubmissionInfo,
)
@@ -21,6 +21,7 @@ from .api_router import router
from fastapi import Depends, Form, HTTPException, Query
from pydantic import BaseModel
from redis import Redis
from sqlalchemy.orm import joinedload
from sqlmodel import col, select, true
from sqlmodel.ext.asyncio.session import AsyncSession
@@ -70,8 +71,10 @@ async def get_beatmap_scores(
).first()
return BeatmapScores(
scores=[await ScoreResp.from_db(db, score) for score in all_scores],
userScore=await ScoreResp.from_db(db, user_score) if user_score else None,
scores=[await ScoreResp.from_db(db, score, score.user) for score in all_scores],
userScore=await ScoreResp.from_db(db, user_score, user_score.user)
if user_score
else None,
)
@@ -100,7 +103,7 @@ async def get_user_beatmap_score(
)
user_score = (
await db.exec(
Score.select_clause()
Score.select_clause(True)
.where(
Score.gamemode == mode if mode is not None else True,
Score.beatmap_id == beatmap,
@@ -117,7 +120,7 @@ async def get_user_beatmap_score(
else:
return BeatmapUserScore(
position=user_score.position if user_score.position is not None else 0,
score=await ScoreResp.from_db(db, user_score),
score=await ScoreResp.from_db(db, user_score, user_score.user),
)
@@ -150,7 +153,9 @@ async def get_user_all_beatmap_scores(
)
).all()
return [await ScoreResp.from_db(db, score) for score in all_user_scores]
return [
await ScoreResp.from_db(db, score, current_user) for score in all_user_scores
]
@router.post(
@@ -187,6 +192,8 @@ async def submit_solo_score(
info: SoloScoreSubmissionInfo,
current_user: DBUser = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
redis: Redis = Depends(get_redis),
fetcher=Depends(get_fetcher),
):
if not info.passed:
info.rank = Rank.F
@@ -214,40 +221,33 @@ async def submit_solo_score(
if not score:
raise HTTPException(status_code=404, detail="Score not found")
else:
score = Score(
accuracy=info.accuracy,
max_combo=info.max_combo,
# maximum_statistics=info.maximum_statistics,
mods=info.mods,
passed=info.passed,
rank=info.rank,
total_score=info.total_score,
total_score_without_mods=info.total_score_without_mods,
beatmap_id=beatmap,
ended_at=datetime.datetime.now(datetime.UTC),
gamemode=INT_TO_MODE[info.ruleset_id],
started_at=score_token.created_at,
user_id=current_user.id,
preserve=info.passed,
map_md5=score_token.beatmap.checksum,
has_replay=False,
pp=info.pp,
type="solo",
n300=info.statistics.get(HitResult.GREAT, 0),
n100=info.statistics.get(HitResult.OK, 0),
n50=info.statistics.get(HitResult.MEH, 0),
nmiss=info.statistics.get(HitResult.MISS, 0),
ngeki=info.statistics.get(HitResult.PERFECT, 0),
nkatu=info.statistics.get(HitResult.GOOD, 0),
beatmap_status = (
await db.exec(
select(Beatmap.beatmap_status).where(Beatmap.id == beatmap)
)
).first()
if beatmap_status is None:
raise HTTPException(status_code=404, detail="Beatmap not found")
ranked = beatmap_status in {
BeatmapRankStatus.RANKED,
BeatmapRankStatus.APPROVED,
}
score = await process_score(
current_user,
beatmap,
ranked,
score_token,
info,
fetcher,
db,
redis,
)
db.add(score)
await db.commit()
await db.refresh(score)
await db.refresh(current_user)
score_id = score.id
score_token.score_id = score_id
await db.commit()
await process_user(db, current_user, score, ranked)
score = (
await db.exec(Score.select_clause().where(Score.id == score_id))
).first()
assert score is not None
return await ScoreResp.from_db(db, score)
return await ScoreResp.from_db(db, score, current_user)

View File

@@ -2,57 +2,51 @@ from __future__ import annotations
from typing import Literal
from app.database import (
User as DBUser,
)
from app.dependencies import get_current_user
from app.database import User as DBUser
from app.dependencies.database import get_db
from app.models.score import INT_TO_MODE
from app.models.user import (
User as ApiUser,
)
from app.models.user import User as ApiUser
from app.utils import convert_db_user_to_api_user
from .api_router import router
from fastapi import Depends, HTTPException, Query
from pydantic import BaseModel
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlmodel.sql.expression import col
@router.get("/users/{user}/{ruleset}", response_model=ApiUser)
@router.get("/users/{user}", response_model=ApiUser)
async def get_user_info_default(
user: str,
ruleset: Literal["osu", "taiko", "fruits", "mania"] = "osu",
current_user: DBUser = Depends(get_current_user),
session: AsyncSession = Depends(get_db),
):
searched_user = (
await session.exec(
DBUser.all_select_clause().where(
DBUser.id == int(user)
if user.isdigit()
else DBUser.name == user.removeprefix("@")
)
)
).first()
if not searched_user:
raise HTTPException(404, detail="User not found")
return await convert_db_user_to_api_user(searched_user, ruleset=ruleset)
# ---------- Shared Utility ----------
async def get_user_by_lookup(
db: AsyncSession, lookup: str, key: str = "id"
) -> DBUser | None:
"""根据查找方式获取用户"""
if key == "id":
try:
user_id = int(lookup)
result = await db.exec(select(DBUser).where(DBUser.id == user_id))
return result.first()
except ValueError:
return None
elif key == "username":
result = await db.exec(select(DBUser).where(DBUser.name == lookup))
return result.first()
else:
return None
# ---------- Batch Users ----------
class BatchUserResponse(BaseModel):
users: list[ApiUser]
@router.get("/users", response_model=BatchUserResponse)
@router.get("/users/lookup", response_model=BatchUserResponse)
@router.get("/users/lookup/", response_model=BatchUserResponse)
async def get_users(
user_ids: list[int] = Query(default_factory=list, alias="ids[]"),
include_variant_statistics: bool = Query(default=False), # TODO
current_user: DBUser = Depends(get_current_user),
include_variant_statistics: bool = Query(default=False), # TODO: future use
session: AsyncSession = Depends(get_db),
):
if user_ids:
@@ -68,8 +62,64 @@ async def get_users(
return BatchUserResponse(
users=[
await convert_db_user_to_api_user(
searched_user, ruleset=INT_TO_MODE[current_user.preferred_mode].value
searched_user, ruleset=INT_TO_MODE[searched_user.preferred_mode].value
)
for searched_user in searched_users
]
)
# # ---------- Individual User ----------
# @router.get("/users/{user_lookup}/{mode}", response_model=ApiUser)
# @router.get("/users/{user_lookup}/{mode}/", response_model=ApiUser)
# async def get_user_with_mode(
# user_lookup: str,
# mode: Literal["osu", "taiko", "fruits", "mania"],
# key: Literal["id", "username"] = Query("id"),
# current_user: DBUser = Depends(get_current_user),
# db: AsyncSession = Depends(get_db),
# ):
# """获取指定游戏模式的用户信息"""
# user = await get_user_by_lookup(db, user_lookup, key)
# if not user:
# raise HTTPException(status_code=404, detail="User not found")
# return await convert_db_user_to_api_user(user, mode)
# @router.get("/users/{user_lookup}", response_model=ApiUser)
# @router.get("/users/{user_lookup}/", response_model=ApiUser)
# async def get_user_default(
# user_lookup: str,
# key: Literal["id", "username"] = Query("id"),
# current_user: DBUser = Depends(get_current_user),
# db: AsyncSession = Depends(get_db),
# ):
# """获取用户信息默认使用osu模式但包含所有模式的统计信息"""
# user = await get_user_by_lookup(db, user_lookup, key)
# if not user:
# raise HTTPException(status_code=404, detail="User not found")
# return await convert_db_user_to_api_user(user, "osu")
@router.get("/users/{user}/{ruleset}", response_model=ApiUser)
@router.get("/users/{user}/", response_model=ApiUser)
@router.get("/users/{user}", response_model=ApiUser)
async def get_user_info(
user: str,
ruleset: Literal["osu", "taiko", "fruits", "mania"] = "osu",
session: AsyncSession = Depends(get_db),
):
searched_user = (
await session.exec(
DBUser.all_select_clause().where(
DBUser.id == int(user)
if user.isdigit()
else DBUser.name == user.removeprefix("@")
)
)
).first()
if not searched_user:
raise HTTPException(404, detail="User not found")
return await convert_db_user_to_api_user(searched_user, ruleset=ruleset)

View File

@@ -8,7 +8,7 @@ from typing import (
Protocol as TypingProtocol,
)
import msgpack
import msgpack_lazer_api as m
SEP = b"\x1e"
@@ -104,11 +104,7 @@ class MsgpackProtocol:
def decode(input: bytes) -> list[Packet]:
length, offset = MsgpackProtocol._decode_varint(input)
message_data = input[offset : offset + length]
# FIXME: custom deserializer for APIMod
# https://github.com/ppy/osu/blob/master/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
unpacked = msgpack.unpackb(
message_data, raw=False, strict_map_key=False, use_list=True
)
unpacked = m.decode(message_data)
packet_type = PacketType(unpacked[0])
if packet_type not in PACKETS:
raise ValueError(f"Unknown packet type: {packet_type}")
@@ -180,7 +176,7 @@ class MsgpackProtocol:
)
elif isinstance(packet, PingPacket):
payload.pop(-1)
data = msgpack.packb(payload, use_bin_type=True, datetime=True)
data = m.encode(payload)
return MsgpackProtocol._encode_varint(len(data)) + data

View File

@@ -8,12 +8,10 @@ from app.database import (
LazerUserStatistics,
User as DBUser,
)
from app.models.beatmap import BeatmapAttributes
from app.models.mods import APIMod
from app.models.score import GameMode
from app.models.user import (
Country,
Cover,
DailyChallengeStats,
GradeCounts,
Kudosu,
Level,
@@ -25,8 +23,6 @@ from app.models.user import (
UserAchievement,
)
import rosu_pp_py as rosu
def unix_timestamp_to_windows(timestamp: int) -> int:
"""Convert a Unix timestamp to a Windows timestamp."""
@@ -115,34 +111,37 @@ async def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu") ->
# 转换所有模式的统计信息
statistics_rulesets = {}
for stat in db_user.statistics:
statistics_rulesets[stat.mode] = Statistics(
count_100=stat.count_100,
count_300=stat.count_300,
count_50=stat.count_50,
count_miss=stat.count_miss,
level=Level(current=stat.level_current, progress=stat.level_progress),
global_rank=stat.global_rank,
global_rank_exp=stat.global_rank_exp,
pp=stat.pp,
pp_exp=stat.pp_exp,
ranked_score=stat.ranked_score,
hit_accuracy=stat.hit_accuracy,
play_count=stat.play_count,
play_time=stat.play_time,
total_score=stat.total_score,
total_hits=stat.total_hits,
maximum_combo=stat.maximum_combo,
replays_watched_by_others=stat.replays_watched_by_others,
is_ranked=stat.is_ranked,
grade_counts=GradeCounts(
ss=stat.grade_ss,
ssh=stat.grade_ssh,
s=stat.grade_s,
sh=stat.grade_sh,
a=stat.grade_a,
),
)
if db_user.lazer_statistics:
for stat in db_user.lazer_statistics:
statistics_rulesets[stat.mode] = Statistics(
count_100=stat.count_100,
count_300=stat.count_300,
count_50=stat.count_50,
count_miss=stat.count_miss,
level=Level(current=stat.level_current, progress=stat.level_progress),
global_rank=stat.global_rank,
global_rank_exp=stat.global_rank_exp,
pp=float(stat.pp) if stat.pp else 0.0,
pp_exp=float(stat.pp_exp) if stat.pp_exp else 0.0,
ranked_score=stat.ranked_score,
hit_accuracy=float(stat.hit_accuracy) if stat.hit_accuracy else 0.0,
play_count=stat.play_count,
play_time=stat.play_time,
total_score=stat.total_score,
total_hits=stat.total_hits,
maximum_combo=stat.maximum_combo,
replays_watched_by_others=stat.replays_watched_by_others,
is_ranked=stat.is_ranked,
grade_counts=GradeCounts(
ss=stat.grade_ss,
ssh=stat.grade_ssh,
s=stat.grade_s,
sh=stat.grade_sh,
a=stat.grade_a,
),
country_rank=stat.country_rank,
rank={"country": stat.country_rank} if stat.country_rank else None,
)
# 转换国家信息
country = Country(code=user_country_code, name=get_country_name(user_country_code))
@@ -401,7 +400,36 @@ async def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu") ->
active_tournament_banners=active_tournament_banners,
badges=badges,
current_season_stats=None,
daily_challenge_user_stats=None,
daily_challenge_user_stats=DailyChallengeStats(
user_id=user_id,
daily_streak_best=db_user.daily_challenge_stats.daily_streak_best
if db_user.daily_challenge_stats
else 0,
daily_streak_current=db_user.daily_challenge_stats.daily_streak_current
if db_user.daily_challenge_stats
else 0,
last_update=db_user.daily_challenge_stats.last_update
if db_user.daily_challenge_stats
else None,
last_weekly_streak=db_user.daily_challenge_stats.last_weekly_streak
if db_user.daily_challenge_stats
else None,
playcount=db_user.daily_challenge_stats.playcount
if db_user.daily_challenge_stats
else 0,
top_10p_placements=db_user.daily_challenge_stats.top_10p_placements
if db_user.daily_challenge_stats
else 0,
top_50p_placements=db_user.daily_challenge_stats.top_50p_placements
if db_user.daily_challenge_stats
else 0,
weekly_streak_best=db_user.daily_challenge_stats.weekly_streak_best
if db_user.daily_challenge_stats
else 0,
weekly_streak_current=db_user.daily_challenge_stats.weekly_streak_current
if db_user.daily_challenge_stats
else 0,
),
groups=[],
monthly_playcounts=monthly_playcounts,
page=Page(html=profile.page_html or "", raw=profile.page_raw or "")
@@ -435,26 +463,3 @@ def get_country_name(country_code: str) -> str:
# 可以添加更多国家
}
return country_names.get(country_code, "Unknown")
def calculate_beatmap_attribute(
beatmap: str,
gamemode: GameMode | None = None,
mods: int | list[APIMod] | list[str] = 0,
) -> BeatmapAttributes:
map = rosu.Beatmap(content=beatmap)
if gamemode is not None:
map.convert(gamemode.to_rosu(), mods)
diff = rosu.Difficulty(mods=mods).calculate(map)
return BeatmapAttributes(
star_rating=diff.stars,
max_combo=diff.max_combo,
aim_difficulty=diff.aim,
aim_difficult_slider_count=diff.aim_difficult_slider_count,
speed_difficulty=diff.speed,
speed_note_count=diff.speed_note_count,
slider_factor=diff.slider_factor,
aim_difficult_strain_count=diff.aim_difficult_strain_count,
speed_difficult_strain_count=diff.speed_difficult_strain_count,
mono_stamina_factor=diff.stamina,
)

View File

@@ -38,6 +38,7 @@ async def create_sample_user():
existing_user2 = result2.first()
if existing_user is not None and existing_user2 is not None:
print("示例用户已存在,跳过创建")
return
# 当前时间戳
# current_timestamp = int(time.time())

View File

@@ -4,12 +4,16 @@ from contextlib import asynccontextmanager
from datetime import datetime
from app.config import settings
from app.database import Team # noqa: F401
from app.dependencies.database import create_tables, engine
from app.dependencies.fetcher import get_fetcher
from app.models.user import User
from app.router import api_router, auth_router, fetcher_router, signalr_router
from fastapi import FastAPI
User.model_rebuild()
@asynccontextmanager
async def lifespan(app: FastAPI):

View File

@@ -0,0 +1,38 @@
"""score: remove best_id in database
Revision ID: 78be13c71791
Revises: dc4d25c428c7
Create Date: 2025-07-29 07:57:33.764517
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "78be13c71791"
down_revision: str | Sequence[str] | None = "dc4d25c428c7"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("scores", "best_id")
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"scores",
sa.Column("best_id", mysql.INTEGER(), autoincrement=False, nullable=True),
)
# ### end Alembic commands ###

View File

@@ -0,0 +1,36 @@
"""score: add nlarge_tick_hit & nsmall_tick_hit for pp calculator
Revision ID: dc4d25c428c7
Revises:
Create Date: 2025-07-29 01:43:40.221070
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "dc4d25c428c7"
down_revision: str | Sequence[str] | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("scores", sa.Column("nlarge_tick_hit", sa.Integer(), nullable=True))
op.add_column("scores", sa.Column("nsmall_tick_hit", sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("scores", "nsmall_tick_hit")
op.drop_column("scores", "nlarge_tick_hit")
# ### end Alembic commands ###

View File

@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "packages/msgpack_lazer_api"
}
],
"settings": {}
}

424
packages/msgpack_lazer_api/Cargo.lock generated Normal file
View File

@@ -0,0 +1,424 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "chrono"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indoc"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "msgpack-lazer-api"
version = "0.1.0"
dependencies = [
"chrono",
"pyo3",
"rmp",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a"
dependencies = [
"chrono",
"indoc",
"libc",
"memoffset",
"once_cell",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-build-config"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc"
dependencies = [
"heck",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rmp"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-lexicon"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unindent"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]

View File

@@ -0,0 +1,14 @@
[package]
name = "msgpack-lazer-api"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "msgpack_lazer_api"
crate-type = ["cdylib"]
[dependencies]
chrono = "0.4.41"
pyo3 = { version = "0.25.0", features = ["extension-module", "chrono"] }
rmp = "0.8.14"

View File

@@ -0,0 +1,11 @@
from typing import Any
class APIMod:
def __init__(self, acronym: str, settings: dict[str, Any]) -> None: ...
@property
def acronym(self) -> str: ...
@property
def settings(self) -> str: ...
def encode(obj: Any) -> bytes: ...
def decode(data: bytes) -> Any: ...

View File

@@ -0,0 +1,16 @@
[build-system]
requires = ["maturin>=1.9,<2.0"]
build-backend = "maturin"
[project]
name = "msgpack-lazer-api"
requires-python = ">=3.12"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dynamic = ["version"]
[tool.maturin]
features = ["pyo3/extension-module"]

View File

@@ -0,0 +1,315 @@
use crate::APIMod;
use chrono::{TimeZone, Utc};
use pyo3::types::PyDict;
use pyo3::{prelude::*, IntoPyObjectExt};
use std::collections::HashMap;
use std::io::Read;
pub fn read_object(
py: Python<'_>,
cursor: &mut std::io::Cursor<&[u8]>,
api_mod: bool,
) -> PyResult<PyObject> {
match rmp::decode::read_marker(cursor) {
Ok(marker) => match marker {
rmp::Marker::Null => Ok(py.None()),
rmp::Marker::FixPos(val) => Ok(val.into_pyobject(py)?.into_any().unbind()),
rmp::Marker::FixNeg(val) => Ok(val.into_pyobject(py)?.into_any().unbind()),
rmp::Marker::U8 => {
let mut buf = [0u8; 1];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
Ok(buf[0].into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::U16 => {
let mut buf = [0u8; 2];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let val = u16::from_be_bytes(buf);
Ok(val.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::U32 => {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let val = u32::from_be_bytes(buf);
Ok(val.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::U64 => {
let mut buf = [0u8; 8];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let val = u64::from_be_bytes(buf);
Ok(val.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::I8 => {
let mut buf = [0u8; 1];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let val = i8::from_be_bytes(buf);
Ok(val.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::I16 => {
let mut buf = [0u8; 2];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let val = i16::from_be_bytes(buf);
Ok(val.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::I32 => {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let val = i32::from_be_bytes(buf);
Ok(val.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::I64 => {
let mut buf = [0u8; 8];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let val = i64::from_be_bytes(buf);
Ok(val.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::Bin8 => {
let mut buf = [0u8; 1];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = buf[0] as u32;
let mut data = vec![0u8; len as usize];
cursor.read_exact(&mut data).map_err(to_py_err)?;
Ok(data.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::Bin16 => {
let mut buf = [0u8; 2];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = u16::from_be_bytes(buf) as u32;
let mut data = vec![0u8; len as usize];
cursor.read_exact(&mut data).map_err(to_py_err)?;
Ok(data.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::Bin32 => {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = u32::from_be_bytes(buf);
let mut data = vec![0u8; len as usize];
cursor.read_exact(&mut data).map_err(to_py_err)?;
Ok(data.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::True => Ok(true.into_py_any(py)?),
rmp::Marker::False => Ok(false.into_py_any(py)?),
rmp::Marker::FixStr(len) => read_string(py, cursor, len as u32),
rmp::Marker::Str8 => {
let mut buf = [0u8; 1];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = buf[0] as u32;
read_string(py, cursor, len)
}
rmp::Marker::Str16 => {
let mut buf = [0u8; 2];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = u16::from_be_bytes(buf) as u32;
read_string(py, cursor, len)
}
rmp::Marker::Str32 => {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = u32::from_be_bytes(buf);
read_string(py, cursor, len)
}
rmp::Marker::FixArray(len) => read_array(py, cursor, len as u32, api_mod),
rmp::Marker::Array16 => {
let mut buf = [0u8; 2];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = u16::from_be_bytes(buf) as u32;
read_array(py, cursor, len, api_mod)
}
rmp::Marker::Array32 => {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = u32::from_be_bytes(buf);
read_array(py, cursor, len, api_mod)
}
rmp::Marker::FixMap(len) => read_map(py, cursor, len as u32),
rmp::Marker::Map16 => {
let mut buf = [0u8; 2];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = u16::from_be_bytes(buf) as u32;
read_map(py, cursor, len)
}
rmp::Marker::Map32 => {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = u32::from_be_bytes(buf);
read_map(py, cursor, len)
}
rmp::Marker::F32 => {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let val = f32::from_be_bytes(buf);
Ok(val.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::F64 => {
let mut buf = [0u8; 8];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let val = f64::from_be_bytes(buf);
Ok(val.into_pyobject(py)?.into_any().unbind())
}
rmp::Marker::FixExt1 => read_ext(py, cursor, 1),
rmp::Marker::FixExt2 => read_ext(py, cursor, 2),
rmp::Marker::FixExt4 => read_ext(py, cursor, 4),
rmp::Marker::FixExt8 => read_ext(py, cursor, 8),
rmp::Marker::FixExt16 => read_ext(py, cursor, 16),
rmp::Marker::Ext8 => {
let mut buf = [0u8; 1];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = buf[0] as u32;
read_ext(py, cursor, len)
}
rmp::Marker::Ext16 => {
let mut buf = [0u8; 2];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = u16::from_be_bytes(buf) as u32;
read_ext(py, cursor, len)
}
rmp::Marker::Ext32 => {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let len = u32::from_be_bytes(buf);
read_ext(py, cursor, len)
}
_ => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
"Unsupported MessagePack marker",
)),
},
Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"Failed to read marker: {:?}",
e
))),
}
}
fn read_string(
py: Python<'_>,
cursor: &mut std::io::Cursor<&[u8]>,
len: u32,
) -> PyResult<PyObject> {
let mut buf = vec![0u8; len as usize];
cursor.read_exact(&mut buf).map_err(to_py_err)?;
let s = String::from_utf8(buf)
.map_err(|_| PyErr::new::<pyo3::exceptions::PyUnicodeDecodeError, _>("Invalid UTF-8"))?;
Ok(s.into_pyobject(py)?.into_any().unbind())
}
fn read_array(
py: Python,
cursor: &mut std::io::Cursor<&[u8]>,
len: u32,
api_mod: bool,
) -> PyResult<PyObject> {
let mut items = Vec::new();
let array_len = if api_mod { len * 2 } else { len };
let dict = PyDict::new(py);
let mut i = 0;
if len == 2 && !api_mod {
// 姑且这样判断列表长度为2第一个元素为长度为2的字符串api_mod 模式未启用(不存在嵌套 APIMod
let obj1 = read_object(py, cursor, false)?;
if obj1.extract::<String>(py).map_or(false, |k| k.len() == 2) {
let obj2 = read_object(py, cursor, true)?;
return Ok(APIMod {
acronym: obj1.extract::<String>(py)?,
settings: obj2.extract::<HashMap<String, PyObject>>(py)?,
}
.into_pyobject(py)?
.into_any()
.unbind());
} else {
items.push(obj1);
i += 1;
}
}
while i < array_len {
if api_mod && i % 2 == 0 {
let key = read_object(py, cursor, false)?;
let value = read_object(py, cursor, false)?;
dict.set_item(key, value)?;
i += 2;
} else {
let item = read_object(py, cursor, api_mod)?;
items.push(item);
i += 1;
}
}
if api_mod {
return Ok(dict.into_pyobject(py)?.into_any().unbind());
} else {
Ok(items.into_pyobject(py)?.into_any().unbind())
}
}
fn read_map(py: Python, cursor: &mut std::io::Cursor<&[u8]>, len: u32) -> PyResult<PyObject> {
let mut pairs = Vec::new();
for _ in 0..len {
let key = read_object(py, cursor, false)?;
let value = read_object(py, cursor, false)?;
pairs.push((key, value));
}
let dict = PyDict::new(py);
for (key, value) in pairs {
dict.set_item(key, value)?;
}
return Ok(dict.into_pyobject(py)?.into_any().unbind());
}
fn to_py_err(err: std::io::Error) -> PyErr {
PyErr::new::<pyo3::exceptions::PyIOError, _>(format!("IO error: {}", err))
}
fn read_ext(py: Python, cursor: &mut std::io::Cursor<&[u8]>, len: u32) -> PyResult<PyObject> {
// Read the extension type
let mut type_buf = [0u8; 1];
cursor.read_exact(&mut type_buf).map_err(to_py_err)?;
let ext_type = type_buf[0] as i8;
// Read the extension data
let mut data = vec![0u8; len as usize];
cursor.read_exact(&mut data).map_err(to_py_err)?;
// Handle timestamp extension (type = -1)
if ext_type == -1 {
read_timestamp(py, &data)
} else {
// For other extension types, return as bytes or handle as needed
Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"Unsupported extension type: {}",
ext_type
)))
}
}
fn read_timestamp(py: Python, data: &[u8]) -> PyResult<PyObject> {
let (secs, nsec) = match data.len() {
4 => {
// timestamp32: 4-byte big endian seconds
let secs = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as u64;
(secs, 0u32)
}
8 => {
// timestamp64: 8-byte packed => upper 34 bits nsec, lower 30 bits secs
let packed = u64::from_be_bytes([
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
]);
let nsec = (packed >> 34) as u32;
let secs = packed & 0x3FFFFFFFF; // lower 34 bits
(secs, nsec)
}
12 => {
// timestamp96: 12 bytes = 4-byte nsec + 8-byte seconds signed
let nsec = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let secs = i64::from_be_bytes([
data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
]) as u64;
(secs, nsec)
}
_ => {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"Invalid timestamp data length: {}",
data.len()
)));
}
};
let time = Utc.timestamp_opt(secs as i64, nsec).single();
Ok(time.into_pyobject(py)?.into_any().unbind())
}

View File

@@ -0,0 +1,132 @@
use crate::APIMod;
use chrono::{DateTime, Utc};
use pyo3::prelude::{PyAnyMethods, PyDictMethods, PyListMethods, PyStringMethods};
use pyo3::types::{PyBool, PyBytes, PyDateTime, PyDict, PyFloat, PyInt, PyList, PyNone, PyString};
use pyo3::{Bound, PyAny, PyRef, Python};
use std::io::Write;
fn write_list(buf: &mut Vec<u8>, obj: &Bound<'_, PyList>) {
rmp::encode::write_array_len(buf, obj.len() as u32).unwrap();
for item in obj.iter() {
write_object(buf, &item);
}
}
fn write_string(buf: &mut Vec<u8>, obj: &Bound<'_, PyString>) {
let s = obj.to_string_lossy();
rmp::encode::write_str(buf, &s).unwrap();
}
fn write_integer(buf: &mut Vec<u8>, obj: &Bound<'_, PyInt>) {
if let Ok(val) = obj.extract::<i32>() {
rmp::encode::write_i32(buf, val).unwrap();
} else if let Ok(val) = obj.extract::<i64>() {
rmp::encode::write_i64(buf, val).unwrap();
} else {
panic!("Unsupported integer type");
}
}
fn write_float(buf: &mut Vec<u8>, obj: &Bound<'_, PyAny>) {
if let Ok(val) = obj.extract::<f32>() {
rmp::encode::write_f32(buf, val).unwrap();
} else if let Ok(val) = obj.extract::<f64>() {
rmp::encode::write_f64(buf, val).unwrap();
} else {
panic!("Unsupported float type");
}
}
fn write_bool(buf: &mut Vec<u8>, obj: &Bound<'_, PyBool>) {
if let Ok(b) = obj.extract::<bool>() {
rmp::encode::write_bool(buf, b).unwrap();
} else {
panic!("Unsupported boolean type");
}
}
fn write_bin(buf: &mut Vec<u8>, obj: &Bound<'_, PyBytes>) {
if let Ok(bytes) = obj.extract::<Vec<u8>>() {
rmp::encode::write_bin(buf, &bytes).unwrap();
} else {
panic!("Unsupported binary type");
}
}
fn write_hashmap(buf: &mut Vec<u8>, obj: &Bound<'_, PyDict>) {
rmp::encode::write_map_len(buf, obj.len() as u32).unwrap();
for (key, value) in obj.iter() {
write_object(buf, &key);
write_object(buf, &value);
}
}
fn write_nil(buf: &mut Vec<u8>){
rmp::encode::write_nil(buf).unwrap();
}
// https://github.com/ppy/osu/blob/3dced3/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
fn write_api_mod(buf: &mut Vec<u8>, api_mod: PyRef<APIMod>) {
rmp::encode::write_array_len(buf, 2).unwrap();
rmp::encode::write_str(buf, &api_mod.acronym).unwrap();
rmp::encode::write_array_len(buf, api_mod.settings.len() as u32).unwrap();
for (k, v) in api_mod.settings.iter() {
rmp::encode::write_str(buf, k).unwrap();
Python::with_gil(|py| write_object(buf, &v.bind(py)));
}
}
fn write_datetime(buf: &mut Vec<u8>, obj: &Bound<'_, PyDateTime>) {
if let Ok(dt) = obj.extract::<DateTime<Utc>>() {
let secs = dt.timestamp();
let nsec = dt.timestamp_subsec_nanos();
write_timestamp(buf, secs, nsec);
} else {
panic!("Unsupported datetime type. Check your input, timezone is needed.");
}
}
fn write_timestamp(wr: &mut Vec<u8>, secs: i64, nsec: u32) {
let buf: Vec<u8> = if nsec == 0 && secs >= 0 && secs <= u32::MAX as i64 {
// timestamp32: 4-byte big endian seconds
secs.to_be_bytes()[4..].to_vec()
} else if secs >= -(1 << 34) && secs < (1 << 34) {
// timestamp64: 8-byte packed => upper 34 bits nsec, lower 34 bits secs
let packed = ((nsec as u64) << 34) | (secs as u64 & ((1 << 34) - 1));
packed.to_be_bytes().to_vec()
} else {
// timestamp96: 12 bytes = 4-byte nsec + 8-byte seconds signed
let mut v = Vec::with_capacity(12);
v.extend_from_slice(&nsec.to_be_bytes());
v.extend_from_slice(&secs.to_be_bytes());
v
};
rmp::encode::write_ext_meta(wr, buf.len() as u32, -1).unwrap();
wr.write_all(&buf).unwrap();
}
pub fn write_object(buf: &mut Vec<u8>, obj: &Bound<'_, PyAny>) {
if let Ok(list) = obj.downcast::<PyList>() {
write_list(buf, list);
} else if let Ok(string) = obj.downcast::<PyString>() {
write_string(buf, string);
} else if let Ok(integer) = obj.downcast::<PyInt>() {
write_integer(buf, integer);
} else if let Ok(float) = obj.downcast::<PyFloat>() {
write_float(buf, float);
} else if let Ok(boolean) = obj.downcast::<PyBool>() {
write_bool(buf, boolean);
} else if let Ok(bytes) = obj.downcast::<PyBytes>() {
write_bin(buf, bytes);
} else if let Ok(dict) = obj.downcast::<PyDict>() {
write_hashmap(buf, dict);
} else if let Ok(_none) = obj.downcast::<PyNone>() {
write_nil(buf);
} else if let Ok(datetime) = obj.downcast::<PyDateTime>() {
write_datetime(buf, datetime);
} else if let Ok(api_mod) = obj.extract::<PyRef<APIMod>>() {
write_api_mod(buf, api_mod);
} else {
panic!("Unsupported type");
}
}

View File

@@ -0,0 +1,51 @@
mod decode;
mod encode;
use pyo3::prelude::*;
use std::collections::HashMap;
#[pyclass]
struct APIMod {
#[pyo3(get, set)]
acronym: String,
#[pyo3(get, set)]
settings: HashMap<String, PyObject>,
}
#[pymethods]
impl APIMod {
#[new]
fn new(acronym: String, settings: HashMap<String, PyObject>) -> Self {
APIMod { acronym, settings }
}
fn __repr__(&self) -> String {
format!(
"APIMod(acronym='{}', settings={:?})",
self.acronym, self.settings
)
}
}
#[pyfunction]
#[pyo3(name = "encode")]
fn encode_py(obj: &Bound<'_, PyAny>) -> PyResult<Vec<u8>> {
let mut buf = Vec::new();
encode::write_object(&mut buf, obj);
Ok(buf)
}
#[pyfunction]
#[pyo3(name = "decode")]
fn decode_py(py: Python, data: &[u8]) -> PyResult<PyObject> {
let mut cursor = std::io::Cursor::new(data);
decode::read_object(py, &mut cursor, false)
}
#[pymodule]
fn msgpack_lazer_api(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(encode_py, m)?)?;
m.add_function(wrap_pyfunction!(decode_py, m)?)?;
m.add_class::<APIMod>()?;
Ok(())
}

View File

@@ -12,7 +12,7 @@ dependencies = [
"fastapi>=0.104.1",
"httpx>=0.28.1",
"loguru>=0.7.3",
"msgpack>=1.1.1",
"msgpack-lazer-api",
"passlib[bcrypt]>=1.7.4",
"pydantic[email]>=2.5.0",
"python-dotenv>=1.0.0",
@@ -87,9 +87,20 @@ disableBytesTypePromotions = true
reportIncompatibleMethodOverride = false
reportIncompatibleVariableOverride = false
[tool.uv.workspace]
members = [
"packages/msgpack_lazer_api",
]
[tool.uv.sources]
msgpack-lazer-api = { workspace = true }
[tool.uv]
cache-keys = [{file = "pyproject.toml"}, {file = "packages/msgpack_lazer_api/Cargo.toml"}, {file = "**/*.rs"}]
[dependency-groups]
dev = [
"msgpack-types>=0.5.0",
"maturin>=1.9.2",
"pre-commit>=4.2.0",
"ruff>=0.12.4",
]

View File

@@ -1,735 +1,57 @@
# This file was autogenerated by uv via the following command:
# uv export
aiomysql==0.2.0 \
--hash=sha256:558b9c26d580d08b8c5fd1be23c5231ce3aeff2dadad989540fee740253deb67 \
--hash=sha256:b7c26da0daf23a5ec5e0b133c03d20657276e4eae9b73e040b72787f6f6ade0a
# via osu-lazer-api
alembic==1.16.4 \
--hash=sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d \
--hash=sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2
# via osu-lazer-api
annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
# via pydantic
anyio==4.9.0 \
--hash=sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028 \
--hash=sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c
# via
# httpx
# starlette
# watchfiles
async-timeout==5.0.1 ; python_full_version < '3.11.3' \
--hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \
--hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3
# via redis
bcrypt==4.3.0 \
--hash=sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f \
--hash=sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d \
--hash=sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24 \
--hash=sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3 \
--hash=sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c \
--hash=sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd \
--hash=sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f \
--hash=sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f \
--hash=sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d \
--hash=sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe \
--hash=sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231 \
--hash=sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef \
--hash=sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18 \
--hash=sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f \
--hash=sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e \
--hash=sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732 \
--hash=sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304 \
--hash=sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0 \
--hash=sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8 \
--hash=sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938 \
--hash=sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62 \
--hash=sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180 \
--hash=sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af \
--hash=sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669 \
--hash=sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761 \
--hash=sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51 \
--hash=sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23 \
--hash=sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09 \
--hash=sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505 \
--hash=sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4 \
--hash=sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753 \
--hash=sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59 \
--hash=sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b \
--hash=sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d \
--hash=sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a \
--hash=sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b \
--hash=sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a \
--hash=sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce \
--hash=sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb \
--hash=sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb \
--hash=sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676 \
--hash=sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b \
--hash=sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe \
--hash=sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281 \
--hash=sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1 \
--hash=sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef \
--hash=sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d
# via
# osu-lazer-api
# passlib
certifi==2025.7.14 \
--hash=sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2 \
--hash=sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995
# via
# httpcore
# httpx
cffi==1.17.1 ; platform_python_implementation != 'PyPy' \
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
--hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
--hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
--hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
--hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
--hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
--hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
--hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
--hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
--hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
--hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
--hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
--hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
--hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
--hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
--hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
--hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
--hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
--hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
--hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
--hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
--hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
--hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
# via cryptography
cfgv==3.4.0 \
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
# via pre-commit
click==8.2.1 \
--hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \
--hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b
# via uvicorn
colorama==0.4.6 ; sys_platform == 'win32' \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via
# click
# uvicorn
cryptography==45.0.5 \
--hash=sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7 \
--hash=sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8 \
--hash=sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463 \
--hash=sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f \
--hash=sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135 \
--hash=sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d \
--hash=sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57 \
--hash=sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd \
--hash=sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e \
--hash=sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1 \
--hash=sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0 \
--hash=sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a \
--hash=sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a \
--hash=sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492 \
--hash=sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e \
--hash=sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e \
--hash=sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97 \
--hash=sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174 \
--hash=sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9 \
--hash=sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18 \
--hash=sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0 \
--hash=sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27 \
--hash=sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63 \
--hash=sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6 \
--hash=sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42 \
--hash=sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9 \
--hash=sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d \
--hash=sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f \
--hash=sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0 \
--hash=sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5 \
--hash=sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8
# via
# osu-lazer-api
# python-jose
distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
dnspython==2.7.0 \
--hash=sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86 \
--hash=sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1
# via email-validator
ecdsa==0.19.1 \
--hash=sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 \
--hash=sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61
# via python-jose
email-validator==2.2.0 \
--hash=sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631 \
--hash=sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7
# via pydantic
fastapi==0.116.1 \
--hash=sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565 \
--hash=sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143
# via osu-lazer-api
filelock==3.18.0 \
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
# via virtualenv
greenlet==3.2.3 ; (python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64') \
--hash=sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36 \
--hash=sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892 \
--hash=sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83 \
--hash=sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688 \
--hash=sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d \
--hash=sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5 \
--hash=sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b \
--hash=sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb \
--hash=sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86 \
--hash=sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c \
--hash=sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad \
--hash=sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95 \
--hash=sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3 \
--hash=sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147 \
--hash=sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb \
--hash=sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849 \
--hash=sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba \
--hash=sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822 \
--hash=sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97 \
--hash=sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34 \
--hash=sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141 \
--hash=sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3 \
--hash=sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0 \
--hash=sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b \
--hash=sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365 \
--hash=sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a \
--hash=sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc \
--hash=sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163 \
--hash=sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef \
--hash=sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d \
--hash=sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264 \
--hash=sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b \
--hash=sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf \
--hash=sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a \
--hash=sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728
# via sqlalchemy
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via
# httpcore
# uvicorn
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httptools==0.6.4 \
--hash=sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a \
--hash=sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2 \
--hash=sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17 \
--hash=sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8 \
--hash=sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3 \
--hash=sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5 \
--hash=sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721 \
--hash=sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636 \
--hash=sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0 \
--hash=sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071 \
--hash=sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c \
--hash=sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1 \
--hash=sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44 \
--hash=sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083 \
--hash=sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660 \
--hash=sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988 \
--hash=sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970 \
--hash=sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2 \
--hash=sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81 \
--hash=sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069 \
--hash=sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975 \
--hash=sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f
# via uvicorn
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via osu-lazer-api
identify==2.6.12 \
--hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \
--hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6
# via pre-commit
idna==3.10 \
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
# via
# anyio
# email-validator
# httpx
mako==1.3.10 \
--hash=sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28 \
--hash=sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59
# via alembic
markupsafe==3.0.2 \
--hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \
--hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
--hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \
--hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \
--hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
--hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \
--hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
--hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \
--hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \
--hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \
--hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \
--hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
--hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \
--hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \
--hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \
--hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \
--hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \
--hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
--hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
--hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
--hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \
--hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \
--hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
--hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \
--hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \
--hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
--hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \
--hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \
--hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \
--hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \
--hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \
--hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \
--hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \
--hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \
--hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \
--hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
--hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \
--hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \
--hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \
--hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \
--hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430
# via mako
msgpack==1.1.1 \
--hash=sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8 \
--hash=sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157 \
--hash=sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d \
--hash=sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0 \
--hash=sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5 \
--hash=sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752 \
--hash=sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac \
--hash=sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef \
--hash=sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323 \
--hash=sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4 \
--hash=sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458 \
--hash=sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69 \
--hash=sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce \
--hash=sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558 \
--hash=sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd \
--hash=sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295 \
--hash=sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c \
--hash=sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2 \
--hash=sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f \
--hash=sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9 \
--hash=sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a \
--hash=sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0 \
--hash=sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a \
--hash=sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238 \
--hash=sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7 \
--hash=sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704 \
--hash=sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a \
--hash=sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c \
--hash=sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b \
--hash=sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2 \
--hash=sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b
# via
# msgpack-types
# osu-lazer-api
msgpack-types==0.5.0 \
--hash=sha256:8b633ed75e495a555fa0615843de559a74b1d176828d59bb393d266e51f6bda7 \
--hash=sha256:aebd1b8da23f8f9966d66ebb1a43bd261b95751c6a267bd21a124d2ccac84201
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
# via pre-commit
passlib==1.7.4 \
--hash=sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1 \
--hash=sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04
# via osu-lazer-api
platformdirs==4.3.8 \
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
# via virtualenv
pre-commit==4.2.0 \
--hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \
--hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd
pyasn1==0.6.1 \
--hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \
--hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034
# via
# python-jose
# rsa
pycparser==2.22 ; platform_python_implementation != 'PyPy' \
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
# via cffi
pydantic==2.11.7 \
--hash=sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db \
--hash=sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b
# via
# fastapi
# osu-lazer-api
# sqlmodel
pydantic-core==2.33.2 \
--hash=sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56 \
--hash=sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef \
--hash=sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a \
--hash=sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f \
--hash=sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab \
--hash=sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916 \
--hash=sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf \
--hash=sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a \
--hash=sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7 \
--hash=sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612 \
--hash=sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1 \
--hash=sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7 \
--hash=sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a \
--hash=sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7 \
--hash=sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025 \
--hash=sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849 \
--hash=sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b \
--hash=sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e \
--hash=sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea \
--hash=sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac \
--hash=sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51 \
--hash=sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e \
--hash=sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162 \
--hash=sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65 \
--hash=sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de \
--hash=sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc \
--hash=sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb \
--hash=sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef \
--hash=sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1 \
--hash=sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5 \
--hash=sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88 \
--hash=sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290 \
--hash=sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d \
--hash=sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc \
--hash=sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc \
--hash=sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30 \
--hash=sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e \
--hash=sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9 \
--hash=sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9 \
--hash=sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f \
--hash=sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5 \
--hash=sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab \
--hash=sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593 \
--hash=sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1 \
--hash=sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f \
--hash=sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8 \
--hash=sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf \
--hash=sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246 \
--hash=sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9 \
--hash=sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011 \
--hash=sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6 \
--hash=sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8 \
--hash=sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2 \
--hash=sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6 \
--hash=sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d
# via pydantic
pymysql==1.1.1 \
--hash=sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c \
--hash=sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0
# via aiomysql
python-dotenv==1.1.1 \
--hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \
--hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab
# via
# osu-lazer-api
# uvicorn
python-jose==3.5.0 \
--hash=sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771 \
--hash=sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b
# via osu-lazer-api
python-multipart==0.0.20 \
--hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \
--hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13
# via osu-lazer-api
pyyaml==6.0.2 \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
# via
# pre-commit
# uvicorn
redis==6.2.0 \
--hash=sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e \
--hash=sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977
# via osu-lazer-api
rsa==4.9.1 \
--hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \
--hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75
# via python-jose
ruff==0.12.4 \
--hash=sha256:0618ec4442a83ab545e5b71202a5c0ed7791e8471435b94e655b570a5031a98e \
--hash=sha256:0fc426bec2e4e5f4c4f182b9d2ce6a75c85ba9bcdbe5c6f2a74fcb8df437df4b \
--hash=sha256:13efa16df6c6eeb7d0f091abae50f58e9522f3843edb40d56ad52a5a4a4b6873 \
--hash=sha256:2abc48f3d9667fdc74022380b5c745873499ff827393a636f7a59da1515e7c57 \
--hash=sha256:2b2449dc0c138d877d629bea151bee8c0ae3b8e9c43f5fcaafcd0c0d0726b184 \
--hash=sha256:478fccdb82ca148a98a9ff43658944f7ab5ec41c3c49d77cd99d44da019371a1 \
--hash=sha256:4de27977827893cdfb1211d42d84bc180fceb7b72471104671c59be37041cf93 \
--hash=sha256:55c0f4ca9769408d9b9bac530c30d3e66490bd2beb2d3dae3e4128a1f05c7442 \
--hash=sha256:56e45bb11f625db55f9b70477062e6a1a04d53628eda7784dce6e0f55fd549eb \
--hash=sha256:a7dea966bcb55d4ecc4cc3270bccb6f87a337326c9dcd3c07d5b97000dbff41c \
--hash=sha256:a8224cc3722c9ad9044da7f89c4c1ec452aef2cfe3904365025dd2f51daeae0e \
--hash=sha256:afcfa3ab5ab5dd0e1c39bf286d829e042a15e966b3726eea79528e2e24d8371a \
--hash=sha256:be0593c69df9ad1465e8a2d10e3defd111fdb62dcd5be23ae2c06da77e8fcffb \
--hash=sha256:c057ce464b1413c926cdb203a0f858cd52f3e73dcb3270a3318d1630f6395bb3 \
--hash=sha256:cb0d261dac457ab939aeb247e804125a5d521b21adf27e721895b0d3f83a0d0a \
--hash=sha256:e64b90d1122dc2713330350626b10d60818930819623abbb56535c6466cce045 \
--hash=sha256:e9949d01d64fa3672449a51ddb5d7548b33e130240ad418884ee6efa7a229586 \
--hash=sha256:fe0b9e9eb23736b453143d72d2ceca5db323963330d5b7859d60d101147d461a
six==1.17.0 \
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
# via ecdsa
sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
# via anyio
sqlalchemy==2.0.41 \
--hash=sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582 \
--hash=sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8 \
--hash=sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f \
--hash=sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504 \
--hash=sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae \
--hash=sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443 \
--hash=sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23 \
--hash=sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576 \
--hash=sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1 \
--hash=sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0 \
--hash=sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e \
--hash=sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f \
--hash=sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9 \
--hash=sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df \
--hash=sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6 \
--hash=sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04 \
--hash=sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560 \
--hash=sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70 \
--hash=sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1 \
--hash=sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6 \
--hash=sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078 \
--hash=sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f \
--hash=sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d \
--hash=sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc \
--hash=sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a \
--hash=sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9
# via
# alembic
# osu-lazer-api
# sqlmodel
sqlmodel==0.0.24 \
--hash=sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193 \
--hash=sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423
# via osu-lazer-api
starlette==0.47.2 \
--hash=sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8 \
--hash=sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b
# via fastapi
typing-extensions==4.14.1 \
--hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \
--hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76
# via
# alembic
# anyio
# fastapi
# msgpack-types
# pydantic
# pydantic-core
# sqlalchemy
# starlette
# typing-inspection
typing-inspection==0.4.1 \
--hash=sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51 \
--hash=sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28
# via pydantic
uvicorn==0.35.0 \
--hash=sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a \
--hash=sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01
# via osu-lazer-api
uvloop==0.21.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' \
--hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \
--hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \
--hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \
--hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \
--hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \
--hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \
--hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \
--hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \
--hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \
--hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \
--hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \
--hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \
--hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \
--hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \
--hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \
--hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \
--hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \
--hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \
--hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2
# via uvicorn
virtualenv==20.32.0 \
--hash=sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56 \
--hash=sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0
# via pre-commit
watchfiles==1.1.0 \
--hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \
--hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \
--hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \
--hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \
--hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \
--hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \
--hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \
--hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \
--hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \
--hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \
--hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \
--hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \
--hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \
--hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \
--hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \
--hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \
--hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \
--hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \
--hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \
--hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \
--hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \
--hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \
--hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \
--hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \
--hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \
--hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \
--hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \
--hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \
--hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \
--hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \
--hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \
--hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \
--hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \
--hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \
--hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \
--hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \
--hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \
--hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \
--hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \
--hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \
--hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \
--hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \
--hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \
--hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \
--hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \
--hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \
--hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \
--hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \
--hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \
--hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \
--hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \
--hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \
--hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \
--hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \
--hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \
--hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \
--hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \
--hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \
--hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \
--hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \
--hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \
--hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \
--hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \
--hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \
--hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \
--hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \
--hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \
--hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \
--hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \
--hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \
--hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \
--hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \
--hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \
--hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f
# via uvicorn
websockets==15.0.1 \
--hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \
--hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \
--hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \
--hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \
--hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \
--hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \
--hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \
--hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \
--hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \
--hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \
--hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \
--hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \
--hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \
--hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \
--hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \
--hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \
--hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \
--hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \
--hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \
--hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \
--hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \
--hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \
--hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \
--hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \
--hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \
--hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \
--hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \
--hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \
--hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \
--hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \
--hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \
--hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \
--hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \
--hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \
--hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7
# via uvicorn
aiomysql==0.2.0
alembic==1.16.4
annotated-types==0.7.0
anyio==4.9.0
bcrypt==4.3.0
certifi==2025.7.14
cffi==1.17.1
cfgv==3.4.0
click==8.2.1
cryptography==45.0.5
distlib==0.4.0
dnspython==2.7.0
ecdsa==0.19.1
email-validator==2.2.0
fastapi==0.116.1
filelock==3.18.0
greenlet==3.2.3
h11==0.16.0
httpcore==1.0.9
httptools==0.6.4
httpx==0.28.1
identify==2.6.12
idna==3.10
loguru==0.7.3
mako==1.3.10
markupsafe==3.0.2
maturin==1.9.2
-e file:///workspaces/osu_lazer_api/packages/msgpack_lazer_api
nodeenv==1.9.1
passlib==1.7.4
platformdirs==4.3.8
pre-commit==4.2.0
pyasn1==0.6.1
pycparser==2.22
pydantic==2.11.7
pydantic-core==2.33.2
pymysql==1.1.1
python-dotenv==1.1.1
python-jose==3.5.0
python-multipart==0.0.20
pyyaml==6.0.2
redis==6.2.0
rosu-pp-py==3.1.0
rsa==4.9.1
ruff==0.12.4
six==1.17.0
sniffio==1.3.1
sqlalchemy==2.0.41
sqlmodel==0.0.24
starlette==0.47.2
typing-extensions==4.14.1
typing-inspection==0.4.1
uvicorn==0.35.0
uvloop==0.21.0
virtualenv==20.32.0
watchfiles==1.1.0
websockets==15.0.1

66
uv.lock generated
View File

@@ -2,6 +2,12 @@ version = 1
revision = 2
requires-python = ">=3.12"
[manifest]
members = [
"msgpack-lazer-api",
"osu-lazer-api",
]
[[package]]
name = "aiomysql"
version = "0.2.0"
@@ -448,45 +454,28 @@ wheels = [
]
[[package]]
name = "msgpack"
version = "1.1.1"
name = "maturin"
version = "1.9.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555, upload-time = "2025-06-13T06:52:51.324Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b1/b5/8d9843ba4d2a107ea83499d0fb6758d6d9376a3e2202dbcc5ffa972e2e4a/maturin-1.9.2.tar.gz", hash = "sha256:8b534d3a8acb922fc7a01ec89c12ba950dccdc11b57457c1d4c2661ae24bf96d", size = 211836, upload-time = "2025-07-27T19:18:26.089Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/26/389b9c593eda2b8551b2e7126ad3a06af6f9b44274eb3a4f054d48ff7e47/msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238", size = 82359, upload-time = "2025-06-13T06:52:03.909Z" },
{ url = "https://files.pythonhosted.org/packages/ab/65/7d1de38c8a22cf8b1551469159d4b6cf49be2126adc2482de50976084d78/msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157", size = 79172, upload-time = "2025-06-13T06:52:05.246Z" },
{ url = "https://files.pythonhosted.org/packages/0f/bd/cacf208b64d9577a62c74b677e1ada005caa9b69a05a599889d6fc2ab20a/msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce", size = 425013, upload-time = "2025-06-13T06:52:06.341Z" },
{ url = "https://files.pythonhosted.org/packages/4d/ec/fd869e2567cc9c01278a736cfd1697941ba0d4b81a43e0aa2e8d71dab208/msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a", size = 426905, upload-time = "2025-06-13T06:52:07.501Z" },
{ url = "https://files.pythonhosted.org/packages/55/2a/35860f33229075bce803a5593d046d8b489d7ba2fc85701e714fc1aaf898/msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c", size = 407336, upload-time = "2025-06-13T06:52:09.047Z" },
{ url = "https://files.pythonhosted.org/packages/8c/16/69ed8f3ada150bf92745fb4921bd621fd2cdf5a42e25eb50bcc57a5328f0/msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b", size = 409485, upload-time = "2025-06-13T06:52:10.382Z" },
{ url = "https://files.pythonhosted.org/packages/c6/b6/0c398039e4c6d0b2e37c61d7e0e9d13439f91f780686deb8ee64ecf1ae71/msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef", size = 412182, upload-time = "2025-06-13T06:52:11.644Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d0/0cf4a6ecb9bc960d624c93effaeaae75cbf00b3bc4a54f35c8507273cda1/msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a", size = 419883, upload-time = "2025-06-13T06:52:12.806Z" },
{ url = "https://files.pythonhosted.org/packages/62/83/9697c211720fa71a2dfb632cad6196a8af3abea56eece220fde4674dc44b/msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c", size = 65406, upload-time = "2025-06-13T06:52:14.271Z" },
{ url = "https://files.pythonhosted.org/packages/c0/23/0abb886e80eab08f5e8c485d6f13924028602829f63b8f5fa25a06636628/msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4", size = 72558, upload-time = "2025-06-13T06:52:15.252Z" },
{ url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0", size = 81677, upload-time = "2025-06-13T06:52:16.64Z" },
{ url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9", size = 78603, upload-time = "2025-06-13T06:52:17.843Z" },
{ url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8", size = 420504, upload-time = "2025-06-13T06:52:18.982Z" },
{ url = "https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a", size = 423749, upload-time = "2025-06-13T06:52:20.211Z" },
{ url = "https://files.pythonhosted.org/packages/40/1b/54c08dd5452427e1179a40b4b607e37e2664bca1c790c60c442c8e972e47/msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac", size = 404458, upload-time = "2025-06-13T06:52:21.429Z" },
{ url = "https://files.pythonhosted.org/packages/2e/60/6bb17e9ffb080616a51f09928fdd5cac1353c9becc6c4a8abd4e57269a16/msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b", size = 405976, upload-time = "2025-06-13T06:52:22.995Z" },
{ url = "https://files.pythonhosted.org/packages/ee/97/88983e266572e8707c1f4b99c8fd04f9eb97b43f2db40e3172d87d8642db/msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7", size = 408607, upload-time = "2025-06-13T06:52:24.152Z" },
{ url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172, upload-time = "2025-06-13T06:52:25.704Z" },
{ url = "https://files.pythonhosted.org/packages/8c/87/a75eb622b555708fe0427fab96056d39d4c9892b0c784b3a721088c7ee37/msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323", size = 65347, upload-time = "2025-06-13T06:52:26.846Z" },
{ url = "https://files.pythonhosted.org/packages/ca/91/7dc28d5e2a11a5ad804cf2b7f7a5fcb1eb5a4966d66a5d2b41aee6376543/msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69", size = 72341, upload-time = "2025-06-13T06:52:27.835Z" },
{ url = "https://files.pythonhosted.org/packages/71/db/f20d4ccdfac063bde8c502d6bc6fae10c41ac8e2b1eadbce75ee706d5050/maturin-1.9.2-py3-none-linux_armv6l.whl", hash = "sha256:8f5d448b58c67ba8ef62c066ca02cf154557f2df20791d63e392e08e7721a03b", size = 8271893, upload-time = "2025-07-27T19:18:02.749Z" },
{ url = "https://files.pythonhosted.org/packages/10/c7/abfc2320a5e03d4c9d69bfa1c222f376f44c07374ed7c4c777b75099d265/maturin-1.9.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8dc091e8d98a9f7b8740b151f79685f43f305aa5ec444dbb3e49cacc036eb991", size = 16083703, upload-time = "2025-07-27T19:18:05.857Z" },
{ url = "https://files.pythonhosted.org/packages/c0/8c/2bde4220b74cb3ccf85cd1d8bd69493810540b079690624de0e4539bfa70/maturin-1.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee768507f25da3ae8e18d2fb2e430a44552b3192a40d3ab1eae3f4a67f5792b5", size = 8423450, upload-time = "2025-07-27T19:18:07.885Z" },
{ url = "https://files.pythonhosted.org/packages/39/2c/eccb9705470ac331feb5887293348be3d6a04f37cd611ee2cd2e78339a47/maturin-1.9.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:b7dc9a38089bbd1ac93d9046a9db14a41d011efdb7d6059bb557ebd97ab207e7", size = 8267146, upload-time = "2025-07-27T19:18:09.887Z" },
{ url = "https://files.pythonhosted.org/packages/dc/40/90f43c8334e7a498817188728f758f628a2c2250ab8dcd062ce184303668/maturin-1.9.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:234b8cb12f14173452d71293c7ec86998b2d1c9f247b66023aa8b306d288817c", size = 8792134, upload-time = "2025-07-27T19:18:11.871Z" },
{ url = "https://files.pythonhosted.org/packages/70/44/94d50a59311c9a41af88d96be7d074190d1908709876b1c9b136fb65141b/maturin-1.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0fe1480ac341b9ca733fdbf16a499fd2771e797819de416b5a47846ed0f8a17d", size = 8071546, upload-time = "2025-07-27T19:18:13.91Z" },
{ url = "https://files.pythonhosted.org/packages/f3/8b/3a45dc1dbe4561658923b86a50bb6413bc0e9e56fd5d0f5293c7919dc4c7/maturin-1.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:029572e692af5bac4f58586f294883732b14d41abb3ba83a587a9c68c6824a2a", size = 8132544, upload-time = "2025-07-27T19:18:15.72Z" },
{ url = "https://files.pythonhosted.org/packages/93/f2/56c78ebd3d305a0912b5043b53bbdb627a2d3e2bb027e5e19305fe0ae557/maturin-1.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:58d69b388e52e93f43d25429afccaf4d1b04bb94606d46ddb21f7a97003ec174", size = 10616860, upload-time = "2025-07-27T19:18:17.478Z" },
{ url = "https://files.pythonhosted.org/packages/d3/ab/77c5ef78b069c4a3bda175eafa922537bccebe038735f165ed1a04220748/maturin-1.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:09750de66dce2ae04ad3ac7df1266dfe77abdcf84b7b6cbe43a41cd7e89cf958", size = 8939318, upload-time = "2025-07-27T19:18:19.097Z" },
{ url = "https://files.pythonhosted.org/packages/63/f8/1897ecf4b0115b8df71011d8be3f5aa3a6dde47366d724c064fdb40fa357/maturin-1.9.2-py3-none-win32.whl", hash = "sha256:b2b12e88f5e3145cc4d749a1fb8958a83842f647c459112e9a8d75285521798f", size = 7276436, upload-time = "2025-07-27T19:18:21.318Z" },
{ url = "https://files.pythonhosted.org/packages/14/4f/437d363c4ee97bd9b88d68ff432067aaf07b10ffa0fa0bad3329d2567919/maturin-1.9.2-py3-none-win_amd64.whl", hash = "sha256:1df15e896b3a05396a91ec2e60295bd70075f984736b8c84e8b14941bcba40c8", size = 8283346, upload-time = "2025-07-27T19:18:22.652Z" },
{ url = "https://files.pythonhosted.org/packages/60/04/946552db9cfa831cee2df1bf3c857b06a8ed04f63ded9a3692dd6f88bdee/maturin-1.9.2-py3-none-win_arm64.whl", hash = "sha256:95a08e849fda883e1a961218b7834af6f128eb9fc9ace4b90b65031da6028251", size = 6942238, upload-time = "2025-07-27T19:18:24.432Z" },
]
[[package]]
name = "msgpack-types"
version = "0.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "msgpack" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/26/a15707f2af5681333cd598724bedd1948844ac2af45eafc4175af0671a8d/msgpack_types-0.5.0.tar.gz", hash = "sha256:aebd1b8da23f8f9966d66ebb1a43bd261b95751c6a267bd21a124d2ccac84201", size = 6702, upload-time = "2024-09-21T13:55:05.587Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/dd/cd9d2b0ef506f6164cd81d4e92e408095041f28523d751b9f7dabdc244eb/msgpack_types-0.5.0-py3-none-any.whl", hash = "sha256:8b633ed75e495a555fa0615843de559a74b1d176828d59bb393d266e51f6bda7", size = 8182, upload-time = "2024-09-21T13:55:04.232Z" },
]
name = "msgpack-lazer-api"
source = { editable = "packages/msgpack_lazer_api" }
[[package]]
name = "nodeenv"
@@ -509,7 +498,7 @@ dependencies = [
{ name = "fastapi" },
{ name = "httpx" },
{ name = "loguru" },
{ name = "msgpack" },
{ name = "msgpack-lazer-api" },
{ name = "passlib", extra = ["bcrypt"] },
{ name = "pydantic", extra = ["email"] },
{ name = "python-dotenv" },
@@ -524,7 +513,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "msgpack-types" },
{ name = "maturin" },
{ name = "pre-commit" },
{ name = "ruff" },
]
@@ -538,7 +527,7 @@ requires-dist = [
{ name = "fastapi", specifier = ">=0.104.1" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "msgpack", specifier = ">=1.1.1" },
{ name = "msgpack-lazer-api", editable = "packages/msgpack_lazer_api" },
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.5.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
@@ -553,11 +542,10 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "msgpack-types", specifier = ">=0.5.0" },
{ name = "maturin", specifier = ">=1.9.2" },
{ name = "pre-commit", specifier = ">=4.2.0" },
{ name = "ruff", specifier = ">=0.12.4" },
]
migration = []
[[package]]
name = "passlib"