refactor(signalr): remove SignalR server & msgpack_lazer_api
Maybe we can make `msgpack_lazer_api` independent?
This commit is contained in:
@@ -107,6 +107,6 @@
|
|||||||
80,
|
80,
|
||||||
8080
|
8080
|
||||||
],
|
],
|
||||||
"postCreateCommand": "uv sync --dev && uv run alembic upgrade head && uv run pre-commit install && cd packages/msgpack_lazer_api && cargo check && cd ../../spectator-server && dotnet restore",
|
"postCreateCommand": "uv sync --dev && uv run alembic upgrade head && uv run pre-commit install && cd spectator-server && dotnet restore",
|
||||||
"remoteUser": "vscode"
|
"remoteUser": "vscode"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,18 +266,6 @@ STORAGE_SETTINGS='{
|
|||||||
else:
|
else:
|
||||||
return "/"
|
return "/"
|
||||||
|
|
||||||
# SignalR 设置
|
|
||||||
signalr_negotiate_timeout: Annotated[
|
|
||||||
int,
|
|
||||||
Field(default=30, description="SignalR 协商超时时间(秒)"),
|
|
||||||
"SignalR 服务器设置",
|
|
||||||
]
|
|
||||||
signalr_ping_interval: Annotated[
|
|
||||||
int,
|
|
||||||
Field(default=15, description="SignalR ping 间隔(秒)"),
|
|
||||||
"SignalR 服务器设置",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Fetcher 设置
|
# Fetcher 设置
|
||||||
fetcher_client_id: Annotated[
|
fetcher_client_id: Annotated[
|
||||||
str,
|
str,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from app.models.model import UTCBaseModel
|
from app.models.model import UTCBaseModel
|
||||||
from app.models.mods import APIMod
|
from app.models.mods import APIMod
|
||||||
|
from app.models.playlist import PlaylistItem
|
||||||
|
|
||||||
from .beatmap import Beatmap, BeatmapResp
|
from .beatmap import Beatmap, BeatmapResp
|
||||||
|
|
||||||
@@ -21,8 +22,6 @@ from sqlmodel import (
|
|||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from app.models.multiplayer_hub import PlaylistItem
|
|
||||||
|
|
||||||
from .room import Room
|
from .room import Room
|
||||||
|
|
||||||
|
|
||||||
@@ -73,7 +72,7 @@ class Playlist(PlaylistBase, table=True):
|
|||||||
return result.one()
|
return result.one()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_hub(cls, playlist: "PlaylistItem", room_id: int, session: AsyncSession) -> "Playlist":
|
async def from_model(cls, playlist: PlaylistItem, room_id: int, session: AsyncSession) -> "Playlist":
|
||||||
next_id = await cls.get_next_id_for_room(room_id, session=session)
|
next_id = await cls.get_next_id_for_room(room_id, session=session)
|
||||||
return cls(
|
return cls(
|
||||||
id=next_id,
|
id=next_id,
|
||||||
@@ -90,7 +89,7 @@ class Playlist(PlaylistBase, table=True):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def update(cls, playlist: "PlaylistItem", room_id: int, session: AsyncSession):
|
async def update(cls, playlist: PlaylistItem, room_id: int, session: AsyncSession):
|
||||||
db_playlist = await session.exec(select(cls).where(cls.id == playlist.id, cls.room_id == room_id))
|
db_playlist = await session.exec(select(cls).where(cls.id == playlist.id, cls.room_id == room_id))
|
||||||
db_playlist = db_playlist.first()
|
db_playlist = db_playlist.first()
|
||||||
if db_playlist is None:
|
if db_playlist is None:
|
||||||
@@ -107,8 +106,8 @@ class Playlist(PlaylistBase, table=True):
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def add_to_db(cls, playlist: "PlaylistItem", room_id: int, session: AsyncSession):
|
async def add_to_db(cls, playlist: PlaylistItem, room_id: int, session: AsyncSession):
|
||||||
db_playlist = await cls.from_hub(playlist, room_id, session)
|
db_playlist = await cls.from_model(playlist, room_id, session)
|
||||||
session.add(db_playlist)
|
session.add(db_playlist)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(db_playlist)
|
await session.refresh(db_playlist)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from app.database.item_attempts_count import PlaylistAggregateScore
|
from app.database.item_attempts_count import PlaylistAggregateScore
|
||||||
from app.database.room_participated_user import RoomParticipatedUser
|
from app.database.room_participated_user import RoomParticipatedUser
|
||||||
@@ -32,9 +31,6 @@ from sqlmodel import (
|
|||||||
)
|
)
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from app.models.multiplayer_hub import ServerMultiplayerRoom
|
|
||||||
|
|
||||||
|
|
||||||
class RoomBase(SQLModel, UTCBaseModel):
|
class RoomBase(SQLModel, UTCBaseModel):
|
||||||
name: str = Field(index=True)
|
name: str = Field(index=True)
|
||||||
@@ -163,25 +159,6 @@ class RoomResp(RoomBase):
|
|||||||
resp.current_user_score = await PlaylistAggregateScore.from_db(room.id, user.id, session)
|
resp.current_user_score = await PlaylistAggregateScore.from_db(room.id, user.id, session)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def from_hub(cls, server_room: "ServerMultiplayerRoom") -> "RoomResp":
|
|
||||||
room = server_room.room
|
|
||||||
resp = cls(
|
|
||||||
id=room.room_id,
|
|
||||||
name=room.settings.name,
|
|
||||||
type=room.settings.match_type,
|
|
||||||
queue_mode=room.settings.queue_mode,
|
|
||||||
auto_skip=room.settings.auto_skip,
|
|
||||||
auto_start_duration=int(room.settings.auto_start_duration.total_seconds()),
|
|
||||||
status=server_room.status,
|
|
||||||
category=server_room.category,
|
|
||||||
# duration = room.settings.duration,
|
|
||||||
starts_at=server_room.start_at,
|
|
||||||
participant_count=len(room.users),
|
|
||||||
channel_id=server_room.room.channel_id or 0,
|
|
||||||
)
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
class APIUploadedRoom(RoomBase):
|
class APIUploadedRoom(RoomBase):
|
||||||
def to_room(self) -> Room:
|
def to_room(self) -> Room:
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
class SignalRException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InvokeException(SignalRException):
|
|
||||||
def __init__(self, message: str) -> None:
|
|
||||||
self.message = message
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
"""
|
|
||||||
会话验证接口
|
|
||||||
|
|
||||||
基于osu-web的SessionVerificationInterface实现
|
|
||||||
用于标准化会话验证行为
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
|
||||||
class SessionVerificationInterface(ABC):
|
|
||||||
"""会话验证接口
|
|
||||||
|
|
||||||
定义了会话验证所需的基本操作,参考osu-web的实现
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@abstractmethod
|
|
||||||
async def find_for_verification(cls, session_id: str) -> SessionVerificationInterface | None:
|
|
||||||
"""根据会话ID查找会话用于验证
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session_id: 会话ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
会话实例或None
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_key(self) -> str:
|
|
||||||
"""获取会话密钥/ID"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_key_for_event(self) -> str:
|
|
||||||
"""获取用于事件广播的会话密钥"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_verification_method(self) -> str | None:
|
|
||||||
"""获取当前验证方法
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
验证方法 ('totp', 'mail') 或 None
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def is_verified(self) -> bool:
|
|
||||||
"""检查会话是否已验证"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def mark_verified(self) -> None:
|
|
||||||
"""标记会话为已验证"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def set_verification_method(self, method: str) -> None:
|
|
||||||
"""设置验证方法
|
|
||||||
|
|
||||||
Args:
|
|
||||||
method: 验证方法 ('totp', 'mail')
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def user_id(self) -> int | None:
|
|
||||||
"""获取关联的用户ID"""
|
|
||||||
pass
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from enum import IntEnum
|
|
||||||
from typing import ClassVar, Literal
|
|
||||||
|
|
||||||
from app.models.signalr import SignalRUnionMessage, UserState
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
TOTAL_SCORE_DISTRIBUTION_BINS = 13
|
|
||||||
|
|
||||||
|
|
||||||
class _UserActivity(SignalRUnionMessage): ...
|
|
||||||
|
|
||||||
|
|
||||||
class ChoosingBeatmap(_UserActivity):
|
|
||||||
union_type: ClassVar[Literal[11]] = 11
|
|
||||||
|
|
||||||
|
|
||||||
class _InGame(_UserActivity):
|
|
||||||
beatmap_id: int
|
|
||||||
beatmap_display_title: str
|
|
||||||
ruleset_id: int
|
|
||||||
ruleset_playing_verb: str
|
|
||||||
|
|
||||||
|
|
||||||
class InSoloGame(_InGame):
|
|
||||||
union_type: ClassVar[Literal[12]] = 12
|
|
||||||
|
|
||||||
|
|
||||||
class InMultiplayerGame(_InGame):
|
|
||||||
union_type: ClassVar[Literal[23]] = 23
|
|
||||||
|
|
||||||
|
|
||||||
class SpectatingMultiplayerGame(_InGame):
|
|
||||||
union_type: ClassVar[Literal[24]] = 24
|
|
||||||
|
|
||||||
|
|
||||||
class InPlaylistGame(_InGame):
|
|
||||||
union_type: ClassVar[Literal[31]] = 31
|
|
||||||
|
|
||||||
|
|
||||||
class PlayingDailyChallenge(_InGame):
|
|
||||||
union_type: ClassVar[Literal[52]] = 52
|
|
||||||
|
|
||||||
|
|
||||||
class EditingBeatmap(_UserActivity):
|
|
||||||
union_type: ClassVar[Literal[41]] = 41
|
|
||||||
beatmap_id: int
|
|
||||||
beatmap_display_title: str
|
|
||||||
|
|
||||||
|
|
||||||
class TestingBeatmap(EditingBeatmap):
|
|
||||||
union_type: ClassVar[Literal[43]] = 43
|
|
||||||
|
|
||||||
|
|
||||||
class ModdingBeatmap(EditingBeatmap):
|
|
||||||
union_type: ClassVar[Literal[42]] = 42
|
|
||||||
|
|
||||||
|
|
||||||
class WatchingReplay(_UserActivity):
|
|
||||||
union_type: ClassVar[Literal[13]] = 13
|
|
||||||
score_id: int
|
|
||||||
player_name: str
|
|
||||||
beatmap_id: int
|
|
||||||
beatmap_display_title: str
|
|
||||||
|
|
||||||
|
|
||||||
class SpectatingUser(WatchingReplay):
|
|
||||||
union_type: ClassVar[Literal[14]] = 14
|
|
||||||
|
|
||||||
|
|
||||||
class SearchingForLobby(_UserActivity):
|
|
||||||
union_type: ClassVar[Literal[21]] = 21
|
|
||||||
|
|
||||||
|
|
||||||
class InLobby(_UserActivity):
|
|
||||||
union_type: ClassVar[Literal[22]] = 22
|
|
||||||
room_id: int
|
|
||||||
room_name: str
|
|
||||||
|
|
||||||
|
|
||||||
class InDailyChallengeLobby(_UserActivity):
|
|
||||||
union_type: ClassVar[Literal[51]] = 51
|
|
||||||
|
|
||||||
|
|
||||||
UserActivity = (
|
|
||||||
ChoosingBeatmap
|
|
||||||
| InSoloGame
|
|
||||||
| WatchingReplay
|
|
||||||
| SpectatingUser
|
|
||||||
| SearchingForLobby
|
|
||||||
| InLobby
|
|
||||||
| InMultiplayerGame
|
|
||||||
| SpectatingMultiplayerGame
|
|
||||||
| InPlaylistGame
|
|
||||||
| EditingBeatmap
|
|
||||||
| ModdingBeatmap
|
|
||||||
| TestingBeatmap
|
|
||||||
| InDailyChallengeLobby
|
|
||||||
| PlayingDailyChallenge
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserPresence(BaseModel):
|
|
||||||
activity: UserActivity | None = None
|
|
||||||
|
|
||||||
status: OnlineStatus | None = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pushable(self) -> bool:
|
|
||||||
return self.status is not None and self.status != OnlineStatus.OFFLINE
|
|
||||||
|
|
||||||
@property
|
|
||||||
def for_push(self) -> "UserPresence | None":
|
|
||||||
return UserPresence(
|
|
||||||
activity=self.activity,
|
|
||||||
status=self.status,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataClientState(UserPresence, UserState): ...
|
|
||||||
|
|
||||||
|
|
||||||
class OnlineStatus(IntEnum):
|
|
||||||
OFFLINE = 0 # 隐身
|
|
||||||
DO_NOT_DISTURB = 1
|
|
||||||
ONLINE = 2
|
|
||||||
|
|
||||||
|
|
||||||
class DailyChallengeInfo(BaseModel):
|
|
||||||
room_id: int
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerPlaylistItemStats(BaseModel):
|
|
||||||
playlist_item_id: int = 0
|
|
||||||
total_score_distribution: list[int] = Field(
|
|
||||||
default_factory=list,
|
|
||||||
min_length=TOTAL_SCORE_DISTRIBUTION_BINS,
|
|
||||||
max_length=TOTAL_SCORE_DISTRIBUTION_BINS,
|
|
||||||
)
|
|
||||||
cumulative_score: int = 0
|
|
||||||
last_processed_score_id: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerRoomStats(BaseModel):
|
|
||||||
room_id: int
|
|
||||||
playlist_item_stats: dict[int, MultiplayerPlaylistItemStats] = Field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerRoomScoreSetEvent(BaseModel):
|
|
||||||
room_id: int
|
|
||||||
playlist_item_id: int
|
|
||||||
score_id: int
|
|
||||||
user_id: int
|
|
||||||
total_score: int
|
|
||||||
new_rank: int | None = None
|
|
||||||
|
|||||||
@@ -1,840 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
import asyncio
|
|
||||||
from collections.abc import Awaitable, Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from enum import IntEnum
|
|
||||||
from typing import (
|
|
||||||
TYPE_CHECKING,
|
|
||||||
Annotated,
|
|
||||||
Any,
|
|
||||||
ClassVar,
|
|
||||||
Literal,
|
|
||||||
TypedDict,
|
|
||||||
cast,
|
|
||||||
override,
|
|
||||||
)
|
|
||||||
|
|
||||||
from app.database.beatmap import Beatmap
|
|
||||||
from app.dependencies.database import with_db
|
|
||||||
from app.dependencies.fetcher import get_fetcher
|
|
||||||
from app.exception import InvokeException
|
|
||||||
from app.utils import utcnow
|
|
||||||
|
|
||||||
from .mods import API_MODS, APIMod
|
|
||||||
from .room import (
|
|
||||||
DownloadState,
|
|
||||||
MatchType,
|
|
||||||
MultiplayerRoomState,
|
|
||||||
MultiplayerUserState,
|
|
||||||
QueueMode,
|
|
||||||
RoomCategory,
|
|
||||||
RoomStatus,
|
|
||||||
)
|
|
||||||
from .signalr import (
|
|
||||||
SignalRMeta,
|
|
||||||
SignalRUnionMessage,
|
|
||||||
UserState,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from sqlalchemy import update
|
|
||||||
from sqlmodel import col
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from app.database.room import Room
|
|
||||||
from app.signalr.hub import MultiplayerHub
|
|
||||||
|
|
||||||
HOST_LIMIT = 50
|
|
||||||
PER_USER_LIMIT = 3
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerClientState(UserState):
|
|
||||||
room_id: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerRoomSettings(BaseModel):
|
|
||||||
name: str = "Unnamed Room"
|
|
||||||
playlist_item_id: Annotated[int, Field(default=0), SignalRMeta(use_abbr=False)]
|
|
||||||
password: str = ""
|
|
||||||
match_type: MatchType = MatchType.HEAD_TO_HEAD
|
|
||||||
queue_mode: QueueMode = QueueMode.HOST_ONLY
|
|
||||||
auto_start_duration: timedelta = timedelta(seconds=0)
|
|
||||||
auto_skip: bool = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auto_start_enabled(self) -> bool:
|
|
||||||
return self.auto_start_duration != timedelta(seconds=0)
|
|
||||||
|
|
||||||
|
|
||||||
class BeatmapAvailability(BaseModel):
|
|
||||||
state: DownloadState = DownloadState.UNKNOWN
|
|
||||||
download_progress: float | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class _MatchUserState(SignalRUnionMessage): ...
|
|
||||||
|
|
||||||
|
|
||||||
class TeamVersusUserState(_MatchUserState):
|
|
||||||
team_id: int
|
|
||||||
|
|
||||||
union_type: ClassVar[Literal[0]] = 0
|
|
||||||
|
|
||||||
|
|
||||||
MatchUserState = TeamVersusUserState
|
|
||||||
|
|
||||||
|
|
||||||
class _MatchRoomState(SignalRUnionMessage): ...
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerTeam(BaseModel):
|
|
||||||
id: int
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
class TeamVersusRoomState(_MatchRoomState):
|
|
||||||
teams: list[MultiplayerTeam] = Field(
|
|
||||||
default_factory=lambda: [
|
|
||||||
MultiplayerTeam(id=0, name="Team Red"),
|
|
||||||
MultiplayerTeam(id=1, name="Team Blue"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
union_type: ClassVar[Literal[0]] = 0
|
|
||||||
|
|
||||||
|
|
||||||
MatchRoomState = TeamVersusRoomState
|
|
||||||
|
|
||||||
|
|
||||||
class PlaylistItem(BaseModel):
|
|
||||||
id: Annotated[int, Field(default=0), SignalRMeta(use_abbr=False)]
|
|
||||||
owner_id: int
|
|
||||||
beatmap_id: int
|
|
||||||
beatmap_checksum: str
|
|
||||||
ruleset_id: int
|
|
||||||
required_mods: list[APIMod] = Field(default_factory=list)
|
|
||||||
allowed_mods: list[APIMod] = Field(default_factory=list)
|
|
||||||
expired: bool
|
|
||||||
playlist_order: int
|
|
||||||
played_at: datetime | None = None
|
|
||||||
star_rating: float
|
|
||||||
freestyle: bool
|
|
||||||
|
|
||||||
def _validate_mod_for_ruleset(self, mod: APIMod, ruleset_key: int, context: str = "mod") -> None:
|
|
||||||
typed_ruleset_key = cast(Literal[0, 1, 2, 3], ruleset_key)
|
|
||||||
|
|
||||||
# Check if mod is valid for ruleset
|
|
||||||
if typed_ruleset_key not in API_MODS or mod["acronym"] not in API_MODS[typed_ruleset_key]:
|
|
||||||
raise InvokeException(f"{context} {mod['acronym']} is invalid for this ruleset")
|
|
||||||
|
|
||||||
mod_settings = API_MODS[typed_ruleset_key][mod["acronym"]]
|
|
||||||
|
|
||||||
# Check if mod is unplayable in multiplayer
|
|
||||||
if mod_settings.get("UserPlayable", True) is False:
|
|
||||||
raise InvokeException(f"{context} {mod['acronym']} is not playable by users")
|
|
||||||
|
|
||||||
if mod_settings.get("ValidForMultiplayer", True) is False:
|
|
||||||
raise InvokeException(f"{context} {mod['acronym']} is not valid for multiplayer")
|
|
||||||
|
|
||||||
def _check_mod_compatibility(self, mods: list[APIMod], ruleset_key: int) -> None:
|
|
||||||
from typing import Literal, cast
|
|
||||||
|
|
||||||
typed_ruleset_key = cast(Literal[0, 1, 2, 3], ruleset_key)
|
|
||||||
|
|
||||||
for i, mod1 in enumerate(mods):
|
|
||||||
mod1_settings = API_MODS[typed_ruleset_key].get(mod1["acronym"])
|
|
||||||
if mod1_settings:
|
|
||||||
incompatible = set(mod1_settings.get("IncompatibleMods", []))
|
|
||||||
for mod2 in mods[i + 1 :]:
|
|
||||||
if mod2["acronym"] in incompatible:
|
|
||||||
raise InvokeException(f"Mods {mod1['acronym']} and {mod2['acronym']} are incompatible")
|
|
||||||
|
|
||||||
def _check_required_allowed_compatibility(self, ruleset_key: int) -> None:
|
|
||||||
from typing import Literal, cast
|
|
||||||
|
|
||||||
typed_ruleset_key = cast(Literal[0, 1, 2, 3], ruleset_key)
|
|
||||||
allowed_acronyms = {mod["acronym"] for mod in self.allowed_mods}
|
|
||||||
|
|
||||||
for req_mod in self.required_mods:
|
|
||||||
req_acronym = req_mod["acronym"]
|
|
||||||
req_settings = API_MODS[typed_ruleset_key].get(req_acronym)
|
|
||||||
if req_settings:
|
|
||||||
incompatible = set(req_settings.get("IncompatibleMods", []))
|
|
||||||
conflicting_allowed = allowed_acronyms & incompatible
|
|
||||||
if conflicting_allowed:
|
|
||||||
conflict_list = ", ".join(conflicting_allowed)
|
|
||||||
raise InvokeException(f"Required mod {req_acronym} conflicts with allowed mods: {conflict_list}")
|
|
||||||
|
|
||||||
def validate_playlist_item_mods(self) -> None:
|
|
||||||
ruleset_key = cast(Literal[0, 1, 2, 3], self.ruleset_id)
|
|
||||||
|
|
||||||
# Validate required mods
|
|
||||||
for mod in self.required_mods:
|
|
||||||
self._validate_mod_for_ruleset(mod, ruleset_key, "Required mod")
|
|
||||||
|
|
||||||
# Validate allowed mods
|
|
||||||
for mod in self.allowed_mods:
|
|
||||||
self._validate_mod_for_ruleset(mod, ruleset_key, "Allowed mod")
|
|
||||||
|
|
||||||
# Check internal compatibility of required mods
|
|
||||||
self._check_mod_compatibility(self.required_mods, ruleset_key)
|
|
||||||
|
|
||||||
# Check compatibility between required and allowed mods
|
|
||||||
self._check_required_allowed_compatibility(ruleset_key)
|
|
||||||
|
|
||||||
def validate_user_mods(
|
|
||||||
self,
|
|
||||||
user: "MultiplayerRoomUser",
|
|
||||||
proposed_mods: list[APIMod],
|
|
||||||
) -> tuple[bool, list[APIMod]]:
|
|
||||||
"""
|
|
||||||
Validates user mods against playlist item rules and returns valid mods.
|
|
||||||
Returns (is_valid, valid_mods).
|
|
||||||
"""
|
|
||||||
from typing import Literal, cast
|
|
||||||
|
|
||||||
ruleset_id = user.ruleset_id if user.ruleset_id is not None else self.ruleset_id
|
|
||||||
ruleset_key = cast(Literal[0, 1, 2, 3], ruleset_id)
|
|
||||||
|
|
||||||
valid_mods = []
|
|
||||||
all_proposed_valid = True
|
|
||||||
|
|
||||||
# Check if mods are valid for the ruleset
|
|
||||||
for mod in proposed_mods:
|
|
||||||
if ruleset_key not in API_MODS or mod["acronym"] not in API_MODS[ruleset_key]:
|
|
||||||
all_proposed_valid = False
|
|
||||||
continue
|
|
||||||
valid_mods.append(mod)
|
|
||||||
|
|
||||||
# Check mod compatibility within user mods
|
|
||||||
incompatible_mods = set()
|
|
||||||
final_valid_mods = []
|
|
||||||
for mod in valid_mods:
|
|
||||||
if mod["acronym"] in incompatible_mods:
|
|
||||||
all_proposed_valid = False
|
|
||||||
continue
|
|
||||||
setting_mods = API_MODS[ruleset_key].get(mod["acronym"])
|
|
||||||
if setting_mods:
|
|
||||||
incompatible_mods.update(setting_mods["IncompatibleMods"])
|
|
||||||
final_valid_mods.append(mod)
|
|
||||||
|
|
||||||
# If not freestyle, check against allowed mods
|
|
||||||
if not self.freestyle:
|
|
||||||
allowed_acronyms = {mod["acronym"] for mod in self.allowed_mods}
|
|
||||||
filtered_valid_mods = []
|
|
||||||
for mod in final_valid_mods:
|
|
||||||
if mod["acronym"] not in allowed_acronyms:
|
|
||||||
all_proposed_valid = False
|
|
||||||
else:
|
|
||||||
filtered_valid_mods.append(mod)
|
|
||||||
final_valid_mods = filtered_valid_mods
|
|
||||||
|
|
||||||
# Check compatibility with required mods
|
|
||||||
required_mod_acronyms = {mod["acronym"] for mod in self.required_mods}
|
|
||||||
all_mod_acronyms = {mod["acronym"] for mod in final_valid_mods} | required_mod_acronyms
|
|
||||||
|
|
||||||
# Check for incompatibility between required and user mods
|
|
||||||
filtered_valid_mods = []
|
|
||||||
for mod in final_valid_mods:
|
|
||||||
mod_acronym = mod["acronym"]
|
|
||||||
is_compatible = True
|
|
||||||
|
|
||||||
for other_acronym in all_mod_acronyms:
|
|
||||||
if other_acronym == mod_acronym:
|
|
||||||
continue
|
|
||||||
setting_mods = API_MODS[ruleset_key].get(mod_acronym)
|
|
||||||
if setting_mods and other_acronym in setting_mods["IncompatibleMods"]:
|
|
||||||
is_compatible = False
|
|
||||||
all_proposed_valid = False
|
|
||||||
break
|
|
||||||
|
|
||||||
if is_compatible:
|
|
||||||
filtered_valid_mods.append(mod)
|
|
||||||
|
|
||||||
return all_proposed_valid, filtered_valid_mods
|
|
||||||
|
|
||||||
def clone(self) -> "PlaylistItem":
|
|
||||||
copy = self.model_copy()
|
|
||||||
copy.required_mods = list(self.required_mods)
|
|
||||||
copy.allowed_mods = list(self.allowed_mods)
|
|
||||||
copy.expired = False
|
|
||||||
copy.played_at = None
|
|
||||||
return copy
|
|
||||||
|
|
||||||
|
|
||||||
class _MultiplayerCountdown(SignalRUnionMessage):
|
|
||||||
id: int = 0
|
|
||||||
time_remaining: timedelta
|
|
||||||
is_exclusive: Annotated[bool, Field(default=True), SignalRMeta(member_ignore=True)] = True
|
|
||||||
|
|
||||||
|
|
||||||
class MatchStartCountdown(_MultiplayerCountdown):
|
|
||||||
union_type: ClassVar[Literal[0]] = 0
|
|
||||||
|
|
||||||
|
|
||||||
class ForceGameplayStartCountdown(_MultiplayerCountdown):
|
|
||||||
union_type: ClassVar[Literal[1]] = 1
|
|
||||||
|
|
||||||
|
|
||||||
class ServerShuttingDownCountdown(_MultiplayerCountdown):
|
|
||||||
union_type: ClassVar[Literal[2]] = 2
|
|
||||||
|
|
||||||
|
|
||||||
MultiplayerCountdown = MatchStartCountdown | ForceGameplayStartCountdown | ServerShuttingDownCountdown
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerRoomUser(BaseModel):
|
|
||||||
user_id: int
|
|
||||||
state: MultiplayerUserState = MultiplayerUserState.IDLE
|
|
||||||
availability: BeatmapAvailability = BeatmapAvailability(state=DownloadState.UNKNOWN, download_progress=None)
|
|
||||||
mods: list[APIMod] = Field(default_factory=list)
|
|
||||||
match_state: MatchUserState | None = None
|
|
||||||
ruleset_id: int | None = None # freestyle
|
|
||||||
beatmap_id: int | None = None # freestyle
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerRoom(BaseModel):
|
|
||||||
room_id: int
|
|
||||||
state: MultiplayerRoomState
|
|
||||||
settings: MultiplayerRoomSettings
|
|
||||||
users: list[MultiplayerRoomUser] = Field(default_factory=list)
|
|
||||||
host: MultiplayerRoomUser | None = None
|
|
||||||
match_state: MatchRoomState | None = None
|
|
||||||
playlist: list[PlaylistItem] = Field(default_factory=list)
|
|
||||||
active_countdowns: list[MultiplayerCountdown] = Field(default_factory=list)
|
|
||||||
channel_id: int
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_db(cls, room: "Room") -> "MultiplayerRoom":
|
|
||||||
"""
|
|
||||||
将 Room (数据库模型) 转换为 MultiplayerRoom (业务模型)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 用户列表
|
|
||||||
users = [MultiplayerRoomUser(user_id=room.host_id)]
|
|
||||||
host_user = MultiplayerRoomUser(user_id=room.host_id)
|
|
||||||
# playlist 转换
|
|
||||||
playlist = []
|
|
||||||
if room.playlist:
|
|
||||||
for item in room.playlist:
|
|
||||||
playlist.append(
|
|
||||||
PlaylistItem(
|
|
||||||
id=item.id,
|
|
||||||
owner_id=item.owner_id,
|
|
||||||
beatmap_id=item.beatmap_id,
|
|
||||||
beatmap_checksum=item.beatmap.checksum if item.beatmap else "",
|
|
||||||
ruleset_id=item.ruleset_id,
|
|
||||||
required_mods=item.required_mods,
|
|
||||||
allowed_mods=item.allowed_mods,
|
|
||||||
expired=item.expired,
|
|
||||||
playlist_order=item.playlist_order,
|
|
||||||
played_at=item.played_at,
|
|
||||||
star_rating=item.beatmap.difficulty_rating if item.beatmap is not None else 0.0,
|
|
||||||
freestyle=item.freestyle,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return cls(
|
|
||||||
room_id=room.id,
|
|
||||||
state=getattr(room, "state", MultiplayerRoomState.OPEN),
|
|
||||||
settings=MultiplayerRoomSettings(
|
|
||||||
name=room.name,
|
|
||||||
playlist_item_id=playlist[0].id if playlist else 0,
|
|
||||||
password=getattr(room, "password", ""),
|
|
||||||
match_type=room.type,
|
|
||||||
queue_mode=room.queue_mode,
|
|
||||||
auto_start_duration=timedelta(seconds=room.auto_start_duration),
|
|
||||||
auto_skip=room.auto_skip,
|
|
||||||
),
|
|
||||||
users=users,
|
|
||||||
host=host_user,
|
|
||||||
match_state=None,
|
|
||||||
playlist=playlist,
|
|
||||||
active_countdowns=[],
|
|
||||||
channel_id=room.channel_id or 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerQueue:
|
|
||||||
def __init__(self, room: "ServerMultiplayerRoom"):
|
|
||||||
self.server_room = room
|
|
||||||
self.current_index = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hub(self) -> "MultiplayerHub":
|
|
||||||
return self.server_room.hub
|
|
||||||
|
|
||||||
@property
|
|
||||||
def upcoming_items(self):
|
|
||||||
return sorted(
|
|
||||||
(item for item in self.room.playlist if not item.expired),
|
|
||||||
key=lambda i: i.playlist_order,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def room(self):
|
|
||||||
return self.server_room.room
|
|
||||||
|
|
||||||
async def update_order(self):
|
|
||||||
from app.database import Playlist
|
|
||||||
|
|
||||||
match self.room.settings.queue_mode:
|
|
||||||
case QueueMode.ALL_PLAYERS_ROUND_ROBIN:
|
|
||||||
ordered_active_items = []
|
|
||||||
|
|
||||||
is_first_set = True
|
|
||||||
first_set_order_by_user_id = {}
|
|
||||||
|
|
||||||
active_items = [item for item in self.room.playlist if not item.expired]
|
|
||||||
active_items.sort(key=lambda x: x.id)
|
|
||||||
|
|
||||||
user_item_groups = {}
|
|
||||||
for item in active_items:
|
|
||||||
if item.owner_id not in user_item_groups:
|
|
||||||
user_item_groups[item.owner_id] = []
|
|
||||||
user_item_groups[item.owner_id].append(item)
|
|
||||||
|
|
||||||
max_items = max((len(items) for items in user_item_groups.values()), default=0)
|
|
||||||
|
|
||||||
for i in range(max_items):
|
|
||||||
current_set = []
|
|
||||||
for user_id, items in user_item_groups.items():
|
|
||||||
if i < len(items):
|
|
||||||
current_set.append(items[i])
|
|
||||||
|
|
||||||
if is_first_set:
|
|
||||||
current_set.sort(key=lambda item: (item.playlist_order, item.id))
|
|
||||||
ordered_active_items.extend(current_set)
|
|
||||||
first_set_order_by_user_id = {
|
|
||||||
item.owner_id: idx for idx, item in enumerate(ordered_active_items)
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
current_set.sort(key=lambda item: first_set_order_by_user_id.get(item.owner_id, 0))
|
|
||||||
ordered_active_items.extend(current_set)
|
|
||||||
|
|
||||||
is_first_set = False
|
|
||||||
case _:
|
|
||||||
ordered_active_items = sorted(
|
|
||||||
(item for item in self.room.playlist if not item.expired),
|
|
||||||
key=lambda x: x.id,
|
|
||||||
)
|
|
||||||
async with with_db() as session:
|
|
||||||
for idx, item in enumerate(ordered_active_items):
|
|
||||||
if item.playlist_order == idx:
|
|
||||||
continue
|
|
||||||
item.playlist_order = idx
|
|
||||||
await Playlist.update(item, self.room.room_id, session)
|
|
||||||
await self.hub.playlist_changed(self.server_room, item, beatmap_changed=False)
|
|
||||||
|
|
||||||
async def update_current_item(self):
|
|
||||||
upcoming_items = self.upcoming_items
|
|
||||||
if upcoming_items:
|
|
||||||
# 优先选择未过期的项目
|
|
||||||
next_item = upcoming_items[0]
|
|
||||||
else:
|
|
||||||
# 如果所有项目都过期了,选择最近添加的项目(played_at 为 None 或最新的)
|
|
||||||
# 优先选择 expired=False 的项目,然后是 played_at 最晚的
|
|
||||||
next_item = max(
|
|
||||||
self.room.playlist,
|
|
||||||
key=lambda i: (not i.expired, i.played_at or datetime.min),
|
|
||||||
)
|
|
||||||
self.current_index = self.room.playlist.index(next_item)
|
|
||||||
last_id = self.room.settings.playlist_item_id
|
|
||||||
self.room.settings.playlist_item_id = next_item.id
|
|
||||||
if last_id != next_item.id:
|
|
||||||
await self.hub.setting_changed(self.server_room, True)
|
|
||||||
|
|
||||||
async def add_item(self, item: PlaylistItem, user: MultiplayerRoomUser):
|
|
||||||
from app.database import Playlist
|
|
||||||
|
|
||||||
is_host = self.room.host and self.room.host.user_id == user.user_id
|
|
||||||
if self.room.settings.queue_mode == QueueMode.HOST_ONLY and not is_host:
|
|
||||||
raise InvokeException("You are not the host")
|
|
||||||
|
|
||||||
limit = HOST_LIMIT if is_host else PER_USER_LIMIT
|
|
||||||
if len([True for u in self.room.playlist if u.owner_id == user.user_id and not u.expired]) >= limit:
|
|
||||||
raise InvokeException(f"You can only have {limit} items in the queue")
|
|
||||||
|
|
||||||
if item.freestyle and len(item.allowed_mods) > 0:
|
|
||||||
raise InvokeException("Freestyle items cannot have allowed mods")
|
|
||||||
|
|
||||||
async with with_db() as session:
|
|
||||||
fetcher = await get_fetcher()
|
|
||||||
async with session:
|
|
||||||
beatmap = await Beatmap.get_or_fetch(session, fetcher, bid=item.beatmap_id)
|
|
||||||
if beatmap is None:
|
|
||||||
raise InvokeException("Beatmap not found")
|
|
||||||
if item.beatmap_checksum != beatmap.checksum:
|
|
||||||
raise InvokeException("Checksum mismatch")
|
|
||||||
|
|
||||||
item.validate_playlist_item_mods()
|
|
||||||
item.owner_id = user.user_id
|
|
||||||
item.star_rating = beatmap.difficulty_rating
|
|
||||||
await Playlist.add_to_db(item, self.room.room_id, session)
|
|
||||||
self.room.playlist.append(item)
|
|
||||||
await self.hub.playlist_added(self.server_room, item)
|
|
||||||
await self.update_order()
|
|
||||||
await self.update_current_item()
|
|
||||||
|
|
||||||
async def edit_item(self, item: PlaylistItem, user: MultiplayerRoomUser):
|
|
||||||
from app.database import Playlist
|
|
||||||
|
|
||||||
if item.freestyle and len(item.allowed_mods) > 0:
|
|
||||||
raise InvokeException("Freestyle items cannot have allowed mods")
|
|
||||||
|
|
||||||
async with with_db() as session:
|
|
||||||
fetcher = await get_fetcher()
|
|
||||||
async with session:
|
|
||||||
beatmap = await Beatmap.get_or_fetch(session, fetcher, bid=item.beatmap_id)
|
|
||||||
if item.beatmap_checksum != beatmap.checksum:
|
|
||||||
raise InvokeException("Checksum mismatch")
|
|
||||||
|
|
||||||
existing_item = next((i for i in self.room.playlist if i.id == item.id), None)
|
|
||||||
if existing_item is None:
|
|
||||||
raise InvokeException("Attempted to change an item that doesn't exist")
|
|
||||||
|
|
||||||
if existing_item.owner_id != user.user_id and self.room.host != user:
|
|
||||||
raise InvokeException("Attempted to change an item which is not owned by the user")
|
|
||||||
|
|
||||||
if existing_item.expired:
|
|
||||||
raise InvokeException("Attempted to change an item which has already been played")
|
|
||||||
|
|
||||||
item.validate_playlist_item_mods()
|
|
||||||
item.owner_id = user.user_id
|
|
||||||
item.star_rating = float(beatmap.difficulty_rating)
|
|
||||||
item.playlist_order = existing_item.playlist_order
|
|
||||||
|
|
||||||
await Playlist.update(item, self.room.room_id, session)
|
|
||||||
|
|
||||||
# Update item in playlist
|
|
||||||
for idx, playlist_item in enumerate(self.room.playlist):
|
|
||||||
if playlist_item.id == item.id:
|
|
||||||
self.room.playlist[idx] = item
|
|
||||||
break
|
|
||||||
|
|
||||||
await self.hub.playlist_changed(
|
|
||||||
self.server_room,
|
|
||||||
item,
|
|
||||||
beatmap_changed=item.beatmap_checksum != existing_item.beatmap_checksum,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def remove_item(self, playlist_item_id: int, user: MultiplayerRoomUser):
|
|
||||||
from app.database import Playlist
|
|
||||||
|
|
||||||
item = next(
|
|
||||||
(i for i in self.room.playlist if i.id == playlist_item_id),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
if item is None:
|
|
||||||
raise InvokeException("Item does not exist in the room")
|
|
||||||
|
|
||||||
# Check if it's the only item and current item
|
|
||||||
if item == self.current_item:
|
|
||||||
upcoming_items = [i for i in self.room.playlist if not i.expired]
|
|
||||||
if len(upcoming_items) == 1:
|
|
||||||
raise InvokeException("The only item in the room cannot be removed")
|
|
||||||
|
|
||||||
if item.owner_id != user.user_id and self.room.host != user:
|
|
||||||
raise InvokeException("Attempted to remove an item which is not owned by the user")
|
|
||||||
|
|
||||||
if item.expired:
|
|
||||||
raise InvokeException("Attempted to remove an item which has already been played")
|
|
||||||
|
|
||||||
async with with_db() as session:
|
|
||||||
await Playlist.delete_item(item.id, self.room.room_id, session)
|
|
||||||
|
|
||||||
found_item = next((i for i in self.room.playlist if i.id == item.id), None)
|
|
||||||
if found_item:
|
|
||||||
self.room.playlist.remove(found_item)
|
|
||||||
self.current_index = self.room.playlist.index(self.upcoming_items[0])
|
|
||||||
|
|
||||||
await self.update_order()
|
|
||||||
await self.update_current_item()
|
|
||||||
await self.hub.playlist_removed(self.server_room, item.id)
|
|
||||||
|
|
||||||
async def finish_current_item(self):
|
|
||||||
from app.database import Playlist
|
|
||||||
|
|
||||||
async with with_db() as session:
|
|
||||||
played_at = utcnow()
|
|
||||||
await session.execute(
|
|
||||||
update(Playlist)
|
|
||||||
.where(
|
|
||||||
col(Playlist.id) == self.current_item.id,
|
|
||||||
col(Playlist.room_id) == self.room.room_id,
|
|
||||||
)
|
|
||||||
.values(expired=True, played_at=played_at)
|
|
||||||
)
|
|
||||||
self.room.playlist[self.current_index].expired = True
|
|
||||||
self.room.playlist[self.current_index].played_at = played_at
|
|
||||||
await self.hub.playlist_changed(self.server_room, self.current_item, True)
|
|
||||||
await self.update_order()
|
|
||||||
if self.room.settings.queue_mode == QueueMode.HOST_ONLY and all(
|
|
||||||
playitem.expired for playitem in self.room.playlist
|
|
||||||
):
|
|
||||||
assert self.room.host
|
|
||||||
await self.add_item(self.current_item.clone(), self.room.host)
|
|
||||||
await self.update_current_item()
|
|
||||||
|
|
||||||
async def update_queue_mode(self):
|
|
||||||
if self.room.settings.queue_mode == QueueMode.HOST_ONLY and all(
|
|
||||||
playitem.expired for playitem in self.room.playlist
|
|
||||||
):
|
|
||||||
assert self.room.host
|
|
||||||
await self.add_item(self.current_item.clone(), self.room.host)
|
|
||||||
await self.update_order()
|
|
||||||
await self.update_current_item()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_item(self):
|
|
||||||
return self.room.playlist[self.current_index]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CountdownInfo:
|
|
||||||
countdown: MultiplayerCountdown
|
|
||||||
duration: timedelta
|
|
||||||
task: asyncio.Task | None = None
|
|
||||||
|
|
||||||
def __init__(self, countdown: MultiplayerCountdown):
|
|
||||||
self.countdown = countdown
|
|
||||||
self.duration = (
|
|
||||||
countdown.time_remaining if countdown.time_remaining > timedelta(seconds=0) else timedelta(seconds=0)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class _MatchRequest(SignalRUnionMessage): ...
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeTeamRequest(_MatchRequest):
|
|
||||||
union_type: ClassVar[Literal[0]] = 0
|
|
||||||
team_id: int
|
|
||||||
|
|
||||||
|
|
||||||
class StartMatchCountdownRequest(_MatchRequest):
|
|
||||||
union_type: ClassVar[Literal[1]] = 1
|
|
||||||
duration: timedelta
|
|
||||||
|
|
||||||
|
|
||||||
class StopCountdownRequest(_MatchRequest):
|
|
||||||
union_type: ClassVar[Literal[2]] = 2
|
|
||||||
id: int
|
|
||||||
|
|
||||||
|
|
||||||
MatchRequest = ChangeTeamRequest | StartMatchCountdownRequest | StopCountdownRequest
|
|
||||||
|
|
||||||
|
|
||||||
class MatchTypeHandler(ABC):
|
|
||||||
def __init__(self, room: "ServerMultiplayerRoom"):
|
|
||||||
self.room = room
|
|
||||||
self.hub = room.hub
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def handle_join(self, user: MultiplayerRoomUser): ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def handle_request(self, user: MultiplayerRoomUser, request: MatchRequest): ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def handle_leave(self, user: MultiplayerRoomUser): ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_details(self) -> MatchStartedEventDetail: ...
|
|
||||||
|
|
||||||
|
|
||||||
class HeadToHeadHandler(MatchTypeHandler):
|
|
||||||
@override
|
|
||||||
async def handle_join(self, user: MultiplayerRoomUser):
|
|
||||||
if user.match_state is not None:
|
|
||||||
user.match_state = None
|
|
||||||
await self.hub.change_user_match_state(self.room, user)
|
|
||||||
|
|
||||||
@override
|
|
||||||
async def handle_request(self, user: MultiplayerRoomUser, request: MatchRequest): ...
|
|
||||||
|
|
||||||
@override
|
|
||||||
async def handle_leave(self, user: MultiplayerRoomUser): ...
|
|
||||||
|
|
||||||
@override
|
|
||||||
def get_details(self) -> MatchStartedEventDetail:
|
|
||||||
detail = MatchStartedEventDetail(room_type="head_to_head", team=None)
|
|
||||||
return detail
|
|
||||||
|
|
||||||
|
|
||||||
class TeamVersusHandler(MatchTypeHandler):
|
|
||||||
@override
|
|
||||||
def __init__(self, room: "ServerMultiplayerRoom"):
|
|
||||||
super().__init__(room)
|
|
||||||
self.state = TeamVersusRoomState()
|
|
||||||
room.room.match_state = self.state
|
|
||||||
task = asyncio.create_task(self.hub.change_room_match_state(self.room))
|
|
||||||
self.hub.tasks.add(task)
|
|
||||||
task.add_done_callback(self.hub.tasks.discard)
|
|
||||||
|
|
||||||
def _get_best_available_team(self) -> int:
|
|
||||||
for team in self.state.teams:
|
|
||||||
if all(
|
|
||||||
(
|
|
||||||
user.match_state is None
|
|
||||||
or not isinstance(user.match_state, TeamVersusUserState)
|
|
||||||
or user.match_state.team_id != team.id
|
|
||||||
)
|
|
||||||
for user in self.room.room.users
|
|
||||||
):
|
|
||||||
return team.id
|
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
team_counts = defaultdict(int)
|
|
||||||
for user in self.room.room.users:
|
|
||||||
if user.match_state is not None and isinstance(user.match_state, TeamVersusUserState):
|
|
||||||
team_counts[user.match_state.team_id] += 1
|
|
||||||
|
|
||||||
if team_counts:
|
|
||||||
min_count = min(team_counts.values())
|
|
||||||
for team_id, count in team_counts.items():
|
|
||||||
if count == min_count:
|
|
||||||
return team_id
|
|
||||||
return self.state.teams[0].id if self.state.teams else 0
|
|
||||||
|
|
||||||
@override
|
|
||||||
async def handle_join(self, user: MultiplayerRoomUser):
|
|
||||||
best_team_id = self._get_best_available_team()
|
|
||||||
user.match_state = TeamVersusUserState(team_id=best_team_id)
|
|
||||||
await self.hub.change_user_match_state(self.room, user)
|
|
||||||
|
|
||||||
@override
|
|
||||||
async def handle_request(self, user: MultiplayerRoomUser, request: MatchRequest):
|
|
||||||
if not isinstance(request, ChangeTeamRequest):
|
|
||||||
return
|
|
||||||
|
|
||||||
if request.team_id not in [team.id for team in self.state.teams]:
|
|
||||||
raise InvokeException("Invalid team ID")
|
|
||||||
|
|
||||||
user.match_state = TeamVersusUserState(team_id=request.team_id)
|
|
||||||
await self.hub.change_user_match_state(self.room, user)
|
|
||||||
|
|
||||||
@override
|
|
||||||
async def handle_leave(self, user: MultiplayerRoomUser): ...
|
|
||||||
|
|
||||||
@override
|
|
||||||
def get_details(self) -> MatchStartedEventDetail:
|
|
||||||
teams: dict[int, Literal["blue", "red"]] = {}
|
|
||||||
for user in self.room.room.users:
|
|
||||||
if user.match_state is not None and isinstance(user.match_state, TeamVersusUserState):
|
|
||||||
teams[user.user_id] = "blue" if user.match_state.team_id == 1 else "red"
|
|
||||||
detail = MatchStartedEventDetail(room_type="team_versus", team=teams)
|
|
||||||
return detail
|
|
||||||
|
|
||||||
|
|
||||||
MATCH_TYPE_HANDLERS = {
|
|
||||||
MatchType.HEAD_TO_HEAD: HeadToHeadHandler,
|
|
||||||
MatchType.TEAM_VERSUS: TeamVersusHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ServerMultiplayerRoom:
|
|
||||||
room: MultiplayerRoom
|
|
||||||
category: RoomCategory
|
|
||||||
status: RoomStatus
|
|
||||||
start_at: datetime
|
|
||||||
hub: "MultiplayerHub"
|
|
||||||
match_type_handler: MatchTypeHandler
|
|
||||||
queue: MultiplayerQueue
|
|
||||||
_next_countdown_id: int
|
|
||||||
_countdown_id_lock: asyncio.Lock
|
|
||||||
_tracked_countdown: dict[int, CountdownInfo]
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
room: MultiplayerRoom,
|
|
||||||
category: RoomCategory,
|
|
||||||
start_at: datetime,
|
|
||||||
hub: "MultiplayerHub",
|
|
||||||
):
|
|
||||||
self.room = room
|
|
||||||
self.category = category
|
|
||||||
self.status = RoomStatus.IDLE
|
|
||||||
self.start_at = start_at
|
|
||||||
self.hub = hub
|
|
||||||
self.queue = MultiplayerQueue(self)
|
|
||||||
self._next_countdown_id = 0
|
|
||||||
self._countdown_id_lock = asyncio.Lock()
|
|
||||||
self._tracked_countdown = {}
|
|
||||||
|
|
||||||
async def set_handler(self):
|
|
||||||
self.match_type_handler = MATCH_TYPE_HANDLERS[self.room.settings.match_type](self)
|
|
||||||
for i in self.room.users:
|
|
||||||
await self.match_type_handler.handle_join(i)
|
|
||||||
|
|
||||||
async def get_next_countdown_id(self) -> int:
|
|
||||||
async with self._countdown_id_lock:
|
|
||||||
self._next_countdown_id += 1
|
|
||||||
return self._next_countdown_id
|
|
||||||
|
|
||||||
async def start_countdown(
|
|
||||||
self,
|
|
||||||
countdown: MultiplayerCountdown,
|
|
||||||
on_complete: Callable[["ServerMultiplayerRoom"], Awaitable[Any]] | None = None,
|
|
||||||
):
|
|
||||||
async def _countdown_task(self: "ServerMultiplayerRoom"):
|
|
||||||
await asyncio.sleep(info.duration.total_seconds())
|
|
||||||
if on_complete is not None:
|
|
||||||
await on_complete(self)
|
|
||||||
await self.stop_countdown(countdown)
|
|
||||||
|
|
||||||
if countdown.is_exclusive:
|
|
||||||
await self.stop_all_countdowns(countdown.__class__)
|
|
||||||
countdown.id = await self.get_next_countdown_id()
|
|
||||||
info = CountdownInfo(countdown)
|
|
||||||
self.room.active_countdowns.append(info.countdown)
|
|
||||||
self._tracked_countdown[countdown.id] = info
|
|
||||||
await self.hub.send_match_event(self, CountdownStartedEvent(countdown=info.countdown))
|
|
||||||
info.task = asyncio.create_task(_countdown_task(self))
|
|
||||||
|
|
||||||
async def stop_countdown(self, countdown: MultiplayerCountdown):
|
|
||||||
info = self._tracked_countdown.get(countdown.id)
|
|
||||||
if info is None:
|
|
||||||
return
|
|
||||||
del self._tracked_countdown[countdown.id]
|
|
||||||
self.room.active_countdowns.remove(countdown)
|
|
||||||
await self.hub.send_match_event(self, CountdownStoppedEvent(id=countdown.id))
|
|
||||||
if info.task is not None and not info.task.done():
|
|
||||||
info.task.cancel()
|
|
||||||
|
|
||||||
async def stop_all_countdowns(self, typ: type[MultiplayerCountdown]):
|
|
||||||
for countdown in list(self._tracked_countdown.values()):
|
|
||||||
if isinstance(countdown.countdown, typ):
|
|
||||||
await self.stop_countdown(countdown.countdown)
|
|
||||||
|
|
||||||
|
|
||||||
class _MatchServerEvent(SignalRUnionMessage): ...
|
|
||||||
|
|
||||||
|
|
||||||
class CountdownStartedEvent(_MatchServerEvent):
|
|
||||||
countdown: MultiplayerCountdown
|
|
||||||
|
|
||||||
union_type: ClassVar[Literal[0]] = 0
|
|
||||||
|
|
||||||
|
|
||||||
class CountdownStoppedEvent(_MatchServerEvent):
|
|
||||||
id: int
|
|
||||||
|
|
||||||
union_type: ClassVar[Literal[1]] = 1
|
|
||||||
|
|
||||||
|
|
||||||
MatchServerEvent = CountdownStartedEvent | CountdownStoppedEvent
|
|
||||||
|
|
||||||
|
|
||||||
class GameplayAbortReason(IntEnum):
|
|
||||||
LOAD_TOOK_TOO_LONG = 0
|
|
||||||
HOST_ABORTED = 1
|
|
||||||
|
|
||||||
|
|
||||||
class MatchStartedEventDetail(TypedDict):
|
|
||||||
room_type: Literal["playlists", "head_to_head", "team_versus"]
|
|
||||||
team: dict[int, Literal["blue", "red"]] | None
|
|
||||||
|
|||||||
22
app/models/playlist.py
Normal file
22
app/models/playlist.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.models.mods import APIMod
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistItem(BaseModel):
|
||||||
|
id: int = Field(default=0, ge=-1)
|
||||||
|
owner_id: int
|
||||||
|
beatmap_id: int
|
||||||
|
beatmap_checksum: str = ""
|
||||||
|
ruleset_id: int = 0
|
||||||
|
required_mods: list[APIMod] = Field(default_factory=list)
|
||||||
|
allowed_mods: list[APIMod] = Field(default_factory=list)
|
||||||
|
expired: bool = False
|
||||||
|
playlist_order: int = 0
|
||||||
|
played_at: datetime | None = None
|
||||||
|
star_rating: float = 0.0
|
||||||
|
freestyle: bool = False
|
||||||
@@ -1,37 +1 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import ClassVar
|
|
||||||
|
|
||||||
from pydantic import (
|
|
||||||
BaseModel,
|
|
||||||
Field,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SignalRMeta:
|
|
||||||
member_ignore: bool = False # implement of IgnoreMember (msgpack) attribute
|
|
||||||
json_ignore: bool = False # implement of JsonIgnore (json) attribute
|
|
||||||
use_abbr: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
class SignalRUnionMessage(BaseModel):
|
|
||||||
union_type: ClassVar[int]
|
|
||||||
|
|
||||||
|
|
||||||
class Transport(BaseModel):
|
|
||||||
transport: str
|
|
||||||
transfer_formats: list[str] = Field(default_factory=lambda: ["Binary", "Text"], alias="transferFormats")
|
|
||||||
|
|
||||||
|
|
||||||
class NegotiateResponse(BaseModel):
|
|
||||||
connectionId: str
|
|
||||||
connectionToken: str
|
|
||||||
negotiateVersion: int = 1
|
|
||||||
availableTransports: list[Transport]
|
|
||||||
|
|
||||||
|
|
||||||
class UserState(BaseModel):
|
|
||||||
connection_id: str
|
|
||||||
connection_token: str
|
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from enum import IntEnum
|
|
||||||
from typing import Annotated, Any
|
|
||||||
|
|
||||||
from app.models.beatmap import BeatmapRankStatus
|
|
||||||
from app.models.mods import APIMod
|
|
||||||
|
|
||||||
from .score import (
|
|
||||||
ScoreStatistics,
|
|
||||||
)
|
|
||||||
from .signalr import SignalRMeta, UserState
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, field_validator
|
|
||||||
|
|
||||||
|
|
||||||
class SpectatedUserState(IntEnum):
|
|
||||||
Idle = 0
|
|
||||||
Playing = 1
|
|
||||||
Paused = 2
|
|
||||||
Passed = 3
|
|
||||||
Failed = 4
|
|
||||||
Quit = 5
|
|
||||||
|
|
||||||
|
|
||||||
class SpectatorState(BaseModel):
|
|
||||||
beatmap_id: int | None = None
|
|
||||||
ruleset_id: int | None = None # 0,1,2,3
|
|
||||||
mods: list[APIMod] = Field(default_factory=list)
|
|
||||||
state: SpectatedUserState
|
|
||||||
maximum_statistics: ScoreStatistics = Field(default_factory=dict)
|
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
if not isinstance(other, SpectatorState):
|
|
||||||
return False
|
|
||||||
return (
|
|
||||||
self.beatmap_id == other.beatmap_id
|
|
||||||
and self.ruleset_id == other.ruleset_id
|
|
||||||
and self.mods == other.mods
|
|
||||||
and self.state == other.state
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ScoreProcessorStatistics(BaseModel):
|
|
||||||
base_score: float
|
|
||||||
maximum_base_score: float
|
|
||||||
accuracy_judgement_count: int
|
|
||||||
combo_portion: float
|
|
||||||
bonus_portion: float
|
|
||||||
|
|
||||||
|
|
||||||
class FrameHeader(BaseModel):
|
|
||||||
total_score: int
|
|
||||||
accuracy: float
|
|
||||||
combo: int
|
|
||||||
max_combo: int
|
|
||||||
statistics: ScoreStatistics = Field(default_factory=dict)
|
|
||||||
score_processor_statistics: ScoreProcessorStatistics
|
|
||||||
received_time: datetime.datetime
|
|
||||||
mods: list[APIMod] = Field(default_factory=list)
|
|
||||||
|
|
||||||
@field_validator("received_time", mode="before")
|
|
||||||
@classmethod
|
|
||||||
def validate_timestamp(cls, v: Any) -> datetime.datetime:
|
|
||||||
if isinstance(v, list):
|
|
||||||
return v[0]
|
|
||||||
if isinstance(v, datetime.datetime):
|
|
||||||
return v
|
|
||||||
if isinstance(v, int | float):
|
|
||||||
return datetime.datetime.fromtimestamp(v, tz=datetime.UTC)
|
|
||||||
if isinstance(v, str):
|
|
||||||
return datetime.datetime.fromisoformat(v)
|
|
||||||
raise ValueError(f"Cannot convert {type(v)} to datetime")
|
|
||||||
|
|
||||||
|
|
||||||
# class ReplayButtonState(IntEnum):
|
|
||||||
# NONE = 0
|
|
||||||
# LEFT1 = 1
|
|
||||||
# RIGHT1 = 2
|
|
||||||
# LEFT2 = 4
|
|
||||||
# RIGHT2 = 8
|
|
||||||
# SMOKE = 16
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyReplayFrame(BaseModel):
|
|
||||||
time: float # from ReplayFrame,the parent of LegacyReplayFrame
|
|
||||||
mouse_x: float | None = None
|
|
||||||
mouse_y: float | None = None
|
|
||||||
button_state: int
|
|
||||||
|
|
||||||
header: Annotated[FrameHeader | None, Field(default=None), SignalRMeta(member_ignore=True)]
|
|
||||||
|
|
||||||
|
|
||||||
class FrameDataBundle(BaseModel):
|
|
||||||
header: FrameHeader
|
|
||||||
frames: list[LegacyReplayFrame]
|
|
||||||
|
|
||||||
|
|
||||||
# Use for server
|
|
||||||
class APIUser(BaseModel):
|
|
||||||
id: int
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
class ScoreInfo(BaseModel):
|
|
||||||
mods: list[APIMod]
|
|
||||||
user: APIUser
|
|
||||||
ruleset: int
|
|
||||||
maximum_statistics: ScoreStatistics
|
|
||||||
id: int | None = None
|
|
||||||
total_score: int | None = None
|
|
||||||
accuracy: float | None = None
|
|
||||||
max_combo: int | None = None
|
|
||||||
combo: int | None = None
|
|
||||||
statistics: ScoreStatistics = Field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
class StoreScore(BaseModel):
|
|
||||||
score_info: ScoreInfo
|
|
||||||
replay_frames: list[LegacyReplayFrame] = Field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
class StoreClientState(UserState):
|
|
||||||
state: SpectatorState | None = None
|
|
||||||
beatmap_status: BeatmapRankStatus | None = None
|
|
||||||
checksum: str | None = None
|
|
||||||
ruleset_id: int | None = None
|
|
||||||
score_token: int | None = None
|
|
||||||
watched_user: set[int] = Field(default_factory=set)
|
|
||||||
score: StoreScore | None = None
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
# from app.signalr import signalr_router as signalr_router
|
|
||||||
from .auth import router as auth_router
|
from .auth import router as auth_router
|
||||||
from .fetcher import fetcher_router as fetcher_router
|
from .fetcher import fetcher_router as fetcher_router
|
||||||
from .file import file_router as file_router
|
from .file import file_router as file_router
|
||||||
@@ -25,5 +24,4 @@ __all__ = [
|
|||||||
"private_router",
|
"private_router",
|
||||||
"redirect_api_router",
|
"redirect_api_router",
|
||||||
"redirect_router",
|
"redirect_router",
|
||||||
# "signalr_router",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from app.dependencies.database import Database, Redis
|
|||||||
from app.dependencies.fetcher import Fetcher
|
from app.dependencies.fetcher import Fetcher
|
||||||
from app.dependencies.storage import StorageService
|
from app.dependencies.storage import StorageService
|
||||||
from app.log import log
|
from app.log import log
|
||||||
from app.models.multiplayer_hub import PlaylistItem as HubPlaylistItem
|
from app.models.playlist import PlaylistItem
|
||||||
from app.models.room import MatchType, QueueMode, RoomCategory, RoomStatus
|
from app.models.room import MatchType, QueueMode, RoomCategory, RoomStatus
|
||||||
from app.utils import utcnow
|
from app.utils import utcnow
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ async def _add_playlist_items(db: Database, room_id: int, room_data: dict[str, A
|
|||||||
|
|
||||||
# Insert playlist items
|
# Insert playlist items
|
||||||
for item_data in items_raw:
|
for item_data in items_raw:
|
||||||
hub_item = HubPlaylistItem(
|
playlist_item = PlaylistItem(
|
||||||
id=-1, # Placeholder, will be assigned by add_to_db
|
id=-1, # Placeholder, will be assigned by add_to_db
|
||||||
owner_id=item_data["owner_id"],
|
owner_id=item_data["owner_id"],
|
||||||
ruleset_id=item_data["ruleset_id"],
|
ruleset_id=item_data["ruleset_id"],
|
||||||
@@ -230,7 +230,7 @@ async def _add_playlist_items(db: Database, room_id: int, room_data: dict[str, A
|
|||||||
beatmap_checksum=item_data["beatmap_checksum"],
|
beatmap_checksum=item_data["beatmap_checksum"],
|
||||||
star_rating=item_data["star_rating"],
|
star_rating=item_data["star_rating"],
|
||||||
)
|
)
|
||||||
await DBPlaylist.add_to_db(hub_item, room_id=room_id, session=db)
|
await DBPlaylist.add_to_db(playlist_item, room_id=room_id, session=db)
|
||||||
|
|
||||||
|
|
||||||
async def _add_host_as_participant(db: Database, room_id: int, host_user_id: int) -> None:
|
async def _add_host_as_participant(db: Database, room_id: int, host_user_id: int) -> None:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from datetime import timedelta
|
|
||||||
from math import ceil
|
from math import ceil
|
||||||
import random
|
import random
|
||||||
import shlex
|
import shlex
|
||||||
@@ -10,27 +9,15 @@ import shlex
|
|||||||
from app.calculator import calculate_weighted_pp
|
from app.calculator import calculate_weighted_pp
|
||||||
from app.const import BANCHOBOT_ID
|
from app.const import BANCHOBOT_ID
|
||||||
from app.database import ChatMessageResp
|
from app.database import ChatMessageResp
|
||||||
from app.database.beatmap import Beatmap
|
|
||||||
from app.database.chat import ChannelType, ChatChannel, ChatMessage, MessageType
|
from app.database.chat import ChannelType, ChatChannel, ChatMessage, MessageType
|
||||||
from app.database.score import Score, get_best_id
|
from app.database.score import Score, get_best_id
|
||||||
from app.database.statistics import UserStatistics, get_rank
|
from app.database.statistics import UserStatistics, get_rank
|
||||||
from app.database.user import User
|
from app.database.user import User
|
||||||
from app.dependencies.fetcher import get_fetcher
|
from app.models.mods import mod_to_save
|
||||||
from app.exception import InvokeException
|
|
||||||
from app.models.mods import APIMod, get_available_mods, mod_to_save
|
|
||||||
from app.models.multiplayer_hub import (
|
|
||||||
ChangeTeamRequest,
|
|
||||||
ServerMultiplayerRoom,
|
|
||||||
StartMatchCountdownRequest,
|
|
||||||
)
|
|
||||||
from app.models.room import MatchType, QueueMode, RoomStatus
|
|
||||||
from app.models.score import GameMode
|
from app.models.score import GameMode
|
||||||
from app.signalr.hub import MultiplayerHubs
|
|
||||||
from app.signalr.hub.hub import Client
|
|
||||||
|
|
||||||
from .server import server
|
from .server import server
|
||||||
|
|
||||||
from httpx import HTTPError
|
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from sqlmodel import col, func, select
|
from sqlmodel import col, func, select
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
@@ -216,352 +203,6 @@ PP: {statistics.pp:.2f}
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
async def _mp_name(
|
|
||||||
signalr_client: Client,
|
|
||||||
room: ServerMultiplayerRoom,
|
|
||||||
args: list[str],
|
|
||||||
session: AsyncSession,
|
|
||||||
) -> str:
|
|
||||||
if len(args) < 1:
|
|
||||||
return "Usage: !mp name <name>"
|
|
||||||
|
|
||||||
name = args[0]
|
|
||||||
try:
|
|
||||||
settings = room.room.settings.model_copy()
|
|
||||||
settings.name = name
|
|
||||||
await MultiplayerHubs.ChangeSettings(signalr_client, settings)
|
|
||||||
return f"Room name has changed to {name}"
|
|
||||||
except InvokeException as e:
|
|
||||||
return e.message
|
|
||||||
|
|
||||||
|
|
||||||
async def _mp_set(
|
|
||||||
signalr_client: Client,
|
|
||||||
room: ServerMultiplayerRoom,
|
|
||||||
args: list[str],
|
|
||||||
session: AsyncSession,
|
|
||||||
) -> str:
|
|
||||||
if len(args) < 1:
|
|
||||||
return "Usage: !mp set <teammode> [<queuemode>]"
|
|
||||||
|
|
||||||
teammode = {"0": MatchType.HEAD_TO_HEAD, "2": MatchType.TEAM_VERSUS}.get(args[0])
|
|
||||||
if not teammode:
|
|
||||||
return "Invalid teammode. Use 0 for Head-to-Head or 2 for Team Versus."
|
|
||||||
queuemode = (
|
|
||||||
{
|
|
||||||
"0": QueueMode.HOST_ONLY,
|
|
||||||
"1": QueueMode.ALL_PLAYERS,
|
|
||||||
"2": QueueMode.ALL_PLAYERS_ROUND_ROBIN,
|
|
||||||
}.get(args[1])
|
|
||||||
if len(args) >= 2
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
settings = room.room.settings.model_copy()
|
|
||||||
settings.match_type = teammode
|
|
||||||
if queuemode:
|
|
||||||
settings.queue_mode = queuemode
|
|
||||||
await MultiplayerHubs.ChangeSettings(signalr_client, settings)
|
|
||||||
return f"Room setting 'teammode' has been changed to {teammode.name.lower()}"
|
|
||||||
except InvokeException as e:
|
|
||||||
return e.message
|
|
||||||
|
|
||||||
|
|
||||||
async def _mp_host(
|
|
||||||
signalr_client: Client,
|
|
||||||
room: ServerMultiplayerRoom,
|
|
||||||
args: list[str],
|
|
||||||
session: AsyncSession,
|
|
||||||
) -> str:
|
|
||||||
if len(args) < 1:
|
|
||||||
return "Usage: !mp host <username>"
|
|
||||||
|
|
||||||
username = args[0]
|
|
||||||
user_id = (await session.exec(select(User.id).where(User.username == username))).first()
|
|
||||||
if not user_id:
|
|
||||||
return f"User '{username}' not found."
|
|
||||||
|
|
||||||
try:
|
|
||||||
await MultiplayerHubs.TransferHost(signalr_client, user_id)
|
|
||||||
return f"User '{username}' has been hosted in the room."
|
|
||||||
except InvokeException as e:
|
|
||||||
return e.message
|
|
||||||
|
|
||||||
|
|
||||||
async def _mp_start(
|
|
||||||
signalr_client: Client,
|
|
||||||
room: ServerMultiplayerRoom,
|
|
||||||
args: list[str],
|
|
||||||
session: AsyncSession,
|
|
||||||
) -> str:
|
|
||||||
timer = None
|
|
||||||
if len(args) >= 1 and args[0].isdigit():
|
|
||||||
timer = int(args[0])
|
|
||||||
|
|
||||||
try:
|
|
||||||
if timer is not None:
|
|
||||||
await MultiplayerHubs.SendMatchRequest(
|
|
||||||
signalr_client,
|
|
||||||
StartMatchCountdownRequest(duration=timedelta(seconds=timer)),
|
|
||||||
)
|
|
||||||
return ""
|
|
||||||
else:
|
|
||||||
await MultiplayerHubs.StartMatch(signalr_client)
|
|
||||||
return "Good luck! Enjoy game!"
|
|
||||||
except InvokeException as e:
|
|
||||||
return e.message
|
|
||||||
|
|
||||||
|
|
||||||
async def _mp_abort(
|
|
||||||
signalr_client: Client,
|
|
||||||
room: ServerMultiplayerRoom,
|
|
||||||
args: list[str],
|
|
||||||
session: AsyncSession,
|
|
||||||
) -> str:
|
|
||||||
try:
|
|
||||||
await MultiplayerHubs.AbortMatch(signalr_client)
|
|
||||||
return "Match aborted."
|
|
||||||
except InvokeException as e:
|
|
||||||
return e.message
|
|
||||||
|
|
||||||
|
|
||||||
async def _mp_team(
|
|
||||||
signalr_client: Client,
|
|
||||||
room: ServerMultiplayerRoom,
|
|
||||||
args: list[str],
|
|
||||||
session: AsyncSession,
|
|
||||||
):
|
|
||||||
if room.room.settings.match_type != MatchType.TEAM_VERSUS:
|
|
||||||
return "This command is only available in Team Versus mode."
|
|
||||||
|
|
||||||
if len(args) < 2:
|
|
||||||
return "Usage: !mp team <username> <colour>"
|
|
||||||
|
|
||||||
username = args[0]
|
|
||||||
team = {"red": 0, "blue": 1}.get(args[1])
|
|
||||||
if team is None:
|
|
||||||
return "Invalid team colour. Use 'red' or 'blue'."
|
|
||||||
|
|
||||||
user_id = (await session.exec(select(User.id).where(User.username == username))).first()
|
|
||||||
if not user_id:
|
|
||||||
return f"User '{username}' not found."
|
|
||||||
user_client = MultiplayerHubs.get_client_by_id(str(user_id))
|
|
||||||
if not user_client:
|
|
||||||
return f"User '{username}' is not in the room."
|
|
||||||
assert room.room.host
|
|
||||||
if user_client.user_id != signalr_client.user_id and room.room.host.user_id != signalr_client.user_id:
|
|
||||||
return "You are not allowed to change other users' teams."
|
|
||||||
|
|
||||||
try:
|
|
||||||
await MultiplayerHubs.SendMatchRequest(user_client, ChangeTeamRequest(team_id=team))
|
|
||||||
return ""
|
|
||||||
except InvokeException as e:
|
|
||||||
return e.message
|
|
||||||
|
|
||||||
|
|
||||||
async def _mp_password(
|
|
||||||
signalr_client: Client,
|
|
||||||
room: ServerMultiplayerRoom,
|
|
||||||
args: list[str],
|
|
||||||
session: AsyncSession,
|
|
||||||
) -> str:
|
|
||||||
password = ""
|
|
||||||
if len(args) >= 1:
|
|
||||||
password = args[0]
|
|
||||||
|
|
||||||
try:
|
|
||||||
settings = room.room.settings.model_copy()
|
|
||||||
settings.password = password
|
|
||||||
await MultiplayerHubs.ChangeSettings(signalr_client, settings)
|
|
||||||
return "Room password has been set."
|
|
||||||
except InvokeException as e:
|
|
||||||
return e.message
|
|
||||||
|
|
||||||
|
|
||||||
async def _mp_kick(
|
|
||||||
signalr_client: Client,
|
|
||||||
room: ServerMultiplayerRoom,
|
|
||||||
args: list[str],
|
|
||||||
session: AsyncSession,
|
|
||||||
) -> str:
|
|
||||||
if len(args) < 1:
|
|
||||||
return "Usage: !mp kick <username>"
|
|
||||||
|
|
||||||
username = args[0]
|
|
||||||
user_id = (await session.exec(select(User.id).where(User.username == username))).first()
|
|
||||||
if not user_id:
|
|
||||||
return f"User '{username}' not found."
|
|
||||||
|
|
||||||
try:
|
|
||||||
await MultiplayerHubs.KickUser(signalr_client, user_id)
|
|
||||||
return f"User '{username}' has been kicked from the room."
|
|
||||||
except InvokeException as e:
|
|
||||||
return e.message
|
|
||||||
|
|
||||||
|
|
||||||
async def _mp_map(
|
|
||||||
signalr_client: Client,
|
|
||||||
room: ServerMultiplayerRoom,
|
|
||||||
args: list[str],
|
|
||||||
session: AsyncSession,
|
|
||||||
) -> str:
|
|
||||||
if len(args) < 1:
|
|
||||||
return "Usage: !mp map <mapid> [<playmode>]"
|
|
||||||
|
|
||||||
if room.status != RoomStatus.IDLE:
|
|
||||||
return "Cannot change map while the game is running."
|
|
||||||
|
|
||||||
map_id = args[0]
|
|
||||||
if not map_id.isdigit():
|
|
||||||
return "Invalid map ID."
|
|
||||||
map_id = int(map_id)
|
|
||||||
playmode = GameMode.parse(args[1].upper()) if len(args) >= 2 else None
|
|
||||||
if playmode not in (
|
|
||||||
GameMode.OSU,
|
|
||||||
GameMode.TAIKO,
|
|
||||||
GameMode.FRUITS,
|
|
||||||
GameMode.MANIA,
|
|
||||||
None,
|
|
||||||
):
|
|
||||||
return "Invalid playmode."
|
|
||||||
|
|
||||||
try:
|
|
||||||
beatmap = await Beatmap.get_or_fetch(session, await get_fetcher(), bid=map_id)
|
|
||||||
if beatmap.mode != GameMode.OSU and playmode and playmode != beatmap.mode:
|
|
||||||
return f"Cannot convert to {playmode.value}. Original mode is {beatmap.mode.value}."
|
|
||||||
except HTTPError:
|
|
||||||
return "Beatmap not found"
|
|
||||||
|
|
||||||
try:
|
|
||||||
current_item = room.queue.current_item
|
|
||||||
item = current_item.model_copy(deep=True)
|
|
||||||
item.owner_id = signalr_client.user_id
|
|
||||||
item.beatmap_checksum = beatmap.checksum
|
|
||||||
item.required_mods = []
|
|
||||||
item.allowed_mods = []
|
|
||||||
item.freestyle = False
|
|
||||||
item.beatmap_id = map_id
|
|
||||||
if playmode is not None:
|
|
||||||
item.ruleset_id = int(playmode)
|
|
||||||
if item.expired:
|
|
||||||
item.id = 0
|
|
||||||
item.expired = False
|
|
||||||
item.played_at = None
|
|
||||||
await MultiplayerHubs.AddPlaylistItem(signalr_client, item)
|
|
||||||
else:
|
|
||||||
await MultiplayerHubs.EditPlaylistItem(signalr_client, item)
|
|
||||||
return ""
|
|
||||||
except InvokeException as e:
|
|
||||||
return e.message
|
|
||||||
|
|
||||||
|
|
||||||
async def _mp_mods(
|
|
||||||
signalr_client: Client,
|
|
||||||
room: ServerMultiplayerRoom,
|
|
||||||
args: list[str],
|
|
||||||
session: AsyncSession,
|
|
||||||
) -> str:
|
|
||||||
if len(args) < 1:
|
|
||||||
return "Usage: !mp mods <mod1> [<mod2> ...]"
|
|
||||||
|
|
||||||
if room.status != RoomStatus.IDLE:
|
|
||||||
return "Cannot change mods while the game is running."
|
|
||||||
|
|
||||||
required_mods = []
|
|
||||||
allowed_mods = []
|
|
||||||
freestyle = False
|
|
||||||
freemod = False
|
|
||||||
for arg in args:
|
|
||||||
arg = arg.upper()
|
|
||||||
if arg == "NONE":
|
|
||||||
required_mods.clear()
|
|
||||||
allowed_mods.clear()
|
|
||||||
break
|
|
||||||
elif arg == "FREESTYLE":
|
|
||||||
freestyle = True
|
|
||||||
elif arg == "FREEMOD":
|
|
||||||
freemod = True
|
|
||||||
elif arg.startswith("+"):
|
|
||||||
mod = arg.removeprefix("+")
|
|
||||||
if len(mod) != 2:
|
|
||||||
return f"Invalid mod: {mod}."
|
|
||||||
allowed_mods.append(APIMod(acronym=mod))
|
|
||||||
else:
|
|
||||||
if len(arg) != 2:
|
|
||||||
return f"Invalid mod: {arg}."
|
|
||||||
required_mods.append(APIMod(acronym=arg))
|
|
||||||
|
|
||||||
try:
|
|
||||||
current_item = room.queue.current_item
|
|
||||||
item = current_item.model_copy(deep=True)
|
|
||||||
item.owner_id = signalr_client.user_id
|
|
||||||
item.freestyle = freestyle
|
|
||||||
if freestyle:
|
|
||||||
item.allowed_mods = []
|
|
||||||
elif freemod:
|
|
||||||
item.allowed_mods = get_available_mods(current_item.ruleset_id, required_mods)
|
|
||||||
else:
|
|
||||||
item.allowed_mods = allowed_mods
|
|
||||||
item.required_mods = required_mods
|
|
||||||
if item.expired:
|
|
||||||
item.id = 0
|
|
||||||
item.expired = False
|
|
||||||
item.played_at = None
|
|
||||||
await MultiplayerHubs.AddPlaylistItem(signalr_client, item)
|
|
||||||
else:
|
|
||||||
await MultiplayerHubs.EditPlaylistItem(signalr_client, item)
|
|
||||||
return ""
|
|
||||||
except InvokeException as e:
|
|
||||||
return e.message
|
|
||||||
|
|
||||||
|
|
||||||
_MP_COMMANDS = {
|
|
||||||
"name": _mp_name,
|
|
||||||
"set": _mp_set,
|
|
||||||
"host": _mp_host,
|
|
||||||
"start": _mp_start,
|
|
||||||
"abort": _mp_abort,
|
|
||||||
"map": _mp_map,
|
|
||||||
"mods": _mp_mods,
|
|
||||||
"kick": _mp_kick,
|
|
||||||
"password": _mp_password,
|
|
||||||
"team": _mp_team,
|
|
||||||
}
|
|
||||||
_MP_HELP = """!mp name <name>
|
|
||||||
!mp set <teammode> [<queuemode>]
|
|
||||||
!mp host <host>
|
|
||||||
!mp start [<timer>]
|
|
||||||
!mp abort
|
|
||||||
!mp map <map> [<playmode>]
|
|
||||||
!mp mods <mod1> [<mod2> ...]
|
|
||||||
!mp kick <user>
|
|
||||||
!mp password [<password>]
|
|
||||||
!mp team <user> <team:red|blue>"""
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command("mp")
|
|
||||||
async def _mp(user: User, args: list[str], session: AsyncSession, channel: ChatChannel):
|
|
||||||
if not channel.name.startswith("room_"):
|
|
||||||
return
|
|
||||||
|
|
||||||
room_id = int(channel.name[5:])
|
|
||||||
room = MultiplayerHubs.rooms.get(room_id)
|
|
||||||
if not room:
|
|
||||||
return
|
|
||||||
signalr_client = MultiplayerHubs.get_client_by_id(str(user.id))
|
|
||||||
if not signalr_client:
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(args) < 1:
|
|
||||||
return f"Usage: !mp <{'|'.join(_MP_COMMANDS.keys())}> [args]"
|
|
||||||
|
|
||||||
command = args[0].lower()
|
|
||||||
if command not in _MP_COMMANDS:
|
|
||||||
return f"No such command: {command}"
|
|
||||||
|
|
||||||
return await _MP_COMMANDS[command](signalr_client, room, args[1:], session)
|
|
||||||
|
|
||||||
|
|
||||||
async def _score(
|
async def _score(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from app.dependencies.database import Database, Redis
|
|||||||
from app.dependencies.user import ClientUser, get_current_user
|
from app.dependencies.user import ClientUser, get_current_user
|
||||||
from app.models.room import RoomCategory, RoomStatus
|
from app.models.room import RoomCategory, RoomStatus
|
||||||
from app.service.room import create_playlist_room_from_api
|
from app.service.room import create_playlist_room_from_api
|
||||||
from app.signalr.hub import MultiplayerHubs
|
|
||||||
from app.utils import utcnow
|
from app.utils import utcnow
|
||||||
|
|
||||||
from .router import router
|
from .router import router
|
||||||
@@ -391,14 +390,12 @@ async def get_room_events(
|
|||||||
first_event_id = min(first_event_id, event.id)
|
first_event_id = min(first_event_id, event.id)
|
||||||
last_event_id = max(last_event_id, event.id)
|
last_event_id = max(last_event_id, event.id)
|
||||||
|
|
||||||
if room := MultiplayerHubs.rooms.get(room_id):
|
room = (await db.exec(select(Room).where(Room.id == room_id))).first()
|
||||||
current_playlist_item_id = room.queue.current_item.id
|
if room is None:
|
||||||
room_resp = await RoomResp.from_hub(room)
|
raise HTTPException(404, "Room not found")
|
||||||
else:
|
room_resp = await RoomResp.from_db(room, db)
|
||||||
room = (await db.exec(select(Room).where(Room.id == room_id))).first()
|
if room.category == RoomCategory.REALTIME and room_resp.current_playlist_item:
|
||||||
if room is None:
|
current_playlist_item_id = room_resp.current_playlist_item.id
|
||||||
raise HTTPException(404, "Room not found")
|
|
||||||
room_resp = await RoomResp.from_db(room, db)
|
|
||||||
|
|
||||||
users = await db.exec(select(User).where(col(User.id).in_(user_ids)))
|
users = await db.exec(select(User).where(col(User.id).in_(user_ids)))
|
||||||
user_resps = [await UserResp.from_db(user, db) for user in users]
|
user_resps = [await UserResp.from_db(user, db) for user in users]
|
||||||
|
|||||||
@@ -217,8 +217,7 @@ class MessageQueueProcessor:
|
|||||||
):
|
):
|
||||||
"""通知客户端消息ID已更新"""
|
"""通知客户端消息ID已更新"""
|
||||||
try:
|
try:
|
||||||
# 这里我们需要通过 SignalR 发送消息更新通知
|
# 通过 Redis 发布消息更新事件,由聊天通知服务分发到客户端
|
||||||
# 但为了避免循环依赖,我们将通过 Redis 发布消息更新事件
|
|
||||||
update_event = {
|
update_event = {
|
||||||
"event": "chat.message.update",
|
"event": "chat.message.update",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -229,7 +228,6 @@ class MessageQueueProcessor:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# 发布到 Redis 频道,让 SignalR 服务处理
|
|
||||||
await self._redis_exec(
|
await self._redis_exec(
|
||||||
self.redis_message.publish,
|
self.redis_message.publish,
|
||||||
f"chat_updates:{channel_id}",
|
f"chat_updates:{channel_id}",
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from app.database import PlaylistBestScore, Score
|
|
||||||
from app.database.playlist_best_score import get_position
|
|
||||||
from app.dependencies.database import with_db
|
|
||||||
from app.models.metadata_hub import MultiplayerRoomScoreSetEvent
|
|
||||||
|
|
||||||
from .base import RedisSubscriber
|
|
||||||
|
|
||||||
from sqlmodel import select
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from app.signalr.hub import MetadataHub
|
|
||||||
|
|
||||||
|
|
||||||
CHANNEL = "osu-channel:score:processed"
|
|
||||||
|
|
||||||
|
|
||||||
class ScoreSubscriber(RedisSubscriber):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.room_subscriber: dict[int, list[int]] = {}
|
|
||||||
self.metadata_hub: "MetadataHub | None " = None
|
|
||||||
self.subscribed = False
|
|
||||||
self.handlers[CHANNEL] = [self._handler]
|
|
||||||
|
|
||||||
async def subscribe_room_score(self, room_id: int, user_id: int):
|
|
||||||
if room_id not in self.room_subscriber:
|
|
||||||
await self.subscribe(CHANNEL)
|
|
||||||
self.start()
|
|
||||||
self.room_subscriber.setdefault(room_id, []).append(user_id)
|
|
||||||
|
|
||||||
async def unsubscribe_room_score(self, room_id: int, user_id: int):
|
|
||||||
if room_id in self.room_subscriber:
|
|
||||||
try:
|
|
||||||
self.room_subscriber[room_id].remove(user_id)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if not self.room_subscriber[room_id]:
|
|
||||||
del self.room_subscriber[room_id]
|
|
||||||
|
|
||||||
async def _notify_room_score_processed(self, score_id: int):
|
|
||||||
if not self.metadata_hub:
|
|
||||||
return
|
|
||||||
async with with_db() as session:
|
|
||||||
score = await session.get(Score, score_id)
|
|
||||||
if not score or not score.passed or score.room_id is None or score.playlist_item_id is None:
|
|
||||||
return
|
|
||||||
if not self.room_subscriber.get(score.room_id, []):
|
|
||||||
return
|
|
||||||
|
|
||||||
new_rank = None
|
|
||||||
user_best = (
|
|
||||||
await session.exec(
|
|
||||||
select(PlaylistBestScore).where(
|
|
||||||
PlaylistBestScore.user_id == score.user_id,
|
|
||||||
PlaylistBestScore.room_id == score.room_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
if user_best and user_best.score_id == score_id:
|
|
||||||
new_rank = await get_position(
|
|
||||||
user_best.room_id,
|
|
||||||
user_best.playlist_id,
|
|
||||||
user_best.score_id,
|
|
||||||
session,
|
|
||||||
)
|
|
||||||
|
|
||||||
event = MultiplayerRoomScoreSetEvent(
|
|
||||||
room_id=score.room_id,
|
|
||||||
playlist_item_id=score.playlist_item_id,
|
|
||||||
score_id=score_id,
|
|
||||||
user_id=score.user_id,
|
|
||||||
total_score=score.total_score,
|
|
||||||
new_rank=new_rank,
|
|
||||||
)
|
|
||||||
await self.metadata_hub.notify_room_score_processed(event)
|
|
||||||
|
|
||||||
async def _handler(self, channel: str, data: str):
|
|
||||||
score_id = json.loads(data)["ScoreId"]
|
|
||||||
if self.metadata_hub:
|
|
||||||
await self._notify_room_score_processed(score_id)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .router import router as signalr_router
|
|
||||||
|
|
||||||
__all__ = ["signalr_router"]
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .hub import Hub
|
|
||||||
from .metadata import MetadataHub
|
|
||||||
from .multiplayer import MultiplayerHub
|
|
||||||
from .spectator import SpectatorHub
|
|
||||||
|
|
||||||
SpectatorHubs = SpectatorHub()
|
|
||||||
MultiplayerHubs = MultiplayerHub()
|
|
||||||
MetadataHubs = MetadataHub()
|
|
||||||
Hubs: dict[str, Hub] = {
|
|
||||||
"spectator": SpectatorHubs,
|
|
||||||
"multiplayer": MultiplayerHubs,
|
|
||||||
"metadata": MetadataHubs,
|
|
||||||
}
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from abc import abstractmethod
|
|
||||||
import asyncio
|
|
||||||
import time
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from app.config import settings
|
|
||||||
from app.exception import InvokeException
|
|
||||||
from app.log import logger
|
|
||||||
from app.models.signalr import UserState
|
|
||||||
from app.signalr.packet import (
|
|
||||||
ClosePacket,
|
|
||||||
CompletionPacket,
|
|
||||||
InvocationPacket,
|
|
||||||
Packet,
|
|
||||||
PingPacket,
|
|
||||||
Protocol,
|
|
||||||
)
|
|
||||||
from app.signalr.store import ResultStore
|
|
||||||
from app.signalr.utils import get_signature
|
|
||||||
|
|
||||||
from fastapi import WebSocket
|
|
||||||
from starlette.websockets import WebSocketDisconnect
|
|
||||||
|
|
||||||
|
|
||||||
class CloseConnection(Exception):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
message: str = "Connection closed",
|
|
||||||
allow_reconnect: bool = False,
|
|
||||||
from_client: bool = False,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(message)
|
|
||||||
self.message = message
|
|
||||||
self.allow_reconnect = allow_reconnect
|
|
||||||
self.from_client = from_client
|
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
connection_id: str,
|
|
||||||
connection_token: str,
|
|
||||||
connection: WebSocket,
|
|
||||||
protocol: Protocol,
|
|
||||||
) -> None:
|
|
||||||
self.connection_id = connection_id
|
|
||||||
self.connection_token = connection_token
|
|
||||||
self.connection = connection
|
|
||||||
self.protocol = protocol
|
|
||||||
self._listen_task: asyncio.Task | None = None
|
|
||||||
self._ping_task: asyncio.Task | None = None
|
|
||||||
self._store = ResultStore()
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(self.connection_token)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_id(self) -> int:
|
|
||||||
return int(self.connection_id)
|
|
||||||
|
|
||||||
async def send_packet(self, packet: Packet):
|
|
||||||
await self.connection.send_bytes(self.protocol.encode(packet))
|
|
||||||
|
|
||||||
async def receive_packets(self) -> list[Packet]:
|
|
||||||
message = await self.connection.receive()
|
|
||||||
d = message.get("bytes") or message.get("text", "").encode()
|
|
||||||
if not d:
|
|
||||||
return []
|
|
||||||
return self.protocol.decode(d)
|
|
||||||
|
|
||||||
async def _ping(self):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
await self.send_packet(PingPacket())
|
|
||||||
await asyncio.sleep(settings.signalr_ping_interval)
|
|
||||||
except WebSocketDisconnect:
|
|
||||||
break
|
|
||||||
except RuntimeError as e:
|
|
||||||
if "disconnect message" in str(e) or "close message" in str(e):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
logger.error(f"Error in ping task for {self.connection_id}: {e}")
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f"Error in client {self.connection_id}")
|
|
||||||
|
|
||||||
|
|
||||||
class Hub[TState: UserState]:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.clients: dict[str, Client] = {}
|
|
||||||
self.waited_clients: dict[str, int] = {}
|
|
||||||
self.tasks: set[asyncio.Task] = set()
|
|
||||||
self.groups: dict[str, set[Client]] = {}
|
|
||||||
self.state: dict[int, TState] = {}
|
|
||||||
|
|
||||||
def add_waited_client(self, connection_token: str, timestamp: int) -> None:
|
|
||||||
self.waited_clients[connection_token] = timestamp
|
|
||||||
|
|
||||||
def get_client_by_id(self, id: str, default: Any = None) -> Client:
|
|
||||||
for client in self.clients.values():
|
|
||||||
if client.connection_id == id:
|
|
||||||
return client
|
|
||||||
return default
|
|
||||||
|
|
||||||
def get_before_clients(self, id: str, current_token: str) -> list[Client]:
|
|
||||||
clients = []
|
|
||||||
for client in self.clients.values():
|
|
||||||
if client.connection_id != id:
|
|
||||||
continue
|
|
||||||
if client.connection_token == current_token:
|
|
||||||
continue
|
|
||||||
clients.append(client)
|
|
||||||
return clients
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def create_state(self, client: Client) -> TState:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def get_or_create_state(self, client: Client) -> TState:
|
|
||||||
if (state := self.state.get(client.user_id)) is not None:
|
|
||||||
return state
|
|
||||||
state = self.create_state(client)
|
|
||||||
self.state[client.user_id] = state
|
|
||||||
return state
|
|
||||||
|
|
||||||
def add_to_group(self, client: Client, group_id: str) -> None:
|
|
||||||
self.groups.setdefault(group_id, set()).add(client)
|
|
||||||
|
|
||||||
def remove_from_group(self, client: Client, group_id: str) -> None:
|
|
||||||
if group_id in self.groups:
|
|
||||||
self.groups[group_id].discard(client)
|
|
||||||
|
|
||||||
async def kick_client(self, client: Client) -> None:
|
|
||||||
await self.call_noblock(client, "DisconnectRequested")
|
|
||||||
await client.send_packet(ClosePacket(allow_reconnect=False))
|
|
||||||
await client.connection.close(code=1000, reason="Disconnected by server")
|
|
||||||
|
|
||||||
async def add_client(
|
|
||||||
self,
|
|
||||||
connection_id: str,
|
|
||||||
connection_token: str,
|
|
||||||
protocol: Protocol,
|
|
||||||
connection: WebSocket,
|
|
||||||
) -> Client:
|
|
||||||
if connection_token in self.clients:
|
|
||||||
raise ValueError(f"Client with connection token {connection_token} already exists.")
|
|
||||||
if connection_token in self.waited_clients:
|
|
||||||
if self.waited_clients[connection_token] < time.time() - settings.signalr_negotiate_timeout:
|
|
||||||
raise TimeoutError(f"Connection {connection_id} has waited too long.")
|
|
||||||
del self.waited_clients[connection_token]
|
|
||||||
client = Client(connection_id, connection_token, connection, protocol)
|
|
||||||
self.clients[connection_token] = client
|
|
||||||
task = asyncio.create_task(client._ping())
|
|
||||||
self.tasks.add(task)
|
|
||||||
client._ping_task = task
|
|
||||||
return client
|
|
||||||
|
|
||||||
async def remove_client(self, client: Client) -> None:
|
|
||||||
if client.connection_token not in self.clients:
|
|
||||||
return
|
|
||||||
del self.clients[client.connection_token]
|
|
||||||
if client._listen_task:
|
|
||||||
client._listen_task.cancel()
|
|
||||||
if client._ping_task:
|
|
||||||
client._ping_task.cancel()
|
|
||||||
for group in self.groups.values():
|
|
||||||
group.discard(client)
|
|
||||||
await self.clean_state(client, False)
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def _clean_state(self, state: TState) -> None:
|
|
||||||
return
|
|
||||||
|
|
||||||
async def clean_state(self, client: Client, disconnected: bool) -> None:
|
|
||||||
if (state := self.state.get(client.user_id)) is None:
|
|
||||||
return
|
|
||||||
if disconnected and client.connection_token != state.connection_token:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
await self._clean_state(state)
|
|
||||||
del self.state[client.user_id]
|
|
||||||
except Exception:
|
|
||||||
...
|
|
||||||
|
|
||||||
async def on_connect(self, client: Client) -> None:
|
|
||||||
if method := getattr(self, "on_client_connect", None):
|
|
||||||
await method(client)
|
|
||||||
|
|
||||||
async def send_packet(self, client: Client, packet: Packet) -> None:
|
|
||||||
logger.trace(f"[SignalR] send to {client.connection_id} packet {packet}")
|
|
||||||
try:
|
|
||||||
await client.send_packet(packet)
|
|
||||||
except WebSocketDisconnect as e:
|
|
||||||
logger.info(f"Client {client.connection_id} disconnected: {e.code}, {e.reason}")
|
|
||||||
await self.remove_client(client)
|
|
||||||
except RuntimeError as e:
|
|
||||||
if "disconnect message" in str(e):
|
|
||||||
logger.info(f"Client {client.connection_id} closed the connection.")
|
|
||||||
else:
|
|
||||||
logger.exception(f"RuntimeError in client {client.connection_id}: {e}")
|
|
||||||
await self.remove_client(client)
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f"Error in client {client.connection_id}")
|
|
||||||
await self.remove_client(client)
|
|
||||||
|
|
||||||
async def broadcast_call(self, method: str, *args: Any) -> None:
|
|
||||||
tasks = []
|
|
||||||
for client in self.clients.values():
|
|
||||||
tasks.append(self.call_noblock(client, method, *args))
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def broadcast_group_call(self, group_id: str, method: str, *args: Any) -> None:
|
|
||||||
tasks = []
|
|
||||||
for client in self.groups.get(group_id, []):
|
|
||||||
tasks.append(self.call_noblock(client, method, *args))
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def _listen_client(self, client: Client) -> None:
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
packets = await client.receive_packets()
|
|
||||||
for packet in packets:
|
|
||||||
if isinstance(packet, PingPacket):
|
|
||||||
continue
|
|
||||||
elif isinstance(packet, ClosePacket):
|
|
||||||
raise CloseConnection(
|
|
||||||
packet.error or "Connection closed by client",
|
|
||||||
packet.allow_reconnect,
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
task = asyncio.create_task(self._handle_packet(client, packet))
|
|
||||||
self.tasks.add(task)
|
|
||||||
task.add_done_callback(self.tasks.discard)
|
|
||||||
except WebSocketDisconnect as e:
|
|
||||||
logger.info(f"Client {client.connection_id} disconnected: {e.code}, {e.reason}")
|
|
||||||
except RuntimeError as e:
|
|
||||||
if "disconnect message" in str(e):
|
|
||||||
logger.info(f"Client {client.connection_id} closed the connection.")
|
|
||||||
else:
|
|
||||||
logger.exception(f"RuntimeError in client {client.connection_id}: {e}")
|
|
||||||
except CloseConnection as e:
|
|
||||||
if not e.from_client:
|
|
||||||
await client.send_packet(ClosePacket(error=e.message, allow_reconnect=e.allow_reconnect))
|
|
||||||
logger.info(f"Client {client.connection_id} closed the connection: {e.message}")
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f"Error in client {client.connection_id}")
|
|
||||||
|
|
||||||
await self.remove_client(client)
|
|
||||||
|
|
||||||
async def _handle_packet(self, client: Client, packet: Packet) -> None:
|
|
||||||
if isinstance(packet, PingPacket):
|
|
||||||
return
|
|
||||||
elif isinstance(packet, InvocationPacket):
|
|
||||||
args = packet.arguments or []
|
|
||||||
error = None
|
|
||||||
result = None
|
|
||||||
try:
|
|
||||||
result = await self.invoke_method(client, packet.target, args)
|
|
||||||
except InvokeException as e:
|
|
||||||
error = e.message
|
|
||||||
logger.debug(f"Client {client.connection_token} call {packet.target} failed: {error}")
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f"Error invoking method {packet.target} for client {client.connection_id}")
|
|
||||||
error = "Unknown error occured in server"
|
|
||||||
if packet.invocation_id is not None:
|
|
||||||
await self.send_packet(
|
|
||||||
client,
|
|
||||||
CompletionPacket(
|
|
||||||
invocation_id=packet.invocation_id,
|
|
||||||
error=error,
|
|
||||||
result=result,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
elif isinstance(packet, CompletionPacket):
|
|
||||||
client._store.add_result(packet.invocation_id, packet.result, packet.error)
|
|
||||||
|
|
||||||
async def invoke_method(self, client: Client, method: str, args: list[Any]) -> Any:
|
|
||||||
method_ = getattr(self, method, None)
|
|
||||||
call_params = []
|
|
||||||
if not method_:
|
|
||||||
raise InvokeException(f"Method '{method}' not found in hub.")
|
|
||||||
signature = get_signature(method_)
|
|
||||||
for name, param in signature.parameters.items():
|
|
||||||
if name == "self" or param.annotation is Client:
|
|
||||||
continue
|
|
||||||
call_params.append(client.protocol.validate_object(args.pop(0), param.annotation))
|
|
||||||
return await method_(client, *call_params)
|
|
||||||
|
|
||||||
async def call(self, client: Client, method: str, *args: Any) -> Any:
|
|
||||||
invocation_id = client._store.get_invocation_id()
|
|
||||||
await self.send_packet(
|
|
||||||
client,
|
|
||||||
InvocationPacket(
|
|
||||||
header={},
|
|
||||||
invocation_id=invocation_id,
|
|
||||||
target=method,
|
|
||||||
arguments=list(args),
|
|
||||||
stream_ids=None,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
r = await client._store.fetch(invocation_id, None)
|
|
||||||
if r[1]:
|
|
||||||
raise InvokeException(r[1])
|
|
||||||
return r[0]
|
|
||||||
|
|
||||||
async def call_noblock(self, client: Client, method: str, *args: Any) -> None:
|
|
||||||
await self.send_packet(
|
|
||||||
client,
|
|
||||||
InvocationPacket(
|
|
||||||
header={},
|
|
||||||
invocation_id=None,
|
|
||||||
target=method,
|
|
||||||
arguments=list(args),
|
|
||||||
stream_ids=None,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __contains__(self, item: str) -> bool:
|
|
||||||
return item in self.clients or item in self.waited_clients
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from collections import defaultdict
|
|
||||||
from collections.abc import Coroutine
|
|
||||||
import math
|
|
||||||
from typing import override
|
|
||||||
|
|
||||||
from app.calculator import clamp
|
|
||||||
from app.database import Relationship, RelationshipType, User
|
|
||||||
from app.database.playlist_best_score import PlaylistBestScore
|
|
||||||
from app.database.playlists import Playlist
|
|
||||||
from app.database.room import Room
|
|
||||||
from app.database.score import Score
|
|
||||||
from app.dependencies.database import with_db
|
|
||||||
from app.log import logger
|
|
||||||
from app.models.metadata_hub import (
|
|
||||||
TOTAL_SCORE_DISTRIBUTION_BINS,
|
|
||||||
DailyChallengeInfo,
|
|
||||||
MetadataClientState,
|
|
||||||
MultiplayerPlaylistItemStats,
|
|
||||||
MultiplayerRoomScoreSetEvent,
|
|
||||||
MultiplayerRoomStats,
|
|
||||||
OnlineStatus,
|
|
||||||
UserActivity,
|
|
||||||
)
|
|
||||||
from app.models.room import RoomCategory
|
|
||||||
from app.service.subscribers.score_processed import ScoreSubscriber
|
|
||||||
from app.utils import utcnow
|
|
||||||
|
|
||||||
from .hub import Client, Hub
|
|
||||||
|
|
||||||
from sqlmodel import col, select
|
|
||||||
|
|
||||||
ONLINE_PRESENCE_WATCHERS_GROUP = "metadata:online-presence-watchers"
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataHub(Hub[MetadataClientState]):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.subscriber = ScoreSubscriber()
|
|
||||||
self.subscriber.metadata_hub = self
|
|
||||||
self._daily_challenge_stats: MultiplayerRoomStats | None = None
|
|
||||||
self._today = utcnow().date()
|
|
||||||
self._lock = asyncio.Lock()
|
|
||||||
|
|
||||||
def get_daily_challenge_stats(self, daily_challenge_room: int) -> MultiplayerRoomStats:
|
|
||||||
if self._daily_challenge_stats is None or self._today != utcnow().date():
|
|
||||||
self._daily_challenge_stats = MultiplayerRoomStats(
|
|
||||||
room_id=daily_challenge_room,
|
|
||||||
playlist_item_stats={},
|
|
||||||
)
|
|
||||||
return self._daily_challenge_stats
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def online_presence_watchers_group() -> str:
|
|
||||||
return ONLINE_PRESENCE_WATCHERS_GROUP
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def room_watcher_group(room_id: int) -> str:
|
|
||||||
return f"metadata:multiplayer-room-watchers:{room_id}"
|
|
||||||
|
|
||||||
def broadcast_tasks(self, user_id: int, store: MetadataClientState | None) -> set[Coroutine]:
|
|
||||||
if store is not None and not store.pushable:
|
|
||||||
return set()
|
|
||||||
data = store.for_push if store else None
|
|
||||||
return {
|
|
||||||
self.broadcast_group_call(
|
|
||||||
self.online_presence_watchers_group(),
|
|
||||||
"UserPresenceUpdated",
|
|
||||||
user_id,
|
|
||||||
data,
|
|
||||||
),
|
|
||||||
self.broadcast_group_call(
|
|
||||||
self.friend_presence_watchers_group(user_id),
|
|
||||||
"FriendPresenceUpdated",
|
|
||||||
user_id,
|
|
||||||
data,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def friend_presence_watchers_group(user_id: int):
|
|
||||||
return f"metadata:friend-presence-watchers:{user_id}"
|
|
||||||
|
|
||||||
@override
|
|
||||||
async def _clean_state(self, state: MetadataClientState) -> None:
|
|
||||||
user_id = int(state.connection_id)
|
|
||||||
|
|
||||||
if state.pushable:
|
|
||||||
await asyncio.gather(*self.broadcast_tasks(user_id, None))
|
|
||||||
|
|
||||||
async with with_db() as session:
|
|
||||||
async with session.begin():
|
|
||||||
user = (await session.exec(select(User).where(User.id == int(state.connection_id)))).one()
|
|
||||||
user.last_visit = utcnow()
|
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
@override
|
|
||||||
def create_state(self, client: Client) -> MetadataClientState:
|
|
||||||
return MetadataClientState(
|
|
||||||
connection_id=client.connection_id,
|
|
||||||
connection_token=client.connection_token,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_client_connect(self, client: Client) -> None:
|
|
||||||
user_id = int(client.connection_id)
|
|
||||||
store = self.get_or_create_state(client)
|
|
||||||
|
|
||||||
# CRITICAL FIX: Set online status IMMEDIATELY upon connection
|
|
||||||
# This matches the C# official implementation behavior
|
|
||||||
store.status = OnlineStatus.ONLINE
|
|
||||||
logger.info(f"[MetadataHub] Set user {user_id} status to ONLINE upon connection")
|
|
||||||
|
|
||||||
async with with_db() as session:
|
|
||||||
async with session.begin():
|
|
||||||
friends = (
|
|
||||||
await session.exec(
|
|
||||||
select(Relationship.target_id).where(
|
|
||||||
Relationship.user_id == user_id,
|
|
||||||
Relationship.type == RelationshipType.FOLLOW,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).all()
|
|
||||||
tasks = []
|
|
||||||
for friend_id in friends:
|
|
||||||
self.groups.setdefault(self.friend_presence_watchers_group(friend_id), set()).add(client)
|
|
||||||
if (friend_state := self.state.get(friend_id)) and friend_state.pushable:
|
|
||||||
tasks.append(
|
|
||||||
self.broadcast_group_call(
|
|
||||||
self.friend_presence_watchers_group(friend_id),
|
|
||||||
"FriendPresenceUpdated",
|
|
||||||
friend_id,
|
|
||||||
friend_state.for_push if friend_state.pushable else None,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
daily_challenge_room = (
|
|
||||||
await session.exec(
|
|
||||||
select(Room).where(
|
|
||||||
col(Room.ends_at) > utcnow(),
|
|
||||||
Room.category == RoomCategory.DAILY_CHALLENGE,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
if daily_challenge_room:
|
|
||||||
await self.call_noblock(
|
|
||||||
client,
|
|
||||||
"DailyChallengeUpdated",
|
|
||||||
DailyChallengeInfo(
|
|
||||||
room_id=daily_challenge_room.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# CRITICAL FIX: Immediately broadcast the user's online status to all watchers
|
|
||||||
# This ensures the user appears as "currently online" right after connection
|
|
||||||
# Similar to the C# implementation's immediate broadcast logic
|
|
||||||
online_presence_tasks = self.broadcast_tasks(user_id, store)
|
|
||||||
if online_presence_tasks:
|
|
||||||
await asyncio.gather(*online_presence_tasks)
|
|
||||||
logger.info(f"[MetadataHub] Broadcasted online status for user {user_id} to watchers")
|
|
||||||
|
|
||||||
# Also send the user's own presence update to confirm online status
|
|
||||||
await self.call_noblock(
|
|
||||||
client,
|
|
||||||
"UserPresenceUpdated",
|
|
||||||
user_id,
|
|
||||||
store.for_push,
|
|
||||||
)
|
|
||||||
logger.info(f"[MetadataHub] User {user_id} is now ONLINE and visible to other clients")
|
|
||||||
|
|
||||||
async def UpdateStatus(self, client: Client, status: int) -> None:
|
|
||||||
status_ = OnlineStatus(status)
|
|
||||||
user_id = int(client.connection_id)
|
|
||||||
store = self.get_or_create_state(client)
|
|
||||||
if store.status is not None and store.status == status_:
|
|
||||||
return
|
|
||||||
store.status = OnlineStatus(status_)
|
|
||||||
tasks = self.broadcast_tasks(user_id, store)
|
|
||||||
tasks.add(
|
|
||||||
self.call_noblock(
|
|
||||||
client,
|
|
||||||
"UserPresenceUpdated",
|
|
||||||
user_id,
|
|
||||||
store.for_push,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def UpdateActivity(self, client: Client, activity: UserActivity | None) -> None:
|
|
||||||
user_id = int(client.connection_id)
|
|
||||||
store = self.get_or_create_state(client)
|
|
||||||
store.activity = activity
|
|
||||||
tasks = self.broadcast_tasks(user_id, store)
|
|
||||||
tasks.add(
|
|
||||||
self.call_noblock(
|
|
||||||
client,
|
|
||||||
"UserPresenceUpdated",
|
|
||||||
user_id,
|
|
||||||
store.for_push,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def BeginWatchingUserPresence(self, client: Client) -> None:
|
|
||||||
# Critical fix: Send all currently online users to the new watcher
|
|
||||||
# Must use for_push to get the correct UserPresence format
|
|
||||||
await asyncio.gather(
|
|
||||||
*[
|
|
||||||
self.call_noblock(
|
|
||||||
client,
|
|
||||||
"UserPresenceUpdated",
|
|
||||||
user_id,
|
|
||||||
store.for_push, # Fixed: use for_push instead of store
|
|
||||||
)
|
|
||||||
for user_id, store in self.state.items()
|
|
||||||
if store.pushable
|
|
||||||
]
|
|
||||||
)
|
|
||||||
self.add_to_group(client, self.online_presence_watchers_group())
|
|
||||||
logger.info(
|
|
||||||
f"[MetadataHub] Client {client.connection_id} now watching user presence, "
|
|
||||||
f"sent {len([s for s in self.state.values() if s.pushable])} online users"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def EndWatchingUserPresence(self, client: Client) -> None:
|
|
||||||
self.remove_from_group(client, self.online_presence_watchers_group())
|
|
||||||
|
|
||||||
async def notify_room_score_processed(self, event: MultiplayerRoomScoreSetEvent):
|
|
||||||
await self.broadcast_group_call(self.room_watcher_group(event.room_id), "MultiplayerRoomScoreSet", event)
|
|
||||||
|
|
||||||
async def BeginWatchingMultiplayerRoom(self, client: Client, room_id: int):
|
|
||||||
self.add_to_group(client, self.room_watcher_group(room_id))
|
|
||||||
await self.subscriber.subscribe_room_score(room_id, client.user_id)
|
|
||||||
stats = self.get_daily_challenge_stats(room_id)
|
|
||||||
await self.update_daily_challenge_stats(stats)
|
|
||||||
return list(stats.playlist_item_stats.values())
|
|
||||||
|
|
||||||
async def update_daily_challenge_stats(self, stats: MultiplayerRoomStats) -> None:
|
|
||||||
async with with_db() as session:
|
|
||||||
playlist_ids = (
|
|
||||||
await session.exec(
|
|
||||||
select(Playlist.id).where(
|
|
||||||
Playlist.room_id == stats.room_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).all()
|
|
||||||
for playlist_id in playlist_ids:
|
|
||||||
item = stats.playlist_item_stats.get(playlist_id, None)
|
|
||||||
if item is None:
|
|
||||||
item = MultiplayerPlaylistItemStats(
|
|
||||||
playlist_item_id=playlist_id,
|
|
||||||
total_score_distribution=[0] * TOTAL_SCORE_DISTRIBUTION_BINS,
|
|
||||||
cumulative_score=0,
|
|
||||||
last_processed_score_id=0,
|
|
||||||
)
|
|
||||||
stats.playlist_item_stats[playlist_id] = item
|
|
||||||
last_processed_score_id = item.last_processed_score_id
|
|
||||||
scores = (
|
|
||||||
await session.exec(
|
|
||||||
select(PlaylistBestScore).where(
|
|
||||||
PlaylistBestScore.room_id == stats.room_id,
|
|
||||||
PlaylistBestScore.playlist_id == playlist_id,
|
|
||||||
PlaylistBestScore.score_id > last_processed_score_id,
|
|
||||||
col(PlaylistBestScore.score).has(col(Score.passed).is_(True)),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).all()
|
|
||||||
if len(scores) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
async with self._lock:
|
|
||||||
if item.last_processed_score_id == last_processed_score_id:
|
|
||||||
totals = defaultdict(int)
|
|
||||||
for score in scores:
|
|
||||||
bin_index = int(
|
|
||||||
clamp(
|
|
||||||
math.floor(score.total_score / 100000),
|
|
||||||
0,
|
|
||||||
TOTAL_SCORE_DISTRIBUTION_BINS - 1,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
totals[bin_index] += 1
|
|
||||||
|
|
||||||
item.cumulative_score += sum(score.total_score for score in scores)
|
|
||||||
|
|
||||||
for j in range(TOTAL_SCORE_DISTRIBUTION_BINS):
|
|
||||||
item.total_score_distribution[j] += totals.get(j, 0)
|
|
||||||
|
|
||||||
if scores:
|
|
||||||
item.last_processed_score_id = max(score.score_id for score in scores)
|
|
||||||
|
|
||||||
async def EndWatchingMultiplayerRoom(self, client: Client, room_id: int):
|
|
||||||
self.remove_from_group(client, self.room_watcher_group(room_id))
|
|
||||||
await self.subscriber.unsubscribe_room_score(room_id, client.user_id)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,585 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import lzma
|
|
||||||
import struct
|
|
||||||
import time
|
|
||||||
from typing import override
|
|
||||||
|
|
||||||
from app.calculator import clamp
|
|
||||||
from app.config import settings
|
|
||||||
from app.database import Beatmap, User
|
|
||||||
from app.database.failtime import FailTime, FailTimeResp
|
|
||||||
from app.database.score import Score
|
|
||||||
from app.database.score_token import ScoreToken
|
|
||||||
from app.database.statistics import UserStatistics
|
|
||||||
from app.dependencies.database import get_redis, with_db
|
|
||||||
from app.dependencies.fetcher import get_fetcher
|
|
||||||
from app.dependencies.storage import get_storage_service
|
|
||||||
from app.exception import InvokeException
|
|
||||||
from app.log import logger
|
|
||||||
from app.models.mods import APIMod, mods_to_int
|
|
||||||
from app.models.score import GameMode, LegacyReplaySoloScoreInfo, ScoreStatistics
|
|
||||||
from app.models.spectator_hub import (
|
|
||||||
APIUser,
|
|
||||||
FrameDataBundle,
|
|
||||||
LegacyReplayFrame,
|
|
||||||
ScoreInfo,
|
|
||||||
SpectatedUserState,
|
|
||||||
SpectatorState,
|
|
||||||
StoreClientState,
|
|
||||||
StoreScore,
|
|
||||||
)
|
|
||||||
from app.utils import unix_timestamp_to_windows
|
|
||||||
|
|
||||||
from .hub import Client, Hub
|
|
||||||
|
|
||||||
from httpx import HTTPError
|
|
||||||
from sqlalchemy.orm import joinedload
|
|
||||||
from sqlmodel import select
|
|
||||||
|
|
||||||
READ_SCORE_TIMEOUT = 30
|
|
||||||
REPLAY_LATEST_VER = 30000016
|
|
||||||
|
|
||||||
|
|
||||||
def encode_uleb128(num: int) -> bytes | bytearray:
|
|
||||||
if num == 0:
|
|
||||||
return b"\x00"
|
|
||||||
|
|
||||||
ret = bytearray()
|
|
||||||
|
|
||||||
while num != 0:
|
|
||||||
ret.append(num & 0x7F)
|
|
||||||
num >>= 7
|
|
||||||
if num != 0:
|
|
||||||
ret[-1] |= 0x80
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def encode_string(s: str) -> bytes:
|
|
||||||
"""Write `s` into bytes (ULEB128 & string)."""
|
|
||||||
if s:
|
|
||||||
encoded = s.encode()
|
|
||||||
ret = b"\x0b" + encode_uleb128(len(encoded)) + encoded
|
|
||||||
else:
|
|
||||||
ret = b"\x00"
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
async def save_replay(
|
|
||||||
ruleset_id: int,
|
|
||||||
md5: str,
|
|
||||||
username: str,
|
|
||||||
score: Score,
|
|
||||||
statistics: ScoreStatistics,
|
|
||||||
maximum_statistics: ScoreStatistics,
|
|
||||||
frames: list[LegacyReplayFrame],
|
|
||||||
) -> None:
|
|
||||||
data = bytearray()
|
|
||||||
data.extend(struct.pack("<bi", ruleset_id, REPLAY_LATEST_VER))
|
|
||||||
data.extend(encode_string(md5))
|
|
||||||
data.extend(encode_string(username))
|
|
||||||
data.extend(encode_string(f"lazer-{username}-{score.started_at.isoformat()}"))
|
|
||||||
data.extend(
|
|
||||||
struct.pack(
|
|
||||||
"<hhhhhhihbi",
|
|
||||||
score.n300,
|
|
||||||
score.n100,
|
|
||||||
score.n50,
|
|
||||||
score.ngeki,
|
|
||||||
score.nkatu,
|
|
||||||
score.nmiss,
|
|
||||||
score.total_score,
|
|
||||||
score.max_combo,
|
|
||||||
score.is_perfect_combo,
|
|
||||||
mods_to_int(score.mods),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
data.extend(encode_string("")) # hp graph
|
|
||||||
data.extend(
|
|
||||||
struct.pack(
|
|
||||||
"<q",
|
|
||||||
unix_timestamp_to_windows(round(score.started_at.timestamp())),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# write frames
|
|
||||||
frame_strs = []
|
|
||||||
last_time = 0
|
|
||||||
for frame in frames:
|
|
||||||
time = round(frame.time)
|
|
||||||
frame_strs.append(f"{time - last_time}|{frame.mouse_x or 0.0}|{frame.mouse_y or 0.0}|{frame.button_state}")
|
|
||||||
last_time = time
|
|
||||||
frame_strs.append("-12345|0|0|0")
|
|
||||||
|
|
||||||
compressed = lzma.compress(",".join(frame_strs).encode("ascii"), format=lzma.FORMAT_ALONE)
|
|
||||||
data.extend(struct.pack("<i", len(compressed)))
|
|
||||||
data.extend(compressed)
|
|
||||||
data.extend(struct.pack("<q", score.id))
|
|
||||||
score_info = LegacyReplaySoloScoreInfo(
|
|
||||||
online_id=score.id,
|
|
||||||
mods=score.mods,
|
|
||||||
statistics=statistics,
|
|
||||||
maximum_statistics=maximum_statistics,
|
|
||||||
client_version="",
|
|
||||||
rank=score.rank,
|
|
||||||
user_id=score.user_id,
|
|
||||||
total_score_without_mods=score.total_score_without_mods,
|
|
||||||
)
|
|
||||||
compressed = lzma.compress(json.dumps(score_info).encode(), format=lzma.FORMAT_ALONE)
|
|
||||||
data.extend(struct.pack("<i", len(compressed)))
|
|
||||||
data.extend(compressed)
|
|
||||||
|
|
||||||
storage_service = get_storage_service()
|
|
||||||
replay_path = score.replay_filename
|
|
||||||
await storage_service.write_file(replay_path, bytes(data), "application/x-osu-replay")
|
|
||||||
|
|
||||||
|
|
||||||
class SpectatorHub(Hub[StoreClientState]):
|
|
||||||
@staticmethod
|
|
||||||
def group_id(user_id: int) -> str:
|
|
||||||
return f"watch:{user_id}"
|
|
||||||
|
|
||||||
@override
|
|
||||||
def create_state(self, client: Client) -> StoreClientState:
|
|
||||||
return StoreClientState(
|
|
||||||
connection_id=client.connection_id,
|
|
||||||
connection_token=client.connection_token,
|
|
||||||
)
|
|
||||||
|
|
||||||
@override
|
|
||||||
async def _clean_state(self, state: StoreClientState) -> None:
|
|
||||||
"""
|
|
||||||
Enhanced cleanup based on official osu-server-spectator implementation.
|
|
||||||
Properly notifies watched users when spectator disconnects.
|
|
||||||
"""
|
|
||||||
user_id = int(state.connection_id)
|
|
||||||
if state.state:
|
|
||||||
await self._end_session(user_id, state.state, state)
|
|
||||||
|
|
||||||
# Critical fix: Notify all watched users that this spectator has disconnected
|
|
||||||
# This matches the official CleanUpState implementation
|
|
||||||
for watched_user_id in state.watched_user:
|
|
||||||
if (target_client := self.get_client_by_id(str(watched_user_id))) is not None:
|
|
||||||
await self.call_noblock(target_client, "UserEndedWatching", user_id)
|
|
||||||
logger.debug(f"[SpectatorHub] Notified {watched_user_id} that {user_id} stopped watching")
|
|
||||||
|
|
||||||
async def on_client_connect(self, client: Client) -> None:
|
|
||||||
"""
|
|
||||||
Enhanced connection handling based on official implementation.
|
|
||||||
Send all active player states to newly connected clients.
|
|
||||||
"""
|
|
||||||
logger.info(f"[SpectatorHub] Client {client.user_id} connected")
|
|
||||||
|
|
||||||
# Send all current player states to the new client
|
|
||||||
# This matches the official OnConnectedAsync behavior
|
|
||||||
active_states = []
|
|
||||||
for user_id, store in self.state.items():
|
|
||||||
if store.state is not None:
|
|
||||||
active_states.append((user_id, store.state))
|
|
||||||
|
|
||||||
if active_states:
|
|
||||||
logger.debug(f"[SpectatorHub] Sending {len(active_states)} active player states to {client.user_id}")
|
|
||||||
# Send states sequentially to avoid overwhelming the client
|
|
||||||
for user_id, state in active_states:
|
|
||||||
try:
|
|
||||||
await self.call_noblock(client, "UserBeganPlaying", user_id, state)
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"[SpectatorHub] Failed to send state for user {user_id}: {e}")
|
|
||||||
|
|
||||||
# Also sync with MultiplayerHub for cross-hub spectating
|
|
||||||
await self._sync_with_multiplayer_hub(client)
|
|
||||||
|
|
||||||
async def _sync_with_multiplayer_hub(self, client: Client) -> None:
|
|
||||||
"""
|
|
||||||
Sync with MultiplayerHub to get active multiplayer game states.
|
|
||||||
This ensures spectators can see multiplayer games from other pages.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Import here to avoid circular imports
|
|
||||||
from app.signalr.hub import MultiplayerHubs
|
|
||||||
|
|
||||||
# Check all active multiplayer rooms for playing users
|
|
||||||
for room_id, server_room in MultiplayerHubs.rooms.items():
|
|
||||||
for room_user in server_room.room.users:
|
|
||||||
# Send state for users who are playing or in results
|
|
||||||
if room_user.state.is_playing and room_user.user_id not in self.state:
|
|
||||||
# Create a synthetic SpectatorState for multiplayer players
|
|
||||||
# This helps with cross-hub spectating
|
|
||||||
try:
|
|
||||||
synthetic_state = SpectatorState(
|
|
||||||
beatmap_id=server_room.queue.current_item.beatmap_id,
|
|
||||||
ruleset_id=room_user.ruleset_id or 0, # Default to osu!
|
|
||||||
mods=room_user.mods,
|
|
||||||
state=SpectatedUserState.Playing,
|
|
||||||
maximum_statistics={},
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.call_noblock(
|
|
||||||
client,
|
|
||||||
"UserBeganPlaying",
|
|
||||||
room_user.user_id,
|
|
||||||
synthetic_state,
|
|
||||||
)
|
|
||||||
logger.debug(
|
|
||||||
f"[SpectatorHub] Sent synthetic multiplayer state for user {room_user.user_id}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"[SpectatorHub] Failed to create synthetic state: {e}")
|
|
||||||
|
|
||||||
# Critical addition: Notify about finished players in multiplayer games
|
|
||||||
elif (
|
|
||||||
hasattr(room_user.state, "name")
|
|
||||||
and room_user.state.name == "RESULTS"
|
|
||||||
and room_user.user_id not in self.state
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
# Create a synthetic finished state
|
|
||||||
finished_state = SpectatorState(
|
|
||||||
beatmap_id=server_room.queue.current_item.beatmap_id,
|
|
||||||
ruleset_id=room_user.ruleset_id or 0,
|
|
||||||
mods=room_user.mods,
|
|
||||||
state=SpectatedUserState.Passed, # Assume passed for results
|
|
||||||
maximum_statistics={},
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.call_noblock(
|
|
||||||
client,
|
|
||||||
"UserFinishedPlaying",
|
|
||||||
room_user.user_id,
|
|
||||||
finished_state,
|
|
||||||
)
|
|
||||||
logger.debug(f"[SpectatorHub] Sent synthetic finished state for user {room_user.user_id}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"[SpectatorHub] Failed to create synthetic finished state: {e}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"[SpectatorHub] Failed to sync with MultiplayerHub: {e}")
|
|
||||||
# This is not critical, so we don't raise the exception
|
|
||||||
|
|
||||||
async def BeginPlaySession(self, client: Client, score_token: int, state: SpectatorState) -> None:
|
|
||||||
user_id = int(client.connection_id)
|
|
||||||
store = self.get_or_create_state(client)
|
|
||||||
if store.state is not None:
|
|
||||||
logger.warning(f"[SpectatorHub] User {user_id} began new session without ending previous one; cleaning up")
|
|
||||||
try:
|
|
||||||
await self._end_session(user_id, store.state, store)
|
|
||||||
finally:
|
|
||||||
store.state = None
|
|
||||||
store.beatmap_status = None
|
|
||||||
store.checksum = None
|
|
||||||
store.ruleset_id = None
|
|
||||||
store.score_token = None
|
|
||||||
store.score = None
|
|
||||||
if state.beatmap_id is None or state.ruleset_id is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
fetcher = await get_fetcher()
|
|
||||||
async with with_db() as session:
|
|
||||||
async with session.begin():
|
|
||||||
try:
|
|
||||||
beatmap = await Beatmap.get_or_fetch(session, fetcher, bid=state.beatmap_id)
|
|
||||||
except HTTPError:
|
|
||||||
raise InvokeException(f"Beatmap {state.beatmap_id} not found.")
|
|
||||||
user = (await session.exec(select(User).where(User.id == user_id))).first()
|
|
||||||
if not user:
|
|
||||||
return
|
|
||||||
name = user.username
|
|
||||||
store.state = state
|
|
||||||
store.beatmap_status = beatmap.beatmap_status
|
|
||||||
store.checksum = beatmap.checksum
|
|
||||||
store.ruleset_id = state.ruleset_id
|
|
||||||
store.score_token = score_token
|
|
||||||
store.score = StoreScore(
|
|
||||||
score_info=ScoreInfo(
|
|
||||||
mods=state.mods,
|
|
||||||
user=APIUser(id=user_id, name=name),
|
|
||||||
ruleset=state.ruleset_id,
|
|
||||||
maximum_statistics=state.maximum_statistics,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
logger.info(f"[SpectatorHub] {client.user_id} began playing {state.beatmap_id}")
|
|
||||||
|
|
||||||
await self.broadcast_group_call(
|
|
||||||
self.group_id(user_id),
|
|
||||||
"UserBeganPlaying",
|
|
||||||
user_id,
|
|
||||||
state,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def SendFrameData(self, client: Client, frame_data: FrameDataBundle) -> None:
|
|
||||||
user_id = int(client.connection_id)
|
|
||||||
store = self.get_or_create_state(client)
|
|
||||||
if store.state is None or store.score is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
header = frame_data.header
|
|
||||||
score_info = store.score.score_info
|
|
||||||
score_info.accuracy = header.accuracy
|
|
||||||
score_info.combo = header.combo
|
|
||||||
score_info.max_combo = header.max_combo
|
|
||||||
score_info.statistics = header.statistics
|
|
||||||
store.score.replay_frames.extend(frame_data.frames)
|
|
||||||
|
|
||||||
await self.broadcast_group_call(self.group_id(user_id), "UserSentFrames", user_id, frame_data)
|
|
||||||
|
|
||||||
async def EndPlaySession(self, client: Client, state: SpectatorState) -> None:
|
|
||||||
user_id = int(client.connection_id)
|
|
||||||
store = self.get_or_create_state(client)
|
|
||||||
score = store.score
|
|
||||||
|
|
||||||
# Early return if no active session
|
|
||||||
if (
|
|
||||||
score is None
|
|
||||||
or store.score_token is None
|
|
||||||
or store.beatmap_status is None
|
|
||||||
or store.state is None
|
|
||||||
or store.score is None
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Process score if conditions are met
|
|
||||||
if (settings.enable_all_beatmap_leaderboard and store.beatmap_status.has_leaderboard()) and any(
|
|
||||||
k.is_hit() and v > 0 for k, v in score.score_info.statistics.items()
|
|
||||||
):
|
|
||||||
await self._process_score(store, client)
|
|
||||||
|
|
||||||
# End the play session and notify watchers
|
|
||||||
await self._end_session(user_id, state, store)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# CRITICAL FIX: Always clear state in finally block to ensure cleanup
|
|
||||||
# This matches the official C# implementation pattern
|
|
||||||
store.state = None
|
|
||||||
store.beatmap_status = None
|
|
||||||
store.checksum = None
|
|
||||||
store.ruleset_id = None
|
|
||||||
store.score_token = None
|
|
||||||
store.score = None
|
|
||||||
logger.info(f"[SpectatorHub] Cleared all session state for user {user_id}")
|
|
||||||
|
|
||||||
async def _process_score(self, store: StoreClientState, client: Client) -> None:
|
|
||||||
user_id = int(client.connection_id)
|
|
||||||
assert store.state is not None
|
|
||||||
assert store.score_token is not None
|
|
||||||
assert store.checksum is not None
|
|
||||||
assert store.ruleset_id is not None
|
|
||||||
assert store.score is not None
|
|
||||||
async with with_db() as session:
|
|
||||||
async with session:
|
|
||||||
start_time = time.time()
|
|
||||||
score_record = None
|
|
||||||
while time.time() - start_time < READ_SCORE_TIMEOUT:
|
|
||||||
sub_query = select(ScoreToken.score_id).where(
|
|
||||||
ScoreToken.id == store.score_token,
|
|
||||||
)
|
|
||||||
result = await session.exec(
|
|
||||||
select(Score)
|
|
||||||
.options(joinedload(Score.beatmap))
|
|
||||||
.where(
|
|
||||||
Score.id == sub_query.scalar_subquery(),
|
|
||||||
Score.user_id == user_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
score_record = result.first()
|
|
||||||
if score_record:
|
|
||||||
break
|
|
||||||
if not score_record:
|
|
||||||
return
|
|
||||||
if not score_record.passed:
|
|
||||||
return
|
|
||||||
await self.call_noblock(
|
|
||||||
client,
|
|
||||||
"UserScoreProcessed",
|
|
||||||
user_id,
|
|
||||||
score_record.id,
|
|
||||||
)
|
|
||||||
# save replay
|
|
||||||
score_record.has_replay = True
|
|
||||||
await session.commit()
|
|
||||||
await session.refresh(score_record)
|
|
||||||
await save_replay(
|
|
||||||
ruleset_id=store.ruleset_id,
|
|
||||||
md5=store.checksum,
|
|
||||||
username=store.score.score_info.user.name,
|
|
||||||
score=score_record,
|
|
||||||
statistics=store.score.score_info.statistics,
|
|
||||||
maximum_statistics=store.score.score_info.maximum_statistics,
|
|
||||||
frames=store.score.replay_frames,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _end_session(self, user_id: int, state: SpectatorState, store: StoreClientState) -> None:
|
|
||||||
async def _add_failtime():
|
|
||||||
async with with_db() as session:
|
|
||||||
failtime = await session.get(FailTime, state.beatmap_id)
|
|
||||||
total_length = (
|
|
||||||
await session.exec(select(Beatmap.total_length).where(Beatmap.id == state.beatmap_id))
|
|
||||||
).one()
|
|
||||||
index = clamp(round((exit_time / total_length) * 100), 0, 99)
|
|
||||||
if failtime is not None:
|
|
||||||
resp = FailTimeResp.from_db(failtime)
|
|
||||||
else:
|
|
||||||
resp = FailTimeResp()
|
|
||||||
if state.state == SpectatedUserState.Failed:
|
|
||||||
resp.fail[index] += 1
|
|
||||||
elif state.state == SpectatedUserState.Quit:
|
|
||||||
resp.exit[index] += 1
|
|
||||||
|
|
||||||
assert state.beatmap_id
|
|
||||||
new_failtime = FailTime.from_resp(state.beatmap_id, resp)
|
|
||||||
if failtime is not None:
|
|
||||||
await session.merge(new_failtime)
|
|
||||||
else:
|
|
||||||
session.add(new_failtime)
|
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
async def _edit_playtime(token: int, ruleset_id: int, mods: list[APIMod]):
|
|
||||||
redis = get_redis()
|
|
||||||
key = f"score:existed_time:{token}"
|
|
||||||
messages = await redis.xrange(key, min="-", max="+", count=1)
|
|
||||||
if not messages:
|
|
||||||
return
|
|
||||||
before_time = int(messages[0][1]["time"])
|
|
||||||
await redis.delete(key)
|
|
||||||
async with with_db() as session:
|
|
||||||
gamemode = GameMode.from_int(ruleset_id).to_special_mode(mods)
|
|
||||||
statistics = (
|
|
||||||
await session.exec(
|
|
||||||
select(UserStatistics).where(
|
|
||||||
UserStatistics.user_id == user_id,
|
|
||||||
UserStatistics.mode == gamemode,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
if statistics is None:
|
|
||||||
return
|
|
||||||
statistics.play_time -= before_time
|
|
||||||
statistics.play_time += round(min(before_time, exit_time))
|
|
||||||
|
|
||||||
if state.state == SpectatedUserState.Playing:
|
|
||||||
state.state = SpectatedUserState.Quit
|
|
||||||
logger.debug(f"[SpectatorHub] Changed state from Playing to Quit for user {user_id}")
|
|
||||||
|
|
||||||
# Calculate exit time safely
|
|
||||||
exit_time = 0
|
|
||||||
if store.score and store.score.replay_frames:
|
|
||||||
exit_time = max(frame.time for frame in store.score.replay_frames) // 1000
|
|
||||||
|
|
||||||
# Background task for playtime editing - only if we have valid data
|
|
||||||
if store.score_token and store.ruleset_id and store.score:
|
|
||||||
task = asyncio.create_task(
|
|
||||||
_edit_playtime(
|
|
||||||
store.score_token,
|
|
||||||
store.ruleset_id,
|
|
||||||
store.score.score_info.mods,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.tasks.add(task)
|
|
||||||
task.add_done_callback(self.tasks.discard)
|
|
||||||
|
|
||||||
# Background task for failtime tracking - only for failed/quit states with valid data
|
|
||||||
if (
|
|
||||||
state.beatmap_id is not None
|
|
||||||
and exit_time > 0
|
|
||||||
and state.state in (SpectatedUserState.Failed, SpectatedUserState.Quit)
|
|
||||||
):
|
|
||||||
task = asyncio.create_task(_add_failtime())
|
|
||||||
self.tasks.add(task)
|
|
||||||
task.add_done_callback(self.tasks.discard)
|
|
||||||
|
|
||||||
logger.info(f"[SpectatorHub] {user_id} finished playing {state.beatmap_id} with {state.state}")
|
|
||||||
await self.broadcast_group_call(
|
|
||||||
self.group_id(user_id),
|
|
||||||
"UserFinishedPlaying",
|
|
||||||
user_id,
|
|
||||||
state,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def StartWatchingUser(self, client: Client, target_id: int) -> None:
|
|
||||||
"""
|
|
||||||
Enhanced StartWatchingUser based on official osu-server-spectator implementation.
|
|
||||||
Properly handles state synchronization and watcher notifications.
|
|
||||||
"""
|
|
||||||
user_id = int(client.connection_id)
|
|
||||||
|
|
||||||
logger.info(f"[SpectatorHub] {user_id} started watching {target_id}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get target user's current state if it exists
|
|
||||||
target_store = self.state.get(target_id)
|
|
||||||
if not target_store or not target_store.state:
|
|
||||||
logger.info(f"[SpectatorHub] Rejecting watch request for {target_id}: user not playing")
|
|
||||||
raise InvokeException("Target user is not currently playing")
|
|
||||||
|
|
||||||
if target_store.state.state != SpectatedUserState.Playing:
|
|
||||||
logger.info(
|
|
||||||
f"[SpectatorHub] Rejecting watch request for {target_id}: state is {target_store.state.state}"
|
|
||||||
)
|
|
||||||
raise InvokeException("Target user is not currently playing")
|
|
||||||
|
|
||||||
logger.debug(f"[SpectatorHub] {target_id} is currently playing, sending state")
|
|
||||||
# Send current state to the watcher immediately
|
|
||||||
await self.call_noblock(
|
|
||||||
client,
|
|
||||||
"UserBeganPlaying",
|
|
||||||
target_id,
|
|
||||||
target_store.state,
|
|
||||||
)
|
|
||||||
except InvokeException:
|
|
||||||
# Re-raise to inform caller without adding to group
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
# User isn't tracked or error occurred - this is not critical
|
|
||||||
logger.debug(f"[SpectatorHub] Could not get state for {target_id}: {e}")
|
|
||||||
raise InvokeException("Target user is not currently playing") from e
|
|
||||||
|
|
||||||
# Add watcher to our tracked users only after validation
|
|
||||||
store = self.get_or_create_state(client)
|
|
||||||
store.watched_user.add(target_id)
|
|
||||||
|
|
||||||
# Add to SignalR group for this target user
|
|
||||||
self.add_to_group(client, self.group_id(target_id))
|
|
||||||
|
|
||||||
# Get watcher's username and notify the target user
|
|
||||||
try:
|
|
||||||
async with with_db() as session:
|
|
||||||
username = (await session.exec(select(User.username).where(User.id == user_id))).first()
|
|
||||||
if not username:
|
|
||||||
logger.warning(f"[SpectatorHub] Could not find username for user {user_id}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Notify target user that someone started watching
|
|
||||||
if (target_client := self.get_client_by_id(str(target_id))) is not None:
|
|
||||||
# Create watcher info array (matches official format)
|
|
||||||
watcher_info = [[user_id, username]]
|
|
||||||
await self.call_noblock(target_client, "UserStartedWatching", watcher_info)
|
|
||||||
logger.debug(f"[SpectatorHub] Notified {target_id} that {username} started watching")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[SpectatorHub] Error notifying target user {target_id}: {e}")
|
|
||||||
|
|
||||||
async def EndWatchingUser(self, client: Client, target_id: int) -> None:
|
|
||||||
"""
|
|
||||||
Enhanced EndWatchingUser based on official osu-server-spectator implementation.
|
|
||||||
Properly cleans up watcher state and notifies target user.
|
|
||||||
"""
|
|
||||||
user_id = int(client.connection_id)
|
|
||||||
|
|
||||||
logger.info(f"[SpectatorHub] {user_id} ended watching {target_id}")
|
|
||||||
|
|
||||||
# Remove from SignalR group
|
|
||||||
self.remove_from_group(client, self.group_id(target_id))
|
|
||||||
|
|
||||||
# Remove from our tracked watched users
|
|
||||||
store = self.get_or_create_state(client)
|
|
||||||
store.watched_user.discard(target_id)
|
|
||||||
|
|
||||||
# Notify target user that watcher stopped watching
|
|
||||||
if (target_client := self.get_client_by_id(str(target_id))) is not None:
|
|
||||||
await self.call_noblock(target_client, "UserEndedWatching", user_id)
|
|
||||||
logger.debug(f"[SpectatorHub] Notified {target_id} that {user_id} stopped watching")
|
|
||||||
else:
|
|
||||||
logger.debug(f"[SpectatorHub] Target user {target_id} not found for end watching notification")
|
|
||||||
@@ -1,492 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import datetime
|
|
||||||
from enum import Enum, IntEnum
|
|
||||||
import inspect
|
|
||||||
import json
|
|
||||||
from types import NoneType, UnionType
|
|
||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
Protocol as TypingProtocol,
|
|
||||||
Union,
|
|
||||||
get_args,
|
|
||||||
get_origin,
|
|
||||||
)
|
|
||||||
|
|
||||||
from app.models.signalr import SignalRMeta, SignalRUnionMessage
|
|
||||||
from app.utils import camel_to_snake, snake_to_camel, snake_to_pascal
|
|
||||||
|
|
||||||
import msgpack_lazer_api as m
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
SEP = b"\x1e"
|
|
||||||
|
|
||||||
|
|
||||||
class PacketType(IntEnum):
|
|
||||||
INVOCATION = 1
|
|
||||||
STREAM_ITEM = 2
|
|
||||||
COMPLETION = 3
|
|
||||||
STREAM_INVOCATION = 4
|
|
||||||
CANCEL_INVOCATION = 5
|
|
||||||
PING = 6
|
|
||||||
CLOSE = 7
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Packet:
|
|
||||||
type: PacketType
|
|
||||||
header: dict[str, Any] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class InvocationPacket(Packet):
|
|
||||||
type: PacketType = PacketType.INVOCATION
|
|
||||||
invocation_id: str | None
|
|
||||||
target: str
|
|
||||||
arguments: list[Any] | None = None
|
|
||||||
stream_ids: list[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class CompletionPacket(Packet):
|
|
||||||
type: PacketType = PacketType.COMPLETION
|
|
||||||
invocation_id: str
|
|
||||||
result: Any
|
|
||||||
error: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class PingPacket(Packet):
|
|
||||||
type: PacketType = PacketType.PING
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class ClosePacket(Packet):
|
|
||||||
type: PacketType = PacketType.CLOSE
|
|
||||||
error: str | None = None
|
|
||||||
allow_reconnect: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
PACKETS = {
|
|
||||||
PacketType.INVOCATION: InvocationPacket,
|
|
||||||
PacketType.COMPLETION: CompletionPacket,
|
|
||||||
PacketType.PING: PingPacket,
|
|
||||||
PacketType.CLOSE: ClosePacket,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Protocol(TypingProtocol):
|
|
||||||
@staticmethod
|
|
||||||
def decode(input: bytes) -> list[Packet]: ...
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def encode(packet: Packet) -> bytes: ...
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate_object(cls, v: Any, typ: type) -> Any: ...
|
|
||||||
|
|
||||||
|
|
||||||
class MsgpackProtocol:
|
|
||||||
@classmethod
|
|
||||||
def serialize_msgpack(cls, v: Any) -> Any:
|
|
||||||
typ = v.__class__
|
|
||||||
if issubclass(typ, BaseModel):
|
|
||||||
return cls.serialize_to_list(v)
|
|
||||||
elif issubclass(typ, list):
|
|
||||||
return [cls.serialize_msgpack(item) for item in v]
|
|
||||||
elif issubclass(typ, datetime.datetime):
|
|
||||||
return [v, 0]
|
|
||||||
elif issubclass(typ, datetime.timedelta):
|
|
||||||
return int(v.total_seconds() * 10_000_000)
|
|
||||||
elif isinstance(v, dict):
|
|
||||||
return {cls.serialize_msgpack(k): cls.serialize_msgpack(value) for k, value in v.items()}
|
|
||||||
elif issubclass(typ, Enum):
|
|
||||||
list_ = list(typ)
|
|
||||||
return list_.index(v) if v in list_ else v.value
|
|
||||||
return v
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def serialize_to_list(cls, value: BaseModel) -> list[Any]:
|
|
||||||
values = []
|
|
||||||
for field, info in value.__class__.model_fields.items():
|
|
||||||
metadata = next((m for m in info.metadata if isinstance(m, SignalRMeta)), None)
|
|
||||||
if metadata and metadata.member_ignore:
|
|
||||||
continue
|
|
||||||
values.append(cls.serialize_msgpack(v=getattr(value, field)))
|
|
||||||
if issubclass(value.__class__, SignalRUnionMessage):
|
|
||||||
return [value.__class__.union_type, values]
|
|
||||||
else:
|
|
||||||
return values
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def process_object(v: Any, typ: type[BaseModel]) -> Any:
|
|
||||||
if isinstance(v, list):
|
|
||||||
d = {}
|
|
||||||
i = 0
|
|
||||||
for field, info in typ.model_fields.items():
|
|
||||||
metadata = next((m for m in info.metadata if isinstance(m, SignalRMeta)), None)
|
|
||||||
if metadata and metadata.member_ignore:
|
|
||||||
continue
|
|
||||||
anno = info.annotation
|
|
||||||
if anno is None:
|
|
||||||
d[camel_to_snake(field)] = v[i]
|
|
||||||
else:
|
|
||||||
d[field] = MsgpackProtocol.validate_object(v[i], anno)
|
|
||||||
i += 1
|
|
||||||
return d
|
|
||||||
return v
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _encode_varint(value: int) -> bytes:
|
|
||||||
result = []
|
|
||||||
while value >= 0x80:
|
|
||||||
result.append((value & 0x7F) | 0x80)
|
|
||||||
value >>= 7
|
|
||||||
result.append(value & 0x7F)
|
|
||||||
return bytes(result)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _decode_varint(data: bytes, offset: int = 0) -> tuple[int, int]:
|
|
||||||
result = 0
|
|
||||||
shift = 0
|
|
||||||
pos = offset
|
|
||||||
|
|
||||||
while pos < len(data):
|
|
||||||
byte = data[pos]
|
|
||||||
result |= (byte & 0x7F) << shift
|
|
||||||
pos += 1
|
|
||||||
if (byte & 0x80) == 0:
|
|
||||||
break
|
|
||||||
shift += 7
|
|
||||||
|
|
||||||
return result, pos
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def decode(input: bytes) -> list[Packet]:
|
|
||||||
length, offset = MsgpackProtocol._decode_varint(input)
|
|
||||||
message_data = input[offset : offset + length]
|
|
||||||
unpacked = m.decode(message_data)
|
|
||||||
packet_type = PacketType(unpacked[0])
|
|
||||||
if packet_type not in PACKETS:
|
|
||||||
raise ValueError(f"Unknown packet type: {packet_type}")
|
|
||||||
match packet_type:
|
|
||||||
case PacketType.INVOCATION:
|
|
||||||
return [
|
|
||||||
InvocationPacket(
|
|
||||||
header=unpacked[1],
|
|
||||||
invocation_id=unpacked[2],
|
|
||||||
target=unpacked[3],
|
|
||||||
arguments=unpacked[4] if len(unpacked) > 4 else None,
|
|
||||||
stream_ids=unpacked[5] if len(unpacked) > 5 else None,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
case PacketType.COMPLETION:
|
|
||||||
result_kind = unpacked[3]
|
|
||||||
return [
|
|
||||||
CompletionPacket(
|
|
||||||
header=unpacked[1],
|
|
||||||
invocation_id=unpacked[2],
|
|
||||||
error=unpacked[4] if result_kind == 1 else None,
|
|
||||||
result=unpacked[5] if result_kind == 3 else None,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
case PacketType.PING:
|
|
||||||
return [PingPacket()]
|
|
||||||
case PacketType.CLOSE:
|
|
||||||
return [
|
|
||||||
ClosePacket(
|
|
||||||
error=unpacked[1],
|
|
||||||
allow_reconnect=unpacked[2] if len(unpacked) > 2 else False,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
raise ValueError(f"Unsupported packet type: {packet_type}")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate_object(cls, v: Any, typ: type) -> Any:
|
|
||||||
if issubclass(typ, BaseModel):
|
|
||||||
return typ.model_validate(obj=cls.process_object(v, typ))
|
|
||||||
elif inspect.isclass(typ) and issubclass(typ, datetime.datetime):
|
|
||||||
return v[0]
|
|
||||||
elif inspect.isclass(typ) and issubclass(typ, datetime.timedelta):
|
|
||||||
return datetime.timedelta(seconds=int(v / 10_000_000))
|
|
||||||
elif get_origin(typ) is list:
|
|
||||||
return [cls.validate_object(item, get_args(typ)[0]) for item in v]
|
|
||||||
elif inspect.isclass(typ) and issubclass(typ, Enum):
|
|
||||||
list_ = list(typ)
|
|
||||||
return list_[v] if isinstance(v, int) and 0 <= v < len(list_) else typ(v)
|
|
||||||
elif get_origin(typ) is dict:
|
|
||||||
return {
|
|
||||||
cls.validate_object(k, get_args(typ)[0]): cls.validate_object(v, get_args(typ)[1]) for k, v in v.items()
|
|
||||||
}
|
|
||||||
elif (origin := get_origin(typ)) is Union or origin is UnionType:
|
|
||||||
args = get_args(typ)
|
|
||||||
if len(args) == 2 and NoneType in args:
|
|
||||||
non_none_args = [arg for arg in args if arg is not NoneType]
|
|
||||||
if len(non_none_args) == 1:
|
|
||||||
if v is None:
|
|
||||||
return None
|
|
||||||
return cls.validate_object(v, non_none_args[0])
|
|
||||||
|
|
||||||
# suppose use `MessagePack-CSharp Union | None`
|
|
||||||
# except `X (Other Type) | None`
|
|
||||||
if NoneType in args and v is None:
|
|
||||||
return None
|
|
||||||
if not all(issubclass(arg, SignalRUnionMessage) or arg is NoneType for arg in args):
|
|
||||||
raise ValueError(f"Cannot validate {v} to {typ}, only SignalRUnionMessage subclasses are supported")
|
|
||||||
union_type = v[0]
|
|
||||||
for arg in args:
|
|
||||||
assert issubclass(arg, SignalRUnionMessage)
|
|
||||||
if arg.union_type == union_type:
|
|
||||||
return cls.validate_object(v[1], arg)
|
|
||||||
return v
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def encode(packet: Packet) -> bytes:
|
|
||||||
payload = [packet.type.value, packet.header or {}]
|
|
||||||
if isinstance(packet, InvocationPacket):
|
|
||||||
payload.extend(
|
|
||||||
[
|
|
||||||
packet.invocation_id,
|
|
||||||
packet.target,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
if packet.arguments is not None:
|
|
||||||
payload.append([MsgpackProtocol.serialize_msgpack(arg) for arg in packet.arguments])
|
|
||||||
if packet.stream_ids is not None:
|
|
||||||
payload.append(packet.stream_ids)
|
|
||||||
elif isinstance(packet, CompletionPacket):
|
|
||||||
result_kind = 2
|
|
||||||
if packet.error:
|
|
||||||
result_kind = 1
|
|
||||||
elif packet.result is not None:
|
|
||||||
result_kind = 3
|
|
||||||
payload.extend(
|
|
||||||
[
|
|
||||||
packet.invocation_id,
|
|
||||||
result_kind,
|
|
||||||
packet.error or MsgpackProtocol.serialize_msgpack(packet.result) or None,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
elif isinstance(packet, ClosePacket):
|
|
||||||
payload.extend(
|
|
||||||
[
|
|
||||||
packet.error or "",
|
|
||||||
packet.allow_reconnect,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
elif isinstance(packet, PingPacket):
|
|
||||||
payload.pop(-1)
|
|
||||||
data = m.encode(payload)
|
|
||||||
return MsgpackProtocol._encode_varint(len(data)) + data
|
|
||||||
|
|
||||||
|
|
||||||
class JSONProtocol:
|
|
||||||
@classmethod
|
|
||||||
def serialize_to_json(cls, v: Any, dict_key: bool = False, in_union: bool = False):
|
|
||||||
typ = v.__class__
|
|
||||||
if issubclass(typ, BaseModel):
|
|
||||||
return cls.serialize_model(v, in_union)
|
|
||||||
elif isinstance(v, dict):
|
|
||||||
return {cls.serialize_to_json(k, True): cls.serialize_to_json(value) for k, value in v.items()}
|
|
||||||
elif isinstance(v, list):
|
|
||||||
return [cls.serialize_to_json(item) for item in v]
|
|
||||||
elif isinstance(v, datetime.datetime):
|
|
||||||
return v.isoformat()
|
|
||||||
elif isinstance(v, datetime.timedelta):
|
|
||||||
# d.hh:mm:ss
|
|
||||||
total_seconds = int(v.total_seconds())
|
|
||||||
hours, remainder = divmod(total_seconds, 3600)
|
|
||||||
minutes, seconds = divmod(remainder, 60)
|
|
||||||
return f"{hours:02}:{minutes:02}:{seconds:02}"
|
|
||||||
elif isinstance(v, Enum) and dict_key:
|
|
||||||
return v.value
|
|
||||||
elif isinstance(v, Enum):
|
|
||||||
list_ = list(typ)
|
|
||||||
return list_.index(v)
|
|
||||||
return v
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def serialize_model(cls, v: BaseModel, in_union: bool = False) -> dict[str, Any]:
|
|
||||||
d = {}
|
|
||||||
is_union = issubclass(v.__class__, SignalRUnionMessage)
|
|
||||||
for field, info in v.__class__.model_fields.items():
|
|
||||||
metadata = next((m for m in info.metadata if isinstance(m, SignalRMeta)), None)
|
|
||||||
if metadata and metadata.json_ignore:
|
|
||||||
continue
|
|
||||||
name = (
|
|
||||||
snake_to_camel(
|
|
||||||
field,
|
|
||||||
metadata.use_abbr if metadata else True,
|
|
||||||
)
|
|
||||||
if not is_union
|
|
||||||
else snake_to_pascal(
|
|
||||||
field,
|
|
||||||
metadata.use_abbr if metadata else True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
d[name] = cls.serialize_to_json(getattr(v, field), in_union=is_union)
|
|
||||||
if is_union and not in_union:
|
|
||||||
return {
|
|
||||||
"$dtype": v.__class__.__name__,
|
|
||||||
"$value": d,
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def process_object(v: Any, typ: type[BaseModel], from_union: bool = False) -> dict[str, Any]:
|
|
||||||
d = {}
|
|
||||||
for field, info in typ.model_fields.items():
|
|
||||||
metadata = next((m for m in info.metadata if isinstance(m, SignalRMeta)), None)
|
|
||||||
if metadata and metadata.json_ignore:
|
|
||||||
continue
|
|
||||||
name = (
|
|
||||||
snake_to_camel(field, metadata.use_abbr if metadata else True)
|
|
||||||
if not from_union
|
|
||||||
else snake_to_pascal(field, metadata.use_abbr if metadata else True)
|
|
||||||
)
|
|
||||||
value = v.get(name)
|
|
||||||
anno = typ.model_fields[field].annotation
|
|
||||||
if anno is None:
|
|
||||||
d[field] = value
|
|
||||||
continue
|
|
||||||
d[field] = JSONProtocol.validate_object(value, anno)
|
|
||||||
return d
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def decode(input: bytes) -> list[Packet]:
|
|
||||||
packets_raw = input.removesuffix(SEP).split(SEP)
|
|
||||||
packets = []
|
|
||||||
if len(packets_raw) > 1:
|
|
||||||
for packet_raw in packets_raw:
|
|
||||||
packets.extend(JSONProtocol.decode(packet_raw))
|
|
||||||
return packets
|
|
||||||
else:
|
|
||||||
data = json.loads(packets_raw[0])
|
|
||||||
packet_type = PacketType(data["type"])
|
|
||||||
if packet_type not in PACKETS:
|
|
||||||
raise ValueError(f"Unknown packet type: {packet_type}")
|
|
||||||
match packet_type:
|
|
||||||
case PacketType.INVOCATION:
|
|
||||||
return [
|
|
||||||
InvocationPacket(
|
|
||||||
header=data.get("header"),
|
|
||||||
invocation_id=data.get("invocationId"),
|
|
||||||
target=data["target"],
|
|
||||||
arguments=data.get("arguments"),
|
|
||||||
stream_ids=data.get("streamIds"),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
case PacketType.COMPLETION:
|
|
||||||
return [
|
|
||||||
CompletionPacket(
|
|
||||||
header=data.get("header"),
|
|
||||||
invocation_id=data["invocationId"],
|
|
||||||
error=data.get("error"),
|
|
||||||
result=data.get("result"),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
case PacketType.PING:
|
|
||||||
return [PingPacket()]
|
|
||||||
case PacketType.CLOSE:
|
|
||||||
return [
|
|
||||||
ClosePacket(
|
|
||||||
error=data.get("error"),
|
|
||||||
allow_reconnect=data.get("allowReconnect", False),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
raise ValueError(f"Unsupported packet type: {packet_type}")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate_object(cls, v: Any, typ: type, from_union: bool = False) -> Any:
|
|
||||||
if issubclass(typ, BaseModel):
|
|
||||||
return typ.model_validate(JSONProtocol.process_object(v, typ, from_union))
|
|
||||||
elif inspect.isclass(typ) and issubclass(typ, datetime.datetime):
|
|
||||||
return datetime.datetime.fromisoformat(v)
|
|
||||||
elif inspect.isclass(typ) and issubclass(typ, datetime.timedelta):
|
|
||||||
# d.hh:mm:ss
|
|
||||||
parts = v.split(":")
|
|
||||||
if len(parts) == 3:
|
|
||||||
return datetime.timedelta(hours=int(parts[0]), minutes=int(parts[1]), seconds=int(parts[2]))
|
|
||||||
elif len(parts) == 2:
|
|
||||||
return datetime.timedelta(minutes=int(parts[0]), seconds=int(parts[1]))
|
|
||||||
elif len(parts) == 1:
|
|
||||||
return datetime.timedelta(seconds=int(parts[0]))
|
|
||||||
elif get_origin(typ) is list:
|
|
||||||
return [cls.validate_object(item, get_args(typ)[0]) for item in v]
|
|
||||||
elif inspect.isclass(typ) and issubclass(typ, Enum):
|
|
||||||
list_ = list(typ)
|
|
||||||
return list_[v] if isinstance(v, int) and 0 <= v < len(list_) else typ(v)
|
|
||||||
elif get_origin(typ) is dict:
|
|
||||||
return {
|
|
||||||
cls.validate_object(k, get_args(typ)[0]): cls.validate_object(v, get_args(typ)[1]) for k, v in v.items()
|
|
||||||
}
|
|
||||||
elif (origin := get_origin(typ)) is Union or origin is UnionType:
|
|
||||||
args = get_args(typ)
|
|
||||||
if len(args) == 2 and NoneType in args:
|
|
||||||
non_none_args = [arg for arg in args if arg is not NoneType]
|
|
||||||
if len(non_none_args) == 1:
|
|
||||||
if v is None:
|
|
||||||
return None
|
|
||||||
return cls.validate_object(v, non_none_args[0])
|
|
||||||
|
|
||||||
# suppose use `MessagePack-CSharp Union | None`
|
|
||||||
# except `X (Other Type) | None`
|
|
||||||
if NoneType in args and v is None:
|
|
||||||
return None
|
|
||||||
if not all(issubclass(arg, SignalRUnionMessage) or arg is NoneType for arg in args):
|
|
||||||
raise ValueError(f"Cannot validate {v} to {typ}, only SignalRUnionMessage subclasses are supported")
|
|
||||||
# https://github.com/ppy/osu/blob/98acd9/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs
|
|
||||||
union_type = v["$dtype"]
|
|
||||||
for arg in args:
|
|
||||||
assert issubclass(arg, SignalRUnionMessage)
|
|
||||||
if arg.__name__ == union_type:
|
|
||||||
return cls.validate_object(v["$value"], arg, True)
|
|
||||||
return v
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def encode(packet: Packet) -> bytes:
|
|
||||||
payload: dict[str, Any] = {
|
|
||||||
"type": packet.type.value,
|
|
||||||
}
|
|
||||||
if packet.header:
|
|
||||||
payload["header"] = packet.header
|
|
||||||
if isinstance(packet, InvocationPacket):
|
|
||||||
payload.update(
|
|
||||||
{
|
|
||||||
"target": packet.target,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if packet.invocation_id is not None:
|
|
||||||
payload["invocationId"] = packet.invocation_id
|
|
||||||
if packet.arguments is not None:
|
|
||||||
payload["arguments"] = [JSONProtocol.serialize_to_json(arg) for arg in packet.arguments]
|
|
||||||
if packet.stream_ids is not None:
|
|
||||||
payload["streamIds"] = packet.stream_ids
|
|
||||||
elif isinstance(packet, CompletionPacket):
|
|
||||||
payload.update(
|
|
||||||
{
|
|
||||||
"invocationId": packet.invocation_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if packet.error is not None:
|
|
||||||
payload["error"] = packet.error
|
|
||||||
if packet.result is not None:
|
|
||||||
payload["result"] = JSONProtocol.serialize_to_json(packet.result)
|
|
||||||
elif isinstance(packet, PingPacket):
|
|
||||||
pass
|
|
||||||
elif isinstance(packet, ClosePacket):
|
|
||||||
payload.update(
|
|
||||||
{
|
|
||||||
"allowReconnect": packet.allow_reconnect,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if packet.error is not None:
|
|
||||||
payload["error"] = packet.error
|
|
||||||
return json.dumps(payload).encode("utf-8") + SEP
|
|
||||||
|
|
||||||
|
|
||||||
PROTOCOLS: dict[str, Protocol] = {
|
|
||||||
"json": JSONProtocol,
|
|
||||||
"messagepack": MsgpackProtocol,
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from typing import Literal
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from app.database import User as DBUser
|
|
||||||
from app.dependencies.database import DBFactory, get_db_factory
|
|
||||||
from app.dependencies.user import get_current_user, get_current_user_and_token
|
|
||||||
from app.log import logger
|
|
||||||
from app.models.signalr import NegotiateResponse, Transport
|
|
||||||
|
|
||||||
from .hub import Hubs
|
|
||||||
from .packet import PROTOCOLS, SEP
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, WebSocket
|
|
||||||
from fastapi.security import SecurityScopes
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/signalr", include_in_schema=False)
|
|
||||||
logger.warning(
|
|
||||||
"The Python version of SignalR server is deprecated. "
|
|
||||||
"Maybe it will be removed or be fixed to continuously use in the future"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{hub}/negotiate", response_model=NegotiateResponse)
|
|
||||||
async def negotiate(
|
|
||||||
hub: Literal["spectator", "multiplayer", "metadata"],
|
|
||||||
negotiate_version: int = Query(1, alias="negotiateVersion"),
|
|
||||||
user: DBUser = Depends(get_current_user),
|
|
||||||
):
|
|
||||||
connectionId = str(user.id)
|
|
||||||
connectionToken = f"{connectionId}:{uuid.uuid4()}"
|
|
||||||
Hubs[hub].add_waited_client(
|
|
||||||
connection_token=connectionToken,
|
|
||||||
timestamp=int(time.time()),
|
|
||||||
)
|
|
||||||
return NegotiateResponse(
|
|
||||||
connectionId=connectionId,
|
|
||||||
connectionToken=connectionToken,
|
|
||||||
negotiateVersion=negotiate_version,
|
|
||||||
availableTransports=[Transport(transport="WebSockets")],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.websocket("/{hub}")
|
|
||||||
async def connect(
|
|
||||||
hub: Literal["spectator", "multiplayer", "metadata"],
|
|
||||||
websocket: WebSocket,
|
|
||||||
id: str,
|
|
||||||
authorization: str = Header(...),
|
|
||||||
factory: DBFactory = Depends(get_db_factory),
|
|
||||||
):
|
|
||||||
token = authorization[7:]
|
|
||||||
user_id = id.split(":")[0]
|
|
||||||
hub_ = Hubs[hub]
|
|
||||||
if id not in hub_:
|
|
||||||
await websocket.close(code=1008)
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
async for session in factory():
|
|
||||||
if (
|
|
||||||
user_and_token := await get_current_user_and_token(
|
|
||||||
session, SecurityScopes(scopes=["*"]), token_pw=token
|
|
||||||
)
|
|
||||||
) is None or str(user_and_token[0].id) != user_id:
|
|
||||||
await websocket.close(code=1008)
|
|
||||||
return
|
|
||||||
except HTTPException:
|
|
||||||
await websocket.close(code=1008)
|
|
||||||
return
|
|
||||||
await websocket.accept()
|
|
||||||
|
|
||||||
# handshake
|
|
||||||
handshake = await websocket.receive()
|
|
||||||
message = handshake.get("bytes") or handshake.get("text")
|
|
||||||
if not message:
|
|
||||||
await websocket.close(code=1008)
|
|
||||||
return
|
|
||||||
handshake_payload = json.loads(message[:-1])
|
|
||||||
error = ""
|
|
||||||
protocol = handshake_payload.get("protocol", "json")
|
|
||||||
|
|
||||||
client = None
|
|
||||||
try:
|
|
||||||
client = await hub_.add_client(
|
|
||||||
connection_id=user_id,
|
|
||||||
connection_token=id,
|
|
||||||
connection=websocket,
|
|
||||||
protocol=PROTOCOLS[protocol],
|
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
error = f"Protocol '{protocol}' is not supported."
|
|
||||||
except TimeoutError:
|
|
||||||
error = f"Connection {id} has waited too long."
|
|
||||||
except ValueError as e:
|
|
||||||
error = str(e)
|
|
||||||
payload = {"error": error} if error else {}
|
|
||||||
# finish handshake
|
|
||||||
await websocket.send_bytes(json.dumps(payload).encode() + SEP)
|
|
||||||
if error or not client:
|
|
||||||
await websocket.close(code=1008)
|
|
||||||
return
|
|
||||||
|
|
||||||
connected_clients = hub_.get_before_clients(user_id, id)
|
|
||||||
for connected_client in connected_clients:
|
|
||||||
await hub_.kick_client(connected_client)
|
|
||||||
|
|
||||||
await hub_.clean_state(client, False)
|
|
||||||
task = asyncio.create_task(hub_.on_connect(client))
|
|
||||||
hub_.tasks.add(task)
|
|
||||||
task.add_done_callback(hub_.tasks.discard)
|
|
||||||
await hub_._listen_client(client)
|
|
||||||
try:
|
|
||||||
await websocket.close()
|
|
||||||
except Exception:
|
|
||||||
...
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class ResultStore:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._seq: int = 1
|
|
||||||
self._futures: dict[str, asyncio.Future] = {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_invocation_id(self) -> int:
|
|
||||||
return self._seq
|
|
||||||
|
|
||||||
def get_invocation_id(self) -> str:
|
|
||||||
s = self._seq
|
|
||||||
self._seq = (self._seq + 1) % sys.maxsize
|
|
||||||
return str(s)
|
|
||||||
|
|
||||||
def add_result(self, invocation_id: str, result: Any, error: str | None = None) -> None:
|
|
||||||
if isinstance(invocation_id, str) and invocation_id.isdecimal():
|
|
||||||
if future := self._futures.get(invocation_id):
|
|
||||||
future.set_result((result, error))
|
|
||||||
|
|
||||||
async def fetch(
|
|
||||||
self,
|
|
||||||
invocation_id: str,
|
|
||||||
timeout: float | None, # noqa: ASYNC109
|
|
||||||
) -> tuple[Any, str | None]:
|
|
||||||
future = asyncio.get_event_loop().create_future()
|
|
||||||
self._futures[invocation_id] = future
|
|
||||||
try:
|
|
||||||
return await asyncio.wait_for(future, timeout)
|
|
||||||
finally:
|
|
||||||
del self._futures[invocation_id]
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
import inspect
|
|
||||||
import sys
|
|
||||||
from typing import Any, ForwardRef, cast
|
|
||||||
|
|
||||||
# https://github.com/pydantic/pydantic/blob/main/pydantic/v1/typing.py#L61-L75
|
|
||||||
if sys.version_info < (3, 12, 4):
|
|
||||||
|
|
||||||
def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
|
|
||||||
return cast(Any, type_)._evaluate(globalns, localns, recursive_guard=set())
|
|
||||||
else:
|
|
||||||
|
|
||||||
def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
|
|
||||||
return cast(Any, type_)._evaluate(globalns, localns, type_params=(), recursive_guard=set())
|
|
||||||
|
|
||||||
|
|
||||||
def get_annotation(param: inspect.Parameter, globalns: dict[str, Any]) -> Any:
|
|
||||||
annotation = param.annotation
|
|
||||||
if isinstance(annotation, str):
|
|
||||||
annotation = ForwardRef(annotation)
|
|
||||||
try:
|
|
||||||
annotation = evaluate_forwardref(annotation, globalns, globalns)
|
|
||||||
except Exception:
|
|
||||||
return inspect.Parameter.empty
|
|
||||||
return annotation
|
|
||||||
|
|
||||||
|
|
||||||
def get_signature(call: Callable[..., Any]) -> inspect.Signature:
|
|
||||||
signature = inspect.signature(call)
|
|
||||||
globalns = getattr(call, "__globals__", {})
|
|
||||||
typed_params = [
|
|
||||||
inspect.Parameter(
|
|
||||||
name=param.name,
|
|
||||||
kind=param.kind,
|
|
||||||
default=param.default,
|
|
||||||
annotation=get_annotation(param, globalns),
|
|
||||||
)
|
|
||||||
for param in signature.parameters.values()
|
|
||||||
]
|
|
||||||
return inspect.Signature(typed_params)
|
|
||||||
@@ -14,7 +14,6 @@ from app.database.user import User
|
|||||||
from app.dependencies.database import get_redis, with_db
|
from app.dependencies.database import get_redis, with_db
|
||||||
from app.dependencies.scheduler import get_scheduler
|
from app.dependencies.scheduler import get_scheduler
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.models.metadata_hub import DailyChallengeInfo
|
|
||||||
from app.models.mods import APIMod, get_available_mods
|
from app.models.mods import APIMod, get_available_mods
|
||||||
from app.models.room import RoomCategory
|
from app.models.room import RoomCategory
|
||||||
from app.service.room import create_playlist_room
|
from app.service.room import create_playlist_room
|
||||||
@@ -54,8 +53,6 @@ async def create_daily_challenge_room(
|
|||||||
|
|
||||||
@get_scheduler().scheduled_job("cron", hour=0, minute=0, second=0, id="daily_challenge")
|
@get_scheduler().scheduled_job("cron", hour=0, minute=0, second=0, id="daily_challenge")
|
||||||
async def daily_challenge_job():
|
async def daily_challenge_job():
|
||||||
from app.signalr.hub import MetadataHubs
|
|
||||||
|
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
redis = get_redis()
|
redis = get_redis()
|
||||||
key = f"daily_challenge:{now.date()}"
|
key = f"daily_challenge:{now.date()}"
|
||||||
@@ -108,7 +105,6 @@ async def daily_challenge_job():
|
|||||||
allowed_mods=allowed_mods_list,
|
allowed_mods=allowed_mods_list,
|
||||||
duration=int((next_day - now - timedelta(minutes=2)).total_seconds() / 60),
|
duration=int((next_day - now - timedelta(minutes=2)).total_seconds() / 60),
|
||||||
)
|
)
|
||||||
await MetadataHubs.broadcast_call("DailyChallengeUpdated", DailyChallengeInfo(room_id=room.id))
|
|
||||||
logger.success(f"Added today's daily challenge: {beatmap=}, {ruleset_id=}, {required_mods=}")
|
logger.success(f"Added today's daily challenge: {beatmap=}, {ruleset_id=}, {required_mods=}")
|
||||||
return
|
return
|
||||||
except (ValueError, json.JSONDecodeError) as e:
|
except (ValueError, json.JSONDecodeError) as e:
|
||||||
|
|||||||
3
main.py
3
main.py
@@ -166,9 +166,6 @@ app.include_router(auth_router)
|
|||||||
app.include_router(private_router)
|
app.include_router(private_router)
|
||||||
app.include_router(lio_router)
|
app.include_router(lio_router)
|
||||||
|
|
||||||
# from app.signalr import signalr_router
|
|
||||||
# app.include_router(signalr_router)
|
|
||||||
|
|
||||||
# 会话验证中间件
|
# 会话验证中间件
|
||||||
if settings.enable_session_verification:
|
if settings.enable_session_verification:
|
||||||
app.add_middleware(VerifySessionMiddleware)
|
app.add_middleware(VerifySessionMiddleware)
|
||||||
|
|||||||
@@ -4,10 +4,7 @@
|
|||||||
"path": "."
|
"path": "."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "packages/msgpack_lazer_api"
|
"path": "spectator-server"
|
||||||
},
|
}
|
||||||
{
|
|
||||||
"path": "spectator-server"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
424
packages/msgpack_lazer_api/Cargo.lock
generated
424
packages/msgpack_lazer_api/Cargo.lock
generated
@@ -1,424 +0,0 @@
|
|||||||
# 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",
|
|
||||||
]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[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"
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
def encode(obj: Any) -> bytes: ...
|
|
||||||
def decode(data: bytes) -> Any: ...
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[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"]
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
use chrono::{TimeZone, Utc};
|
|
||||||
use pyo3::types::PyDict;
|
|
||||||
use pyo3::{prelude::*, IntoPyObjectExt};
|
|
||||||
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::True => Ok(true.into_py_any(py)?),
|
|
||||||
rmp::Marker::False => Ok(false.into_py_any(py)?),
|
|
||||||
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::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)?;
|
|
||||||
|
|
||||||
let api_mod_dict = PyDict::new(py);
|
|
||||||
api_mod_dict.set_item("acronym", obj1)?;
|
|
||||||
api_mod_dict.set_item("settings", obj2)?;
|
|
||||||
|
|
||||||
return Ok(api_mod_dict.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())
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
use chrono::{DateTime, Utc};
|
|
||||||
use pyo3::prelude::{PyAnyMethods, PyDictMethods, PyListMethods, PyResult, PyStringMethods};
|
|
||||||
use pyo3::types::{PyBool, PyBytes, PyDateTime, PyDict, PyFloat, PyInt, PyList, PyNone, PyString};
|
|
||||||
use pyo3::{Bound, PyAny};
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_api_mod(dict: &Bound<'_, PyDict>) -> bool {
|
|
||||||
if let Ok(Some(acronym)) = dict.get_item("acronym") {
|
|
||||||
if let Ok(acronym_str) = acronym.extract::<String>() {
|
|
||||||
return acronym_str.len() == 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/ppy/osu/blob/3dced3/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
|
|
||||||
fn write_api_mod(buf: &mut Vec<u8>, api_mod: &Bound<'_, PyDict>) -> PyResult<()> {
|
|
||||||
let acronym = api_mod
|
|
||||||
.get_item("acronym")?
|
|
||||||
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("APIMod missing 'acronym' field"))?;
|
|
||||||
let acronym_str = acronym.extract::<String>()?;
|
|
||||||
|
|
||||||
let settings = api_mod
|
|
||||||
.get_item("settings")?
|
|
||||||
.unwrap_or_else(|| PyDict::new(acronym.py()).into_any());
|
|
||||||
let settings_dict = settings.downcast::<PyDict>()?;
|
|
||||||
|
|
||||||
rmp::encode::write_array_len(buf, 2).unwrap();
|
|
||||||
rmp::encode::write_str(buf, &acronym_str).unwrap();
|
|
||||||
rmp::encode::write_array_len(buf, settings_dict.len() as u32).unwrap();
|
|
||||||
|
|
||||||
for (k, v) in settings_dict.iter() {
|
|
||||||
let key_str = k.extract::<String>()?;
|
|
||||||
rmp::encode::write_str(buf, &key_str).unwrap();
|
|
||||||
write_object(buf, &v);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
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(boolean) = obj.downcast::<PyBool>() {
|
|
||||||
write_bool(buf, boolean);
|
|
||||||
} else if let Ok(float) = obj.downcast::<PyFloat>() {
|
|
||||||
write_float(buf, float);
|
|
||||||
} else if let Ok(integer) = obj.downcast::<PyInt>() {
|
|
||||||
write_integer(buf, integer);
|
|
||||||
} else if let Ok(bytes) = obj.downcast::<PyBytes>() {
|
|
||||||
write_bin(buf, bytes);
|
|
||||||
} else if let Ok(dict) = obj.downcast::<PyDict>() {
|
|
||||||
if is_api_mod(dict) {
|
|
||||||
write_api_mod(buf, dict).unwrap_or_else(|_| write_hashmap(buf, dict));
|
|
||||||
} else {
|
|
||||||
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 {
|
|
||||||
panic!("Unsupported type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
mod decode;
|
|
||||||
mod encode;
|
|
||||||
|
|
||||||
use pyo3::prelude::*;
|
|
||||||
|
|
||||||
#[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)?)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "g0v0-server"
|
name = "g0v0-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "3rd-party osu!lazer server which supports the latest osu!lazer."
|
description = "3rd-party osu!lazer private server which supports the latest osu!lazer."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -18,7 +18,6 @@ dependencies = [
|
|||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
"loguru>=0.7.3",
|
"loguru>=0.7.3",
|
||||||
"maxminddb>=2.8.2",
|
"maxminddb>=2.8.2",
|
||||||
"msgpack-lazer-api",
|
|
||||||
"newrelic>=10.1.0",
|
"newrelic>=10.1.0",
|
||||||
"osupyparser>=1.0.7",
|
"osupyparser>=1.0.7",
|
||||||
"passlib[bcrypt]>=1.7.4",
|
"passlib[bcrypt]>=1.7.4",
|
||||||
@@ -103,20 +102,14 @@ exclude = ["migrations/", ".venv/", "venv/"]
|
|||||||
|
|
||||||
[tool.uv.workspace]
|
[tool.uv.workspace]
|
||||||
members = [
|
members = [
|
||||||
"packages/msgpack_lazer_api",
|
|
||||||
"packages/osupyparser",
|
"packages/osupyparser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
msgpack-lazer-api = { workspace = true }
|
|
||||||
osupyparser = { git = "https://github.com/MingxuanGame/osupyparser.git" }
|
osupyparser = { git = "https://github.com/MingxuanGame/osupyparser.git" }
|
||||||
|
|
||||||
[tool.uv]
|
|
||||||
cache-keys = [{file = "pyproject.toml"}, {file = "packages/msgpack_lazer_api/Cargo.toml"}, {file = "**/*.rs"}]
|
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"maturin>=1.9.2",
|
|
||||||
"pre-commit>=4.2.0",
|
"pre-commit>=4.2.0",
|
||||||
"pyright>=1.1.404",
|
"pyright>=1.1.404",
|
||||||
"ruff>=0.12.4",
|
"ruff>=0.12.4",
|
||||||
|
|||||||
493
uv.lock
generated
493
uv.lock
generated
@@ -2,12 +2,6 @@ version = 1
|
|||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
[manifest]
|
|
||||||
members = [
|
|
||||||
"g0v0-server",
|
|
||||||
"msgpack-lazer-api",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aioboto3"
|
name = "aioboto3"
|
||||||
version = "15.1.0"
|
version = "15.1.0"
|
||||||
@@ -172,16 +166,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyio"
|
name = "anyio"
|
||||||
version = "4.10.0"
|
version = "4.11.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "idna" },
|
{ name = "idna" },
|
||||||
{ name = "sniffio" },
|
{ name = "sniffio" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
|
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -207,52 +201,68 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bcrypt"
|
name = "bcrypt"
|
||||||
version = "4.3.0"
|
version = "5.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" },
|
{ url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" },
|
{ url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" },
|
{ url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" },
|
{ url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" },
|
{ url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" },
|
{ url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" },
|
{ url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" },
|
{ url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" },
|
{ url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" },
|
{ url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" },
|
{ url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" },
|
{ url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" },
|
{ url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" },
|
{ url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" },
|
{ url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" },
|
{ url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" },
|
{ url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" },
|
{ url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" },
|
{ url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" },
|
{ url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" },
|
{ url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" },
|
{ url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" },
|
{ url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" },
|
{ url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" },
|
{ url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" },
|
{ url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" },
|
{ url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" },
|
{ url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" },
|
{ url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" },
|
{ url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" },
|
{ url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -297,14 +307,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "botocore-stubs"
|
name = "botocore-stubs"
|
||||||
version = "1.40.30"
|
version = "1.40.33"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "types-awscrt" },
|
{ name = "types-awscrt" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/d8/a8cf273b0342ee4e8c8ed30cf2b32b00616e1090c4df12beba8bb65334a1/botocore_stubs-1.40.30.tar.gz", hash = "sha256:73baabaef96fa74af4034c22e37fd71a752075867dd31e06e5a3809ffbc151ec", size = 42768, upload-time = "2025-09-12T20:24:45.257Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ae/94/16f8e1f41feaa38f1350aa5a4c60c5724b6c8524ca0e6c28523bf5070e74/botocore_stubs-1.40.33.tar.gz", hash = "sha256:89c51ae0b28d9d79fde8c497cf908ddf872ce027d2737d4d4ba473fde9cdaa82", size = 42742, upload-time = "2025-09-17T20:25:56.388Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/42/7d20894547feccb3f0ce16150d98795a2aae3b469f8dd582d99078478b39/botocore_stubs-1.40.30-py3-none-any.whl", hash = "sha256:7d511dcd45f327446ebb130b71d9d124b026f572e12da956df58fdc56ab426ac", size = 66843, upload-time = "2025-09-12T20:24:42.841Z" },
|
{ url = "https://files.pythonhosted.org/packages/af/7b/6d8fe12a955b16094460e89ea7c4e063f131f4b3bd461b96bcd625d0c79e/botocore_stubs-1.40.33-py3-none-any.whl", hash = "sha256:ad21fee32cbdc7ad4730f29baf88424c7086bf88a745f8e43660ca3e9a7e5f89", size = 66843, upload-time = "2025-09-17T20:25:54.052Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -384,14 +394,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.2.1"
|
version = "8.3.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -405,37 +415,58 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptography"
|
name = "cryptography"
|
||||||
version = "45.0.7"
|
version = "46.0.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/4a/9b/e301418629f7bfdf72db9e80ad6ed9d1b83c487c471803eaa6464c511a01/cryptography-46.0.2.tar.gz", hash = "sha256:21b6fc8c71a3f9a604f028a329e5560009cc4a3a828bfea5fcba8eb7647d88fe", size = 749293, upload-time = "2025-10-01T00:29:11.856Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105, upload-time = "2025-09-01T11:13:59.684Z" },
|
{ url = "https://files.pythonhosted.org/packages/e0/98/7a8df8c19a335c8028414738490fc3955c0cecbfdd37fcc1b9c3d04bd561/cryptography-46.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3e32ab7dd1b1ef67b9232c4cf5e2ee4cd517d4316ea910acaaa9c5712a1c663", size = 7261255, upload-time = "2025-10-01T00:27:22.947Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799, upload-time = "2025-09-01T11:14:02.517Z" },
|
{ url = "https://files.pythonhosted.org/packages/c6/38/b2adb2aa1baa6706adc3eb746691edd6f90a656a9a65c3509e274d15a2b8/cryptography-46.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd1a69086926b623ef8126b4c33d5399ce9e2f3fac07c9c734c2a4ec38b6d02", size = 4297596, upload-time = "2025-10-01T00:27:25.258Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504, upload-time = "2025-09-01T11:14:04.522Z" },
|
{ url = "https://files.pythonhosted.org/packages/e4/27/0f190ada240003119488ae66c897b5e97149292988f556aef4a6a2a57595/cryptography-46.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb7fb9cd44c2582aa5990cf61a4183e6f54eea3172e54963787ba47287edd135", size = 4450899, upload-time = "2025-10-01T00:27:27.458Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542, upload-time = "2025-09-01T11:14:06.309Z" },
|
{ url = "https://files.pythonhosted.org/packages/85/d5/e4744105ab02fdf6bb58ba9a816e23b7a633255987310b4187d6745533db/cryptography-46.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9066cfd7f146f291869a9898b01df1c9b0e314bfa182cef432043f13fc462c92", size = 4300382, upload-time = "2025-10-01T00:27:29.091Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244, upload-time = "2025-09-01T11:14:08.152Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/fb/bf9571065c18c04818cb07de90c43fc042c7977c68e5de6876049559c72f/cryptography-46.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:97e83bf4f2f2c084d8dd792d13841d0a9b241643151686010866bbd076b19659", size = 4017347, upload-time = "2025-10-01T00:27:30.767Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975, upload-time = "2025-09-01T11:14:09.755Z" },
|
{ url = "https://files.pythonhosted.org/packages/35/72/fc51856b9b16155ca071080e1a3ad0c3a8e86616daf7eb018d9565b99baa/cryptography-46.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:4a766d2a5d8127364fd936572c6e6757682fc5dfcbdba1632d4554943199f2fa", size = 4983500, upload-time = "2025-10-01T00:27:32.741Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082, upload-time = "2025-09-01T11:14:11.229Z" },
|
{ url = "https://files.pythonhosted.org/packages/c1/53/0f51e926799025e31746d454ab2e36f8c3f0d41592bc65cb9840368d3275/cryptography-46.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fab8f805e9675e61ed8538f192aad70500fa6afb33a8803932999b1049363a08", size = 4482591, upload-time = "2025-10-01T00:27:34.869Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397, upload-time = "2025-09-01T11:14:12.924Z" },
|
{ url = "https://files.pythonhosted.org/packages/86/96/4302af40b23ab8aa360862251fb8fc450b2a06ff24bc5e261c2007f27014/cryptography-46.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1e3b6428a3d56043bff0bb85b41c535734204e599c1c0977e1d0f261b02f3ad5", size = 4300019, upload-time = "2025-10-01T00:27:37.029Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244, upload-time = "2025-09-01T11:14:14.431Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/59/0be12c7fcc4c5e34fe2b665a75bc20958473047a30d095a7657c218fa9e8/cryptography-46.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:1a88634851d9b8de8bb53726f4300ab191d3b2f42595e2581a54b26aba71b7cc", size = 4950006, upload-time = "2025-10-01T00:27:40.272Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862, upload-time = "2025-09-01T11:14:16.185Z" },
|
{ url = "https://files.pythonhosted.org/packages/55/1d/42fda47b0111834b49e31590ae14fd020594d5e4dadd639bce89ad790fba/cryptography-46.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:be939b99d4e091eec9a2bcf41aaf8f351f312cd19ff74b5c83480f08a8a43e0b", size = 4482088, upload-time = "2025-10-01T00:27:42.668Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578, upload-time = "2025-09-01T11:14:17.638Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/50/60f583f69aa1602c2bdc7022dae86a0d2b837276182f8c1ec825feb9b874/cryptography-46.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f13b040649bc18e7eb37936009b24fd31ca095a5c647be8bb6aaf1761142bd1", size = 4425599, upload-time = "2025-10-01T00:27:44.616Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400, upload-time = "2025-09-01T11:14:18.958Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/57/d8d4134cd27e6e94cf44adb3f3489f935bde85f3a5508e1b5b43095b917d/cryptography-46.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bdc25e4e01b261a8fda4e98618f1c9515febcecebc9566ddf4a70c63967043b", size = 4697458, upload-time = "2025-10-01T00:27:46.209Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824, upload-time = "2025-09-01T11:14:20.954Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/2b/531e37408573e1da33adfb4c58875013ee8ac7d548d1548967d94a0ae5c4/cryptography-46.0.2-cp311-abi3-win32.whl", hash = "sha256:8b9bf67b11ef9e28f4d78ff88b04ed0929fcd0e4f70bb0f704cfc32a5c6311ee", size = 3056077, upload-time = "2025-10-01T00:27:48.424Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233, upload-time = "2025-09-01T11:14:22.454Z" },
|
{ url = "https://files.pythonhosted.org/packages/a8/cd/2f83cafd47ed2dc5a3a9c783ff5d764e9e70d3a160e0df9a9dcd639414ce/cryptography-46.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:758cfc7f4c38c5c5274b55a57ef1910107436f4ae842478c4989abbd24bd5acb", size = 3512585, upload-time = "2025-10-01T00:27:50.521Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075, upload-time = "2025-09-01T11:14:24.287Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/36/676f94e10bfaa5c5b86c469ff46d3e0663c5dc89542f7afbadac241a3ee4/cryptography-46.0.2-cp311-abi3-win_arm64.whl", hash = "sha256:218abd64a2e72f8472c2102febb596793347a3e65fafbb4ad50519969da44470", size = 2927474, upload-time = "2025-10-01T00:27:52.91Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517, upload-time = "2025-09-01T11:14:25.679Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/cc/47fc6223a341f26d103cb6da2216805e08a37d3b52bee7f3b2aee8066f95/cryptography-46.0.2-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:bda55e8dbe8533937956c996beaa20266a8eca3570402e52ae52ed60de1faca8", size = 7198626, upload-time = "2025-10-01T00:27:54.8Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893, upload-time = "2025-09-01T11:14:27.1Z" },
|
{ url = "https://files.pythonhosted.org/packages/93/22/d66a8591207c28bbe4ac7afa25c4656dc19dc0db29a219f9809205639ede/cryptography-46.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7155c0b004e936d381b15425273aee1cebc94f879c0ce82b0d7fecbf755d53a", size = 4287584, upload-time = "2025-10-01T00:27:57.018Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132, upload-time = "2025-09-01T11:14:28.58Z" },
|
{ url = "https://files.pythonhosted.org/packages/8c/3e/fac3ab6302b928e0398c269eddab5978e6c1c50b2b77bb5365ffa8633b37/cryptography-46.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a61c154cc5488272a6c4b86e8d5beff4639cdb173d75325ce464d723cda0052b", size = 4433796, upload-time = "2025-10-01T00:27:58.631Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086, upload-time = "2025-09-01T11:14:30.572Z" },
|
{ url = "https://files.pythonhosted.org/packages/7d/d8/24392e5d3c58e2d83f98fe5a2322ae343360ec5b5b93fe18bc52e47298f5/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9ec3f2e2173f36a9679d3b06d3d01121ab9b57c979de1e6a244b98d51fea1b20", size = 4292126, upload-time = "2025-10-01T00:28:00.643Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383, upload-time = "2025-09-01T11:14:32.046Z" },
|
{ url = "https://files.pythonhosted.org/packages/ed/38/3d9f9359b84c16c49a5a336ee8be8d322072a09fac17e737f3bb11f1ce64/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2fafb6aa24e702bbf74de4cb23bfa2c3beb7ab7683a299062b69724c92e0fa73", size = 3993056, upload-time = "2025-10-01T00:28:02.8Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186, upload-time = "2025-09-01T11:14:33.95Z" },
|
{ url = "https://files.pythonhosted.org/packages/d6/a3/4c44fce0d49a4703cc94bfbe705adebf7ab36efe978053742957bc7ec324/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0c7ffe8c9b1fcbb07a26d7c9fa5e857c2fe80d72d7b9e0353dcf1d2180ae60ee", size = 4967604, upload-time = "2025-10-01T00:28:04.783Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639, upload-time = "2025-09-01T11:14:35.343Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/c2/49d73218747c8cac16bb8318a5513fde3129e06a018af3bc4dc722aa4a98/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5840f05518caa86b09d23f8b9405a7b6d5400085aa14a72a98fdf5cf1568c0d2", size = 4465367, upload-time = "2025-10-01T00:28:06.864Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552, upload-time = "2025-09-01T11:14:36.929Z" },
|
{ url = "https://files.pythonhosted.org/packages/1b/64/9afa7d2ee742f55ca6285a54386ed2778556a4ed8871571cb1c1bfd8db9e/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:27c53b4f6a682a1b645fbf1cd5058c72cf2f5aeba7d74314c36838c7cbc06e0f", size = 4291678, upload-time = "2025-10-01T00:28:08.982Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742, upload-time = "2025-09-01T11:14:38.368Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/48/1696d5ea9623a7b72ace87608f6899ca3c331709ac7ebf80740abb8ac673/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:512c0250065e0a6b286b2db4bbcc2e67d810acd53eb81733e71314340366279e", size = 4931366, upload-time = "2025-10-01T00:28:10.74Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/3c/9dfc778401a334db3b24435ee0733dd005aefb74afe036e2d154547cb917/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:07c0eb6657c0e9cca5891f4e35081dbf985c8131825e21d99b4f440a8f496f36", size = 4464738, upload-time = "2025-10-01T00:28:12.491Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/b1/abcde62072b8f3fd414e191a6238ce55a0050e9738090dc6cded24c12036/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48b983089378f50cba258f7f7aa28198c3f6e13e607eaf10472c26320332ca9a", size = 4419305, upload-time = "2025-10-01T00:28:14.145Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/1f/3d2228492f9391395ca34c677e8f2571fb5370fe13dc48c1014f8c509864/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e6f6775eaaa08c0eec73e301f7592f4367ccde5e4e4df8e58320f2ebf161ea2c", size = 4681201, upload-time = "2025-10-01T00:28:15.951Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/77/b687745804a93a55054f391528fcfc76c3d6bfd082ce9fb62c12f0d29fc1/cryptography-46.0.2-cp314-cp314t-win32.whl", hash = "sha256:e8633996579961f9b5a3008683344c2558d38420029d3c0bc7ff77c17949a4e1", size = 3022492, upload-time = "2025-10-01T00:28:17.643Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/a5/8d498ef2996e583de0bef1dcc5e70186376f00883ae27bf2133f490adf21/cryptography-46.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:48c01988ecbb32979bb98731f5c2b2f79042a6c58cc9a319c8c2f9987c7f68f9", size = 3496215, upload-time = "2025-10-01T00:28:19.272Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/db/ee67aaef459a2706bc302b15889a1a8126ebe66877bab1487ae6ad00f33d/cryptography-46.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8e2ad4d1a5899b7caa3a450e33ee2734be7cc0689010964703a7c4bcc8dd4fd0", size = 2919255, upload-time = "2025-10-01T00:28:21.115Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/bb/fa95abcf147a1b0bb94d95f53fbb09da77b24c776c5d87d36f3d94521d2c/cryptography-46.0.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a08e7401a94c002e79dc3bc5231b6558cd4b2280ee525c4673f650a37e2c7685", size = 7248090, upload-time = "2025-10-01T00:28:22.846Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/66/f42071ce0e3ffbfa80a88feadb209c779fda92a23fbc1e14f74ebf72ef6b/cryptography-46.0.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d30bc11d35743bf4ddf76674a0a369ec8a21f87aaa09b0661b04c5f6c46e8d7b", size = 4293123, upload-time = "2025-10-01T00:28:25.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/5d/1fdbd2e5c1ba822828d250e5a966622ef00185e476d1cd2726b6dd135e53/cryptography-46.0.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bca3f0ce67e5a2a2cf524e86f44697c4323a86e0fd7ba857de1c30d52c11ede1", size = 4439524, upload-time = "2025-10-01T00:28:26.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/c1/5e4989a7d102d4306053770d60f978c7b6b1ea2ff8c06e0265e305b23516/cryptography-46.0.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff798ad7a957a5021dcbab78dfff681f0cf15744d0e6af62bd6746984d9c9e9c", size = 4297264, upload-time = "2025-10-01T00:28:29.327Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/78/b56f847d220cb1d6d6aef5a390e116ad603ce13a0945a3386a33abc80385/cryptography-46.0.2-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cb5e8daac840e8879407acbe689a174f5ebaf344a062f8918e526824eb5d97af", size = 4011872, upload-time = "2025-10-01T00:28:31.479Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/80/2971f214b066b888944f7b57761bf709ee3f2cf805619a18b18cab9b263c/cryptography-46.0.2-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:3f37aa12b2d91e157827d90ce78f6180f0c02319468a0aea86ab5a9566da644b", size = 4978458, upload-time = "2025-10-01T00:28:33.267Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/84/0cb0a2beaa4f1cbe63ebec4e97cd7e0e9f835d0ba5ee143ed2523a1e0016/cryptography-46.0.2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e38f203160a48b93010b07493c15f2babb4e0f2319bbd001885adb3f3696d21", size = 4472195, upload-time = "2025-10-01T00:28:36.039Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/8b/2b542ddbf78835c7cd67b6fa79e95560023481213a060b92352a61a10efe/cryptography-46.0.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d19f5f48883752b5ab34cff9e2f7e4a7f216296f33714e77d1beb03d108632b6", size = 4296791, upload-time = "2025-10-01T00:28:37.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/12/9065b40201b4f4876e93b9b94d91feb18de9150d60bd842a16a21565007f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:04911b149eae142ccd8c9a68892a70c21613864afb47aba92d8c7ed9cc001023", size = 4939629, upload-time = "2025-10-01T00:28:39.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/9e/6507dc048c1b1530d372c483dfd34e7709fc542765015425f0442b08547f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8b16c1ede6a937c291d41176934268e4ccac2c6521c69d3f5961c5a1e11e039e", size = 4471988, upload-time = "2025-10-01T00:28:41.822Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/86/d025584a5f7d5c5ec8d3633dbcdce83a0cd579f1141ceada7817a4c26934/cryptography-46.0.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:747b6f4a4a23d5a215aadd1d0b12233b4119c4313df83ab4137631d43672cc90", size = 4422989, upload-time = "2025-10-01T00:28:43.608Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/39/536370418b38a15a61bbe413006b79dfc3d2b4b0eafceb5581983f973c15/cryptography-46.0.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b275e398ab3a7905e168c036aad54b5969d63d3d9099a0a66cc147a3cc983be", size = 4685578, upload-time = "2025-10-01T00:28:45.361Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/52/ea7e2b1910f547baed566c866fbb86de2402e501a89ecb4871ea7f169a81/cryptography-46.0.2-cp38-abi3-win32.whl", hash = "sha256:0b507c8e033307e37af61cb9f7159b416173bdf5b41d11c4df2e499a1d8e007c", size = 3036711, upload-time = "2025-10-01T00:28:47.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/9e/171f40f9c70a873e73c2efcdbe91e1d4b1777a03398fa1c4af3c56a2477a/cryptography-46.0.2-cp38-abi3-win_amd64.whl", hash = "sha256:f9b2dc7668418fb6f221e4bf701f716e05e8eadb4f1988a2487b11aedf8abe62", size = 3500007, upload-time = "2025-10-01T00:28:48.967Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/7c/15ad426257615f9be8caf7f97990cf3dcbb5b8dd7ed7e0db581a1c4759dd/cryptography-46.0.2-cp38-abi3-win_arm64.whl", hash = "sha256:91447f2b17e83c9e0c89f133119d83f94ce6e0fb55dd47da0a959316e6e9cfa1", size = 2918153, upload-time = "2025-10-01T00:28:51.003Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -483,16 +514,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.116.1"
|
version = "0.118.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "starlette" },
|
{ name = "starlette" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/28/3c/2b9345a6504e4055eaa490e0b41c10e338ad61d9aeaae41d97807873cdf2/fastapi-0.118.0.tar.gz", hash = "sha256:5e81654d98c4d2f53790a7d32d25a7353b30c81441be7d0958a26b5d761fa1c8", size = 310536, upload-time = "2025-09-29T03:37:23.126Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/20/54e2bdaad22ca91a59455251998d43094d5c3d3567c52c7c04774b3f43f2/fastapi-0.118.0-py3-none-any.whl", hash = "sha256:705137a61e2ef71019d2445b123aa8845bd97273c395b744d5a7dfe559056855", size = 97694, upload-time = "2025-09-29T03:37:21.338Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -595,7 +626,6 @@ dependencies = [
|
|||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "loguru" },
|
{ name = "loguru" },
|
||||||
{ name = "maxminddb" },
|
{ name = "maxminddb" },
|
||||||
{ name = "msgpack-lazer-api" },
|
|
||||||
{ name = "newrelic" },
|
{ name = "newrelic" },
|
||||||
{ name = "osupyparser" },
|
{ name = "osupyparser" },
|
||||||
{ name = "passlib", extra = ["bcrypt"] },
|
{ name = "passlib", extra = ["bcrypt"] },
|
||||||
@@ -617,7 +647,6 @@ dependencies = [
|
|||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "maturin" },
|
|
||||||
{ name = "pre-commit" },
|
{ name = "pre-commit" },
|
||||||
{ name = "pyright" },
|
{ name = "pyright" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
@@ -639,7 +668,6 @@ requires-dist = [
|
|||||||
{ name = "httpx", specifier = ">=0.28.1" },
|
{ name = "httpx", specifier = ">=0.28.1" },
|
||||||
{ name = "loguru", specifier = ">=0.7.3" },
|
{ name = "loguru", specifier = ">=0.7.3" },
|
||||||
{ name = "maxminddb", specifier = ">=2.8.2" },
|
{ name = "maxminddb", specifier = ">=2.8.2" },
|
||||||
{ name = "msgpack-lazer-api", editable = "packages/msgpack_lazer_api" },
|
|
||||||
{ name = "newrelic", specifier = ">=10.1.0" },
|
{ name = "newrelic", specifier = ">=10.1.0" },
|
||||||
{ name = "osupyparser", git = "https://github.com/MingxuanGame/osupyparser.git" },
|
{ name = "osupyparser", git = "https://github.com/MingxuanGame/osupyparser.git" },
|
||||||
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
|
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
|
||||||
@@ -661,7 +689,6 @@ requires-dist = [
|
|||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "maturin", specifier = ">=1.9.2" },
|
|
||||||
{ name = "pre-commit", specifier = ">=4.2.0" },
|
{ name = "pre-commit", specifier = ">=4.2.0" },
|
||||||
{ name = "pyright", specifier = ">=1.1.404" },
|
{ name = "pyright", specifier = ">=1.1.404" },
|
||||||
{ name = "ruff", specifier = ">=0.12.4" },
|
{ name = "ruff", specifier = ">=0.12.4" },
|
||||||
@@ -762,11 +789,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "identify"
|
name = "identify"
|
||||||
version = "2.6.14"
|
version = "2.6.15"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" },
|
{ url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -814,61 +841,65 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markupsafe"
|
name = "markupsafe"
|
||||||
version = "3.0.2"
|
version = "3.0.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
|
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
|
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
|
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
|
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
|
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
|
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
|
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
|
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
|
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
|
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
|
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
|
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
|
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
|
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
|
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
|
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
|
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
|
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
|
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
|
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
|
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||||
]
|
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||||
[[package]]
|
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||||
name = "maturin"
|
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||||
version = "1.9.4"
|
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||||
source = { registry = "https://pypi.org/simple" }
|
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/13/7c/b11b870fc4fd84de2099906314ce45488ae17be32ff5493519a6cddc518a/maturin-1.9.4.tar.gz", hash = "sha256:235163a0c99bc6f380fb8786c04fd14dcf6cd622ff295ea3de525015e6ac40cf", size = 213647, upload-time = "2025-08-27T11:37:57.079Z" }
|
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||||
wheels = [
|
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/90/0d99389eea1939116fca841cad0763600c8d3183a02a9478d066736c60e8/maturin-1.9.4-py3-none-linux_armv6l.whl", hash = "sha256:6ff37578e3f5fdbe685110d45f60af1f5a7dfce70a1e26dfe3810af66853ecae", size = 8276133, upload-time = "2025-08-27T11:37:23.325Z" },
|
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/ed/c8ec68b383e50f084bf1fa9605e62a90cd32a3f75d9894ed3a6e5d4cc5b3/maturin-1.9.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f3837bb53611b2dafa1c090436c330f2d743ba305ef00d8801a371f4495e7e1b", size = 15994496, upload-time = "2025-08-27T11:37:27.092Z" },
|
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/4e/401ff5f3cfc6b123364d4b94379bf910d7baee32c9c95b72784ff2329357/maturin-1.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4227d627d8e3bfe45877a8d65e9d8351a9d01434549f0da75d2c06a1b570de58", size = 8362228, upload-time = "2025-08-27T11:37:31.181Z" },
|
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/8e/c56176dd360da9650c62b8a5ecfb85432cf011e97e46c186901e6996002e/maturin-1.9.4-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:1bb2aa0fa29032e9c5aac03ac400396ddea12cadef242f8967e9c8ef715313a1", size = 8271397, upload-time = "2025-08-27T11:37:33.672Z" },
|
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/46/001fcc5c6ad509874896418d6169a61acd619df5b724f99766308c44a99f/maturin-1.9.4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:a0868d52934c8a5d1411b42367633fdb5cd5515bec47a534192282167448ec30", size = 8775625, upload-time = "2025-08-27T11:37:35.86Z" },
|
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/2e/26fa7574f01c19b7a74680fd70e5bae2e8c40fed9683d1752e765062cc2b/maturin-1.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:68b7b833b25741c0f553b78e8b9e095b31ae7c6611533b3c7b71f84c2cb8fc44", size = 8051117, upload-time = "2025-08-27T11:37:38.278Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/ee/ca7308832d4f5b521c1aa176d9265f6f93e0bd1ad82a90fd9cd799f6b28c/maturin-1.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:08dc86312afee55af778af919818632e35d8d0464ccd79cb86700d9ea560ccd7", size = 8132122, upload-time = "2025-08-27T11:37:40.499Z" },
|
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/e8/c623955da75e801a06942edf1fdc4e772a9e8fbc1ceebbdc85d59584dc10/maturin-1.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:ef20ffdd943078c4c3699c29fb2ed722bb6b4419efdade6642d1dbf248f94a70", size = 10586762, upload-time = "2025-08-27T11:37:42.718Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/4b/19ad558fdf54e151b1b4916ed45f1952ada96684ee6db64f9cd91cabec09/maturin-1.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:368e958468431dfeec80f75eea9639b4356d8c42428b0128444424b083fecfb0", size = 8926988, upload-time = "2025-08-27T11:37:45.492Z" },
|
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/27/153ad15eccae26921e8a01812da9f3b7f9013368f8f92c36853f2043b2a3/maturin-1.9.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:273f879214f63f79bfe851cd7d541f8150bdbfae5dfdc3c0c4d125d02d1f41b4", size = 8536758, upload-time = "2025-08-27T11:37:48.213Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/e3/f304c3bdc3fba9adebe5348d4d2dd015f1152c0a9027aaf52cae0bb182c8/maturin-1.9.4-py3-none-win32.whl", hash = "sha256:ed2e54d132ace7e61829bd49709331007dd9a2cc78937f598aa76a4f69b6804d", size = 7265200, upload-time = "2025-08-27T11:37:50.881Z" },
|
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/14/f86d0124bf1816b99005c058a1dbdca7cb5850d9cf4b09dcae07a1bc6201/maturin-1.9.4-py3-none-win_amd64.whl", hash = "sha256:8e450bb2c9afdf38a0059ee2e1ec2b17323f152b59c16f33eb9c74edaf1f9f79", size = 8237391, upload-time = "2025-08-27T11:37:53.23Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/25/8320fc2591e45b750c3ae71fa596b47aefa802d07d6abaaa719034a85160/maturin-1.9.4-py3-none-win_arm64.whl", hash = "sha256:7a6f980a9b67a5c13c844c268eabd855b54a6a765df4b4bb07d15a990572a4c9", size = 6988277, upload-time = "2025-08-27T11:37:55.429Z" },
|
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -925,10 +956,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/62/33/09601f476fd9d494e967f15c1e05aa1e35bdf5ee54555596e05e5c9ec8c9/maxminddb-2.8.2-cp314-cp314t-win_arm64.whl", hash = "sha256:929a00528db82ffa5aa928a9cd1a972e8f93c36243609c25574dfd920c21533b", size = 33990, upload-time = "2025-07-25T20:31:23.367Z" },
|
{ url = "https://files.pythonhosted.org/packages/62/33/09601f476fd9d494e967f15c1e05aa1e35bdf5ee54555596e05e5c9ec8c9/maxminddb-2.8.2-cp314-cp314t-win_arm64.whl", hash = "sha256:929a00528db82ffa5aa928a9cd1a972e8f93c36243609c25574dfd920c21533b", size = 33990, upload-time = "2025-07-25T20:31:23.367Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "msgpack-lazer-api"
|
|
||||||
source = { editable = "packages/msgpack_lazer_api" }
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.6.4"
|
version = "6.6.4"
|
||||||
@@ -994,18 +1021,18 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "newrelic"
|
name = "newrelic"
|
||||||
version = "10.17.0"
|
version = "11.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/82/71eb49483a2675eec2a5cfce2ea08c898c9329072a83f6671d2a6f68d101/newrelic-10.17.0.tar.gz", hash = "sha256:f092109ac024f9524fafdd06126924c0fbd2af54684571167c0ee1d1cc1bcb7d", size = 1286818, upload-time = "2025-09-04T22:15:25.988Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/99/dffd5c418b3551b4598871c2e74c39933d2622e1996f30d751f9bd2f4de9/newrelic-11.0.0.tar.gz", hash = "sha256:3419599597dfcb5c7dd78dd46d12097d20c72b19cc1c89218783804976d0931e", size = 1287164, upload-time = "2025-09-25T22:50:28.4Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/e3/c99b99ebedccb960b835a656d27e0d062904fbe49f8d98bf77eae617b649/newrelic-10.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ab9cbce3f8da5bfcd085b8a59591cfb653d75834b362e3eccb86bdf21eea917", size = 863000, upload-time = "2025-09-04T22:14:55.091Z" },
|
{ url = "https://files.pythonhosted.org/packages/56/f5/ecdd3300dda9168a207eeea53db63219d2b6400e16f2a53b89c3c1c973e1/newrelic-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b42fe1c4c9e35a9fdf4c264faa1e92f6918d0014c31dad69fede2798bc4482bb", size = 885585, upload-time = "2025-09-25T22:50:02.947Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/c8/7c5175f1e070063033a38aa50b920d3523c018a4244890068d211bd98c14/newrelic-10.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:889969407457a9344927b1a9e9d5afde77345857db4d2ea75b119d02c02d9174", size = 862657, upload-time = "2025-09-04T22:14:56.546Z" },
|
{ url = "https://files.pythonhosted.org/packages/78/98/35ac917abd6340e81395a12075f131e1b102bf1ca40a598f601b409bc80b/newrelic-11.0.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:927b150eefd9843a5abfc407a9eeac02f4119a842ea0597cd30e33c4b1d5bc61", size = 885753, upload-time = "2025-09-25T22:50:04.653Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/37/34d71dcf78c7f539381ba71e0b7624ce170822d13364768241f11832ad0d/newrelic-10.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d74c06863356b19c1fcf19900f85057ab33ef779aa3e0de3cb7e3d3f37ca8e20", size = 860626, upload-time = "2025-09-04T22:14:58.707Z" },
|
{ url = "https://files.pythonhosted.org/packages/c8/86/976281a0882ec357d5a67b3ea6e5c7a80552b42196e5b3c6a13f9ff1ba77/newrelic-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1ca9deba4f44e13eab9eb9866757135a88544b33d971e0929c6884821286bb5", size = 883124, upload-time = "2025-09-25T22:50:06.085Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/40/137ed6c2558ba38237193501e3da466dfefe019a7fdf43e7c57eda0bb293/newrelic-10.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:083777458f93d16ae9e605fd66a6298d856e32deea11cf3270ed237858dcbfe6", size = 860491, upload-time = "2025-09-04T22:15:00.497Z" },
|
{ url = "https://files.pythonhosted.org/packages/97/c7/4f5acac16611050200c555ad6ba49a3c95cc59b6fcab2dec45a2f45120c3/newrelic-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee444805ea0df9bcd3bb62873e16f8c5fe3848e0169fc90f8f74b611a51ae2d0", size = 883631, upload-time = "2025-09-25T22:50:07.667Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/60/731dbb138b7ec21a292769f5bed36a207ea957fbd6bfabca8954bfb029b3/newrelic-10.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:165c7045da474342d4ceab67776e8aeb66e77674fab7484b4e2e4ea68d02ed4d", size = 862986, upload-time = "2025-09-04T22:15:01.831Z" },
|
{ url = "https://files.pythonhosted.org/packages/e7/1a/70d4f214c652e7f9df357e0723b7ac2bb7361d89e2e0f403b6c458d8c7e8/newrelic-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a060dd3918e19e629c9e0e5c16b8c97c4ea1989c7fc12e1845abec2b688e80f2", size = 885596, upload-time = "2025-09-25T22:50:09.041Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/d2/fb51ee95e13e00a81d526cdd12359dc543e66ccb975ac0e164ef1008a852/newrelic-10.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3c3c82c95b57fe30fc1f684bf920bd8b0ecae70a16cc11f55d0867ffb7520d", size = 862661, upload-time = "2025-09-04T22:15:03.303Z" },
|
{ url = "https://files.pythonhosted.org/packages/08/02/aa6fdb9089c64686ca06d1faa7411083ff961aeccd37e6e8a9283c68b1a5/newrelic-11.0.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5bbdc9c1062d8178dd47aaf26bda8c1ebc2700ed82927973ac005cdd1668a8", size = 885777, upload-time = "2025-09-25T22:50:10.574Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/e2/adea40e0e6d6ef16083fbb48aae45a353910b61edbd253969ded2042b1e0/newrelic-10.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5789f739e6ca1c4a49649e406b538d67c063ec33ab4658d01049303dfad9398b", size = 860798, upload-time = "2025-09-04T22:15:05.018Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/0a/391f17dd79507a3082a7ab1c3580d947937f51578cdfe2e0aedbf5e9286b/newrelic-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c27e089768c8883d4df35f5f1f406ec475ff5053f1463082e01115b25881e4a6", size = 883238, upload-time = "2025-09-25T22:50:12.364Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/bb/2ee2ef672ab1dafcc40c17a13fa33cd5055275426f8853fbfadde563ccf6/newrelic-10.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4144dd96fa55772456326b3a6032a53d54aa9245ffc5041b002ce9eb5dbd0992", size = 860672, upload-time = "2025-09-04T22:15:06.346Z" },
|
{ url = "https://files.pythonhosted.org/packages/96/02/b878cc2f472dd9868eb46a3b6de40a5b04991db697ddf25179e967eb2967/newrelic-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eb16e55ac25781812bfde1e7e4c181857f9578957919106e049e1a784b333942", size = 883744, upload-time = "2025-09-25T22:50:13.738Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1266,16 +1293,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-settings"
|
name = "pydantic-settings"
|
||||||
version = "2.10.1"
|
version = "2.11.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
|
{ url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1298,15 +1325,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyright"
|
name = "pyright"
|
||||||
version = "1.1.405"
|
version = "1.1.406"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "nodeenv" },
|
{ name = "nodeenv" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/6c/ba4bbee22e76af700ea593a1d8701e3225080956753bee9750dcc25e2649/pyright-1.1.405.tar.gz", hash = "sha256:5c2a30e1037af27eb463a1cc0b9f6d65fec48478ccf092c1ac28385a15c55763", size = 4068319, upload-time = "2025-09-04T03:37:06.776Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f7/16/6b4fbdd1fef59a0292cbb99f790b44983e390321eccbc5921b4d161da5d1/pyright-1.1.406.tar.gz", hash = "sha256:c4872bc58c9643dac09e8a2e74d472c62036910b3bd37a32813989ef7576ea2c", size = 4113151, upload-time = "2025-10-02T01:04:45.488Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/1a/524f832e1ff1962a22a1accc775ca7b143ba2e9f5924bb6749dce566784a/pyright-1.1.405-py3-none-any.whl", hash = "sha256:a2cb13700b5508ce8e5d4546034cb7ea4aedb60215c6c33f56cec7f53996035a", size = 5905038, upload-time = "2025-09-04T03:37:04.913Z" },
|
{ url = "https://files.pythonhosted.org/packages/f6/a2/e309afbb459f50507103793aaef85ca4348b66814c86bc73908bdeb66d12/pyright-1.1.406-py3-none-any.whl", hash = "sha256:1d81fb43c2407bf566e97e57abb01c811973fdb21b2df8df59f870f688bdca71", size = 5980982, upload-time = "2025-10-02T01:04:43.137Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1360,28 +1387,48 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.2"
|
version = "6.0.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
|
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
|
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
|
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
|
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
|
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
|
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
|
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
|
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
|
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
|
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1433,28 +1480,28 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.13.0"
|
version = "0.13.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/1a/1f4b722862840295bcaba8c9e5261572347509548faaa99b2d57ee7bfe6a/ruff-0.13.0.tar.gz", hash = "sha256:5b4b1ee7eb35afae128ab94459b13b2baaed282b1fb0f472a73c82c996c8ae60", size = 5372863, upload-time = "2025-09-10T16:25:37.917Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/8e/f9f9ca747fea8e3ac954e3690d4698c9737c23b51731d02df999c150b1c9/ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e", size = 5438533, upload-time = "2025-10-02T19:29:31.582Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/fe/6f87b419dbe166fd30a991390221f14c5b68946f389ea07913e1719741e0/ruff-0.13.0-py3-none-linux_armv6l.whl", hash = "sha256:137f3d65d58ee828ae136a12d1dc33d992773d8f7644bc6b82714570f31b2004", size = 12187826, upload-time = "2025-09-10T16:24:39.5Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/33/8f7163553481466a92656d35dea9331095122bb84cf98210bef597dd2ecd/ruff-0.13.3-py3-none-linux_armv6l.whl", hash = "sha256:311860a4c5e19189c89d035638f500c1e191d283d0cc2f1600c8c80d6dcd430c", size = 12484040, upload-time = "2025-10-02T19:28:49.199Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/25/c92296b1fc36d2499e12b74a3fdb230f77af7bdf048fad7b0a62e94ed56a/ruff-0.13.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:21ae48151b66e71fd111b7d79f9ad358814ed58c339631450c66a4be33cc28b9", size = 12933428, upload-time = "2025-09-10T16:24:43.866Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/b5/4a21a4922e5dd6845e91896b0d9ef493574cbe061ef7d00a73c61db531af/ruff-0.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2bdad6512fb666b40fcadb65e33add2b040fc18a24997d2e47fee7d66f7fcae2", size = 13122975, upload-time = "2025-10-02T19:28:52.446Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/cf/40bc7221a949470307d9c35b4ef5810c294e6cfa3caafb57d882731a9f42/ruff-0.13.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:64de45f4ca5441209e41742d527944635a05a6e7c05798904f39c85bafa819e3", size = 12095543, upload-time = "2025-09-10T16:24:46.638Z" },
|
{ url = "https://files.pythonhosted.org/packages/40/90/15649af836d88c9f154e5be87e64ae7d2b1baa5a3ef317cb0c8fafcd882d/ruff-0.13.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc6fa4637284708d6ed4e5e970d52fc3b76a557d7b4e85a53013d9d201d93286", size = 12346621, upload-time = "2025-10-02T19:28:54.712Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/03/8b5ff2a211efb68c63a1d03d157e924997ada87d01bebffbd13a0f3fcdeb/ruff-0.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b2c653ae9b9d46e0ef62fc6fbf5b979bda20a0b1d2b22f8f7eb0cde9f4963b8", size = 12312489, upload-time = "2025-09-10T16:24:49.556Z" },
|
{ url = "https://files.pythonhosted.org/packages/a5/42/bcbccb8141305f9a6d3f72549dd82d1134299177cc7eaf832599700f95a7/ruff-0.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c9e6469864f94a98f412f20ea143d547e4c652f45e44f369d7b74ee78185838", size = 12574408, upload-time = "2025-10-02T19:28:56.679Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/fc/2336ef6d5e9c8d8ea8305c5f91e767d795cd4fc171a6d97ef38a5302dadc/ruff-0.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cec632534332062bc9eb5884a267b689085a1afea9801bf94e3ba7498a2d207", size = 11991631, upload-time = "2025-09-10T16:24:53.439Z" },
|
{ url = "https://files.pythonhosted.org/packages/ce/19/0f3681c941cdcfa2d110ce4515624c07a964dc315d3100d889fcad3bfc9e/ruff-0.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bf62b705f319476c78891e0e97e965b21db468b3c999086de8ffb0d40fd2822", size = 12285330, upload-time = "2025-10-02T19:28:58.79Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/7f/f6d574d100fca83d32637d7f5541bea2f5e473c40020bbc7fc4a4d5b7294/ruff-0.13.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd628101d9f7d122e120ac7c17e0a0f468b19bc925501dbe03c1cb7f5415b24", size = 13720602, upload-time = "2025-09-10T16:24:56.392Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/f8/387976bf00d126b907bbd7725219257feea58650e6b055b29b224d8cb731/ruff-0.13.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cc1abed87ce40cb07ee0667ce99dbc766c9f519eabfd948ed87295d8737c60", size = 13980815, upload-time = "2025-10-02T19:29:01.577Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/c8/a8a5b81d8729b5d1f663348d11e2a9d65a7a9bd3c399763b1a51c72be1ce/ruff-0.13.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afe37db8e1466acb173bb2a39ca92df00570e0fd7c94c72d87b51b21bb63efea", size = 14697751, upload-time = "2025-09-10T16:24:59.89Z" },
|
{ url = "https://files.pythonhosted.org/packages/0c/a6/7c8ec09d62d5a406e2b17d159e4817b63c945a8b9188a771193b7e1cc0b5/ruff-0.13.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4fb75e7c402d504f7a9a259e0442b96403fa4a7310ffe3588d11d7e170d2b1e3", size = 14987733, upload-time = "2025-10-02T19:29:04.036Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/f5/183ec292272ce7ec5e882aea74937f7288e88ecb500198b832c24debc6d3/ruff-0.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f96a8d90bb258d7d3358b372905fe7333aaacf6c39e2408b9f8ba181f4b6ef2", size = 14095317, upload-time = "2025-09-10T16:25:03.025Z" },
|
{ url = "https://files.pythonhosted.org/packages/97/e5/f403a60a12258e0fd0c2195341cfa170726f254c788673495d86ab5a9a9d/ruff-0.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b951f9d9afb39330b2bdd2dd144ce1c1335881c277837ac1b50bfd99985ed3", size = 14439848, upload-time = "2025-10-02T19:29:06.684Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/8d/7f9771c971724701af7926c14dab31754e7b303d127b0d3f01116faef456/ruff-0.13.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b5e3d883e4f924c5298e3f2ee0f3085819c14f68d1e5b6715597681433f153", size = 13144418, upload-time = "2025-09-10T16:25:06.272Z" },
|
{ url = "https://files.pythonhosted.org/packages/39/49/3de381343e89364c2334c9f3268b0349dc734fc18b2d99a302d0935c8345/ruff-0.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6052f8088728898e0a449f0dde8fafc7ed47e4d878168b211977e3e7e854f662", size = 13421890, upload-time = "2025-10-02T19:29:08.767Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/a6/7985ad1778e60922d4bef546688cd8a25822c58873e9ff30189cfe5dc4ab/ruff-0.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03447f3d18479df3d24917a92d768a89f873a7181a064858ea90a804a7538991", size = 13370843, upload-time = "2025-09-10T16:25:09.965Z" },
|
{ url = "https://files.pythonhosted.org/packages/ab/b5/c0feca27d45ae74185a6bacc399f5d8920ab82df2d732a17213fb86a2c4c/ruff-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc742c50f4ba72ce2a3be362bd359aef7d0d302bf7637a6f942eaa763bd292af", size = 13444870, upload-time = "2025-10-02T19:29:11.234Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/1c/bafdd5a7a05a50cc51d9f5711da704942d8dd62df3d8c70c311e98ce9f8a/ruff-0.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fbc6b1934eb1c0033da427c805e27d164bb713f8e273a024a7e86176d7f462cf", size = 13321891, upload-time = "2025-09-10T16:25:12.969Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/a1/b655298a1f3fda4fdc7340c3f671a4b260b009068fbeb3e4e151e9e3e1bf/ruff-0.13.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8e5640349493b378431637019366bbd73c927e515c9c1babfea3e932f5e68e1d", size = 13691599, upload-time = "2025-10-02T19:29:13.353Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/3e/7817f989cb9725ef7e8d2cee74186bf90555279e119de50c750c4b7a72fe/ruff-0.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8ab6a3e03665d39d4a25ee199d207a488724f022db0e1fe4002968abdb8001b", size = 12119119, upload-time = "2025-09-10T16:25:16.621Z" },
|
{ url = "https://files.pythonhosted.org/packages/32/b0/a8705065b2dafae007bcae21354e6e2e832e03eb077bb6c8e523c2becb92/ruff-0.13.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b139f638a80eae7073c691a5dd8d581e0ba319540be97c343d60fb12949c8d0", size = 12421893, upload-time = "2025-10-02T19:29:15.668Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/07/9df080742e8d1080e60c426dce6e96a8faf9a371e2ce22eef662e3839c95/ruff-0.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2a5c62f8ccc6dd2fe259917482de7275cecc86141ee10432727c4816235bc41", size = 11961594, upload-time = "2025-09-10T16:25:19.49Z" },
|
{ url = "https://files.pythonhosted.org/packages/0d/1e/cbe7082588d025cddbb2f23e6dfef08b1a2ef6d6f8328584ad3015b5cebd/ruff-0.13.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6b547def0a40054825de7cfa341039ebdfa51f3d4bfa6a0772940ed351d2746c", size = 12267220, upload-time = "2025-10-02T19:29:17.583Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/f4/ae1185349197d26a2316840cb4d6c3fba61d4ac36ed728bf0228b222d71f/ruff-0.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b7b85ca27aeeb1ab421bc787009831cffe6048faae08ad80867edab9f2760945", size = 12933377, upload-time = "2025-09-10T16:25:22.371Z" },
|
{ url = "https://files.pythonhosted.org/packages/a5/99/4086f9c43f85e0755996d09bdcb334b6fee9b1eabdf34e7d8b877fadf964/ruff-0.13.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9cc48a3564423915c93573f1981d57d101e617839bef38504f85f3677b3a0a3e", size = 13177818, upload-time = "2025-10-02T19:29:19.943Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/39/e776c10a3b349fc8209a905bfb327831d7516f6058339a613a8d2aaecacd/ruff-0.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:79ea0c44a3032af768cabfd9616e44c24303af49d633b43e3a5096e009ebe823", size = 13418555, upload-time = "2025-09-10T16:25:25.681Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/de/7b5db7e39947d9dc1c5f9f17b838ad6e680527d45288eeb568e860467010/ruff-0.13.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a993b17ec03719c502881cb2d5f91771e8742f2ca6de740034433a97c561989", size = 13618715, upload-time = "2025-10-02T19:29:22.527Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/09/dca8df3d48e8b3f4202bf20b1658898e74b6442ac835bfe2c1816d926697/ruff-0.13.0-py3-none-win32.whl", hash = "sha256:4e473e8f0e6a04e4113f2e1de12a5039579892329ecc49958424e5568ef4f768", size = 12141613, upload-time = "2025-09-10T16:25:28.664Z" },
|
{ url = "https://files.pythonhosted.org/packages/28/d3/bb25ee567ce2f61ac52430cf99f446b0e6d49bdfa4188699ad005fdd16aa/ruff-0.13.3-py3-none-win32.whl", hash = "sha256:f14e0d1fe6460f07814d03c6e32e815bff411505178a1f539a38f6097d3e8ee3", size = 12334488, upload-time = "2025-10-02T19:29:24.782Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/21/0647eb71ed99b888ad50e44d8ec65d7148babc0e242d531a499a0bbcda5f/ruff-0.13.0-py3-none-win_amd64.whl", hash = "sha256:48e5c25c7a3713eea9ce755995767f4dcd1b0b9599b638b12946e892123d1efb", size = 13258250, upload-time = "2025-09-10T16:25:31.773Z" },
|
{ url = "https://files.pythonhosted.org/packages/cf/49/12f5955818a1139eed288753479ba9d996f6ea0b101784bb1fe6977ec128/ruff-0.13.3-py3-none-win_amd64.whl", hash = "sha256:621e2e5812b691d4f244638d693e640f188bacbb9bc793ddd46837cea0503dd2", size = 13455262, upload-time = "2025-10-02T19:29:26.882Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/a3/03216a6a86c706df54422612981fb0f9041dbb452c3401501d4a22b942c9/ruff-0.13.0-py3-none-win_arm64.whl", hash = "sha256:ab80525317b1e1d38614addec8ac954f1b3e662de9d59114ecbf771d00cf613e", size = 12312357, upload-time = "2025-09-10T16:25:35.595Z" },
|
{ url = "https://files.pythonhosted.org/packages/fe/72/7b83242b26627a00e3af70d0394d68f8f02750d642567af12983031777fc/ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330", size = 12538484, upload-time = "2025-10-02T19:29:28.951Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1471,15 +1518,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentry-sdk"
|
name = "sentry-sdk"
|
||||||
version = "2.37.1"
|
version = "2.39.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi" },
|
{ name = "certifi" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/78/be/ffc232c32d0be18f8e4eff7a22dffc1f1fef2894703d64cc281a80e75da6/sentry_sdk-2.37.1.tar.gz", hash = "sha256:531751da91aa62a909b42a7be155b41f6bb0de9df6ae98441d23b95de2f98475", size = 346235, upload-time = "2025-09-09T13:48:27.137Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/4c/72/43294fa4bdd75c51610b5104a3ff834459ba653abb415150aa7826a249dd/sentry_sdk-2.39.0.tar.gz", hash = "sha256:8c185854d111f47f329ab6bc35993f28f7a6b7114db64aa426b326998cfa14e9", size = 348556, upload-time = "2025-09-25T09:15:39.064Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/c3/cba447ab531331d165d9003c04473be944a308ad916ca2345b5ef1969ed9/sentry_sdk-2.37.1-py2.py3-none-any.whl", hash = "sha256:baaaea6608ed3a639766a69ded06b254b106d32ad9d180bdbe58f3db9364592b", size = 368307, upload-time = "2025-09-09T13:48:25.271Z" },
|
{ url = "https://files.pythonhosted.org/packages/dd/44/4356cc64246ba7b2b920f7c97a85c3c52748e213e250b512ee8152eb559d/sentry_sdk-2.39.0-py2.py3-none-any.whl", hash = "sha256:ba655ca5e57b41569b18e2a5552cb3375209760a5d332cdd87c6c3f28f729602", size = 370851, upload-time = "2025-09-25T09:15:36.35Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -1545,28 +1592,28 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlmodel"
|
name = "sqlmodel"
|
||||||
version = "0.0.24"
|
version = "0.0.25"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "sqlalchemy" },
|
{ name = "sqlalchemy" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780, upload-time = "2025-03-07T05:43:32.887Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ea/80/d9c098a88724ee4554907939cf39590cf67e10c6683723216e228d3315f7/sqlmodel-0.0.25.tar.gz", hash = "sha256:56548c2e645975b1ed94d6c53f0d13c85593f57926a575e2bf566650b2243fa4", size = 117075, upload-time = "2025-09-17T21:44:41.219Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622, upload-time = "2025-03-07T05:43:30.37Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/cf/5d175ce8de07fe694ec4e3d4d65c2dd06cc30f6c79599b31f9d2f6dd2830/sqlmodel-0.0.25-py3-none-any.whl", hash = "sha256:c98234cda701fb77e9dcbd81688c23bb251c13bb98ce1dd8d4adc467374d45b7", size = 28893, upload-time = "2025-09-17T21:44:39.764Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "starlette"
|
name = "starlette"
|
||||||
version = "0.47.3"
|
version = "0.48.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload-time = "2025-08-24T13:36:42.122Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" },
|
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1713,14 +1760,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-inspection"
|
name = "typing-inspection"
|
||||||
version = "0.4.1"
|
version = "0.4.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1755,15 +1802,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.35.0"
|
version = "0.37.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "h11" },
|
{ name = "h11" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
|
{ url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user