Compare commits
18 Commits
feat/plugi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ce5f3cc16 | ||
|
|
96c8b70df6 | ||
|
|
8923d714a7 | ||
|
|
e5802aefbb | ||
|
|
c323373510 | ||
|
|
fe0c13bdd3 | ||
|
|
1c3b309804 | ||
|
|
282eda3250 | ||
|
|
38a2c8720b | ||
|
|
87ffc6f581 | ||
|
|
735a22d500 | ||
|
|
a6c596318e | ||
|
|
fed1471129 | ||
|
|
a58b4cb172 | ||
|
|
e5a4a0d9e4 | ||
|
|
10095f7da2 | ||
|
|
18574587e3 | ||
|
|
f628061971 |
@@ -73,7 +73,6 @@ services:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
upstream app {
|
||||
server devcontainer:8000;
|
||||
zone app_backend 64k;
|
||||
server devcontainer:8000 resolve;
|
||||
}
|
||||
|
||||
upstream spectator {
|
||||
server devcontainer:8086;
|
||||
zone app_backend 64k;
|
||||
server devcontainer:8086 resolve;
|
||||
}
|
||||
|
||||
server {
|
||||
|
||||
@@ -7,7 +7,7 @@ ci:
|
||||
autoupdate_commit_msg: "chore(deps): auto update by pre-commit hooks"
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.7
|
||||
rev: v0.14.10
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: [--fix]
|
||||
|
||||
@@ -31,6 +31,9 @@ class AvailableModes(NamedTuple):
|
||||
|
||||
|
||||
class PerformanceCalculator(abc.ABC):
|
||||
def __init__(self, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get_available_modes(self) -> AvailableModes:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -33,7 +33,7 @@ class AvailableRulesetResp(TypedDict):
|
||||
|
||||
|
||||
class PerformanceServerPerformanceCalculator(BasePerformanceCalculator):
|
||||
def __init__(self, server_url: str = "http://localhost:5225") -> None:
|
||||
def __init__(self, server_url: str = "http://localhost:5225", **kwargs) -> None: # noqa: ARG002
|
||||
self.server_url = server_url
|
||||
|
||||
self._available_modes: AvailableModes | None = None
|
||||
|
||||
@@ -705,6 +705,21 @@ CALCULATOR_CONFIG='{}'
|
||||
Field(default=True, description="检查自定义 ruleset 版本"),
|
||||
"反作弊设置",
|
||||
]
|
||||
check_client_version: Annotated[
|
||||
bool,
|
||||
Field(default=True, description="检查客户端版本"),
|
||||
"反作弊设置",
|
||||
]
|
||||
client_version_urls: Annotated[
|
||||
list[str],
|
||||
Field(
|
||||
default=["https://raw.githubusercontent.com/GooGuTeam/g0v0-client-versions/main/version_list.json"],
|
||||
description=(
|
||||
"客户端版本列表 URL, 查看 https://github.com/GooGuTeam/g0v0-client-versions 来添加你自己的客户端"
|
||||
),
|
||||
),
|
||||
"反作弊设置",
|
||||
]
|
||||
|
||||
# 存储设置
|
||||
storage_service: Annotated[
|
||||
|
||||
@@ -12,6 +12,7 @@ from sqlmodel import (
|
||||
Column,
|
||||
Field,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Relationship,
|
||||
select,
|
||||
)
|
||||
@@ -32,11 +33,7 @@ class BeatmapPlaycountsDict(TypedDict):
|
||||
|
||||
|
||||
class BeatmapPlaycountsModel(AsyncAttrs, DatabaseModel[BeatmapPlaycountsDict]):
|
||||
__tablename__: str = "beatmap_playcounts"
|
||||
|
||||
id: int | None = Field(
|
||||
default=None, sa_column=Column(BigInteger, primary_key=True, autoincrement=True), exclude=True
|
||||
)
|
||||
id: int = Field(default=None, sa_column=Column(BigInteger, primary_key=True, autoincrement=True), exclude=True)
|
||||
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True))
|
||||
beatmap_id: int = Field(foreign_key="beatmaps.id", index=True)
|
||||
playcount: int = Field(default=0, exclude=True)
|
||||
@@ -68,6 +65,9 @@ class BeatmapPlaycountsModel(AsyncAttrs, DatabaseModel[BeatmapPlaycountsDict]):
|
||||
|
||||
|
||||
class BeatmapPlaycounts(BeatmapPlaycountsModel, table=True):
|
||||
__tablename__: str = "beatmap_playcounts"
|
||||
__table_args__ = (Index("idx_beatmap_playcounts_playcount_id", "playcount", "id"),)
|
||||
|
||||
user: "User" = Relationship()
|
||||
beatmap: "Beatmap" = Relationship()
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ class ChatChannelModel(DatabaseModel[ChatChannelDict]):
|
||||
target_user_id = next(u for u in users if u != user.id)
|
||||
target_name = await session.exec(select(User.username).where(User.id == target_user_id))
|
||||
return target_name.one()
|
||||
return channel.name
|
||||
return channel.channel_name
|
||||
|
||||
@included
|
||||
@staticmethod
|
||||
@@ -208,7 +208,7 @@ class ChatChannelModel(DatabaseModel[ChatChannelDict]):
|
||||
class ChatChannel(ChatChannelModel, table=True):
|
||||
__tablename__: str = "chat_channels"
|
||||
|
||||
name: str = Field(sa_column=Column(VARCHAR(50), index=True))
|
||||
channel_name: str = Field(sa_column=Column(name="name", type_=VARCHAR(50), index=True))
|
||||
|
||||
@classmethod
|
||||
async def get(cls, channel: str | int, session: AsyncSession) -> "ChatChannel | None":
|
||||
@@ -218,7 +218,7 @@ class ChatChannel(ChatChannelModel, table=True):
|
||||
channel_ = result.first()
|
||||
if channel_ is not None:
|
||||
return channel_
|
||||
result = await session.exec(select(ChatChannel).where(ChatChannel.name == channel))
|
||||
result = await session.exec(select(ChatChannel).where(ChatChannel.channel_name == channel))
|
||||
return result.first()
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -17,7 +17,8 @@ from sqlmodel import (
|
||||
|
||||
class FavouriteBeatmapset(AsyncAttrs, SQLModel, table=True):
|
||||
__tablename__: str = "favourite_beatmapset"
|
||||
id: int | None = Field(
|
||||
|
||||
id: int = Field(
|
||||
default=None,
|
||||
sa_column=Column(BigInteger, autoincrement=True, primary_key=True),
|
||||
exclude=True,
|
||||
|
||||
@@ -56,9 +56,9 @@ from .user import User, UserDict, UserModel
|
||||
|
||||
from pydantic import BaseModel, field_serializer, field_validator
|
||||
from redis.asyncio import Redis
|
||||
from sqlalchemy import Boolean, Column, DateTime, TextClause
|
||||
from sqlalchemy import Boolean, Column, DateTime, Index, TextClause, exists
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||
from sqlalchemy.orm import Mapped, joinedload
|
||||
from sqlalchemy.orm import Mapped, aliased, joinedload
|
||||
from sqlalchemy.sql.elements import ColumnElement
|
||||
from sqlmodel import (
|
||||
JSON,
|
||||
@@ -158,7 +158,7 @@ class ScoreModel(AsyncAttrs, DatabaseModel[ScoreDict]):
|
||||
total_score: int = Field(default=0, sa_column=Column(BigInteger))
|
||||
maximum_statistics: ScoreStatistics = Field(sa_column=Column(JSON), default_factory=dict)
|
||||
mods: list[APIMod] = Field(sa_column=Column(JSON))
|
||||
total_score_without_mods: int = Field(default=0, sa_column=Column(BigInteger), exclude=True)
|
||||
total_score_without_mods: int = Field(default=0, sa_column=Column(BigInteger))
|
||||
|
||||
# solo
|
||||
classic_total_score: int | None = Field(default=0, sa_column=Column(BigInteger))
|
||||
@@ -414,6 +414,11 @@ class ScoreModel(AsyncAttrs, DatabaseModel[ScoreDict]):
|
||||
|
||||
class Score(ScoreModel, table=True):
|
||||
__tablename__: str = "scores"
|
||||
__table_args__ = (
|
||||
Index("idx_score_user_mode_pinned", "user_id", "gamemode", "pinned_order", "id"),
|
||||
Index("idx_score_user_mode_pp", "user_id", "gamemode", "pp", "id"),
|
||||
Index("idx_score_user_mode_date", "user_id", "gamemode", "ended_at", "id"),
|
||||
)
|
||||
|
||||
# ScoreStatistics
|
||||
n300: int = Field(exclude=True)
|
||||
@@ -828,64 +833,54 @@ async def get_user_best_score_with_mod_in_beatmap(
|
||||
|
||||
|
||||
async def get_user_first_scores(
|
||||
session: AsyncSession, user_id: int, mode: GameMode, limit: int = 5, offset: int = 0
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
mode: GameMode,
|
||||
limit: int = 5,
|
||||
offset: int = 0,
|
||||
cursor_id: int | None = None,
|
||||
) -> list[TotalScoreBestScore]:
|
||||
rownum = (
|
||||
func.row_number()
|
||||
.over(
|
||||
partition_by=(col(TotalScoreBestScore.beatmap_id), col(TotalScoreBestScore.gamemode)),
|
||||
order_by=col(TotalScoreBestScore.total_score).desc(),
|
||||
)
|
||||
.label("rn")
|
||||
# Alias for the subquery table
|
||||
s2 = aliased(TotalScoreBestScore)
|
||||
|
||||
query = select(TotalScoreBestScore).where(
|
||||
TotalScoreBestScore.user_id == user_id,
|
||||
TotalScoreBestScore.gamemode == mode,
|
||||
)
|
||||
|
||||
# Step 1: Fetch top score_ids in Python
|
||||
subq = (
|
||||
select(
|
||||
col(TotalScoreBestScore.score_id).label("score_id"),
|
||||
col(TotalScoreBestScore.user_id).label("user_id"),
|
||||
rownum,
|
||||
)
|
||||
.where(col(TotalScoreBestScore.gamemode) == mode)
|
||||
.subquery()
|
||||
# Subquery for NOT EXISTS
|
||||
# Check if there is a score with same beatmap, same mode, but higher total_score
|
||||
subq = select(1).where(
|
||||
s2.beatmap_id == TotalScoreBestScore.beatmap_id,
|
||||
s2.gamemode == TotalScoreBestScore.gamemode,
|
||||
s2.total_score > TotalScoreBestScore.total_score,
|
||||
)
|
||||
|
||||
top_ids_stmt = select(subq.c.score_id).where(subq.c.rn == 1, subq.c.user_id == user_id).limit(limit).offset(offset)
|
||||
query = query.where(~exists(subq))
|
||||
|
||||
top_ids = await session.exec(top_ids_stmt)
|
||||
top_ids = list(top_ids)
|
||||
if cursor_id:
|
||||
query = query.where(TotalScoreBestScore.score_id < cursor_id)
|
||||
|
||||
stmt = (
|
||||
select(TotalScoreBestScore)
|
||||
.where(col(TotalScoreBestScore.score_id).in_(top_ids))
|
||||
.order_by(col(TotalScoreBestScore.total_score).desc())
|
||||
)
|
||||
query = query.order_by(col(TotalScoreBestScore.score_id).desc()).limit(limit).offset(offset)
|
||||
|
||||
result = await session.exec(stmt)
|
||||
result = await session.exec(query)
|
||||
return list(result.all())
|
||||
|
||||
|
||||
async def get_user_first_score_count(session: AsyncSession, user_id: int, mode: GameMode) -> int:
|
||||
rownum = (
|
||||
func.row_number()
|
||||
.over(
|
||||
partition_by=(col(TotalScoreBestScore.beatmap_id), col(TotalScoreBestScore.gamemode)),
|
||||
order_by=col(TotalScoreBestScore.total_score).desc(),
|
||||
s2 = aliased(TotalScoreBestScore)
|
||||
query = select(func.count()).where(
|
||||
TotalScoreBestScore.user_id == user_id,
|
||||
TotalScoreBestScore.gamemode == mode,
|
||||
)
|
||||
.label("rn")
|
||||
subq = select(1).where(
|
||||
s2.beatmap_id == TotalScoreBestScore.beatmap_id,
|
||||
s2.gamemode == TotalScoreBestScore.gamemode,
|
||||
s2.total_score > TotalScoreBestScore.total_score,
|
||||
)
|
||||
subq = (
|
||||
select(
|
||||
col(TotalScoreBestScore.score_id).label("score_id"),
|
||||
col(TotalScoreBestScore.user_id).label("user_id"),
|
||||
rownum,
|
||||
)
|
||||
.where(col(TotalScoreBestScore.gamemode) == mode)
|
||||
.subquery()
|
||||
)
|
||||
count_stmt = select(func.count()).where(subq.c.rn == 1, subq.c.user_id == user_id)
|
||||
query = query.where(~exists(subq))
|
||||
|
||||
result = await session.exec(count_stmt)
|
||||
result = await session.exec(query)
|
||||
return result.one()
|
||||
|
||||
|
||||
@@ -975,8 +970,6 @@ async def process_score(
|
||||
score_token: ScoreToken,
|
||||
info: SoloScoreSubmissionInfo,
|
||||
session: AsyncSession,
|
||||
item_id: int | None = None,
|
||||
room_id: int | None = None,
|
||||
) -> Score:
|
||||
gamemode = GameMode.from_int(info.ruleset_id).to_special_mode(info.mods)
|
||||
logger.info(
|
||||
@@ -1014,8 +1007,8 @@ async def process_score(
|
||||
nsmall_tick_hit=info.statistics.get(HitResult.SMALL_TICK_HIT, 0),
|
||||
nlarge_tick_hit=info.statistics.get(HitResult.LARGE_TICK_HIT, 0),
|
||||
nslider_tail_hit=info.statistics.get(HitResult.SLIDER_TAIL_HIT, 0),
|
||||
playlist_item_id=item_id,
|
||||
room_id=room_id,
|
||||
playlist_item_id=score_token.playlist_item_id,
|
||||
room_id=score_token.room_id,
|
||||
maximum_statistics=info.maximum_statistics,
|
||||
processed=True,
|
||||
ranked=ranked,
|
||||
|
||||
@@ -13,17 +13,6 @@ from sqlmodel import BigInteger, Field, ForeignKey, Relationship, SQLModel
|
||||
|
||||
|
||||
class ScoreTokenBase(SQLModel, UTCBaseModel):
|
||||
score_id: int | None = Field(sa_column=Column(BigInteger), default=None)
|
||||
ruleset_id: GameMode
|
||||
playlist_item_id: int | None = Field(default=None) # playlist
|
||||
created_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime))
|
||||
updated_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime))
|
||||
|
||||
|
||||
class ScoreToken(ScoreTokenBase, table=True):
|
||||
__tablename__: str = "score_tokens"
|
||||
__table_args__ = (Index("idx_user_playlist", "user_id", "playlist_item_id"),)
|
||||
|
||||
id: int | None = Field(
|
||||
default=None,
|
||||
sa_column=Column(
|
||||
@@ -33,18 +22,28 @@ class ScoreToken(ScoreTokenBase, table=True):
|
||||
autoincrement=True,
|
||||
),
|
||||
)
|
||||
score_id: int | None = Field(sa_column=Column(BigInteger), default=None)
|
||||
ruleset_id: GameMode
|
||||
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id")))
|
||||
beatmap_id: int = Field(foreign_key="beatmaps.id")
|
||||
user: Mapped[User] = Relationship()
|
||||
room_id: int | None = Field(default=None)
|
||||
playlist_item_id: int | None = Field(default=None) # playlist
|
||||
created_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime))
|
||||
updated_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime))
|
||||
|
||||
|
||||
class ScoreToken(ScoreTokenBase, table=True):
|
||||
__tablename__: str = "score_tokens"
|
||||
__table_args__ = (
|
||||
Index("idx_user_playlist", "user_id", "playlist_item_id"),
|
||||
Index("idx_playlist_room", "playlist_item_id", "room_id"),
|
||||
)
|
||||
|
||||
user: Mapped[User] = Relationship()
|
||||
beatmap: Mapped[Beatmap] = Relationship()
|
||||
|
||||
|
||||
class ScoreTokenResp(ScoreTokenBase):
|
||||
id: int
|
||||
user_id: int
|
||||
beatmap_id: int
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, obj: ScoreToken) -> "ScoreTokenResp":
|
||||
return cls.model_validate(obj)
|
||||
|
||||
@@ -6,6 +6,7 @@ from app.models.score import GameMode, Rank
|
||||
from .statistics import UserStatistics
|
||||
from .user import User
|
||||
|
||||
from sqlalchemy import Index
|
||||
from sqlmodel import (
|
||||
JSON,
|
||||
BigInteger,
|
||||
@@ -27,6 +28,10 @@ if TYPE_CHECKING:
|
||||
|
||||
class TotalScoreBestScore(SQLModel, table=True):
|
||||
__tablename__: str = "total_score_best_scores"
|
||||
__table_args__ = (
|
||||
Index("ix_total_score_best_scores_user_mode_score", "user_id", "gamemode", "score_id"),
|
||||
Index("ix_total_score_best_scores_beatmap_mode_score", "beatmap_id", "gamemode", "total_score"),
|
||||
)
|
||||
user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True))
|
||||
score_id: int = Field(sa_column=Column(BigInteger, ForeignKey("scores.id"), primary_key=True))
|
||||
beatmap_id: int = Field(foreign_key="beatmaps.id", index=True)
|
||||
|
||||
10
app/dependencies/client_verification.py
Normal file
10
app/dependencies/client_verification.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from typing import Annotated
|
||||
|
||||
from app.service.client_verification_service import (
|
||||
ClientVerificationService as OriginalClientVerificationService,
|
||||
get_client_verification_service,
|
||||
)
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
ClientVerificationService = Annotated[OriginalClientVerificationService, Depends(get_client_verification_service)]
|
||||
@@ -1,4 +1,4 @@
|
||||
# Version: 2025.1108.0
|
||||
# Version: 2026.117.1
|
||||
# Auto-generated by scripts/generate_ruleset_attributes.py.
|
||||
# Schema generated by https://github.com/GooGuTeam/custom-rulesets
|
||||
# Do not edit this file directly.
|
||||
@@ -29,9 +29,9 @@ class OsuPerformanceAttributes(PerformanceAttributes):
|
||||
accuracy: float
|
||||
flashlight: float
|
||||
effective_miss_count: float
|
||||
speed_deviation: float | None = None
|
||||
speed_deviation: float | None
|
||||
combo_based_estimated_miss_count: float
|
||||
score_based_estimated_miss_count: float | None = None
|
||||
score_based_estimated_miss_count: float | None
|
||||
aim_estimated_slider_breaks: float
|
||||
speed_estimated_slider_breaks: float
|
||||
|
||||
@@ -66,7 +66,7 @@ class OsuDifficultyAttributes(DifficultyAttributes):
|
||||
class TaikoPerformanceAttributes(PerformanceAttributes):
|
||||
difficulty: float
|
||||
accuracy: float
|
||||
estimated_unstable_rate: float | None = None
|
||||
estimated_unstable_rate: float | None
|
||||
|
||||
|
||||
class TaikoDifficultyAttributes(DifficultyAttributes):
|
||||
|
||||
27
app/models/version.py
Normal file
27
app/models/version.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from typing import NamedTuple, TypedDict
|
||||
|
||||
|
||||
class VersionInfo(TypedDict):
|
||||
version: str
|
||||
release_date: str
|
||||
hashes: dict[str, str]
|
||||
|
||||
|
||||
class VersionList(TypedDict):
|
||||
name: str
|
||||
versions: list[VersionInfo]
|
||||
|
||||
|
||||
class VersionCheckResult(NamedTuple):
|
||||
is_valid: bool
|
||||
client_name: str = ""
|
||||
version: str = ""
|
||||
os: str = ""
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self.is_valid
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.is_valid:
|
||||
return f"{self.client_name} {self.version} ({self.os})"
|
||||
return "Invalid Client Version"
|
||||
@@ -59,7 +59,7 @@ async def _ensure_room_chat_channel(
|
||||
await db.commit()
|
||||
await db.refresh(ch)
|
||||
await db.refresh(room)
|
||||
if room.channel_id is None:
|
||||
if room.channel_id == 0:
|
||||
room.channel_id = ch.channel_id
|
||||
else:
|
||||
room.channel_id = ch.channel_id
|
||||
|
||||
@@ -99,7 +99,7 @@ async def join_channel(
|
||||
if channel.isdigit():
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_id == int(channel)))).first()
|
||||
else:
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.name == channel))).first()
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_name == channel))).first()
|
||||
|
||||
if db_channel is None:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
@@ -126,7 +126,7 @@ async def leave_channel(
|
||||
if channel.isdigit():
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_id == int(channel)))).first()
|
||||
else:
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.name == channel))).first()
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_name == channel))).first()
|
||||
|
||||
if db_channel is None:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
@@ -182,14 +182,14 @@ async def get_channel(
|
||||
if channel.isdigit():
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_id == int(channel)))).first()
|
||||
else:
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.name == channel))).first()
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_name == channel))).first()
|
||||
|
||||
if db_channel is None:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
|
||||
# 立即提取需要的属性
|
||||
channel_type = db_channel.type
|
||||
channel_name = db_channel.name
|
||||
channel_name = db_channel.channel_name
|
||||
|
||||
users = []
|
||||
if channel_type == ChannelType.PM:
|
||||
@@ -270,7 +270,7 @@ async def create_channel(
|
||||
channel_name = f"pm_{current_user.id}_{req.target_id}"
|
||||
else:
|
||||
channel_name = req.channel.name if req.channel else "Unnamed Channel"
|
||||
result = await session.exec(select(ChatChannel).where(ChatChannel.name == channel_name))
|
||||
result = await session.exec(select(ChatChannel).where(ChatChannel.channel_name == channel_name))
|
||||
channel = result.first()
|
||||
|
||||
if channel is None:
|
||||
|
||||
@@ -87,7 +87,7 @@ async def send_message(
|
||||
if channel.isdigit():
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_id == int(channel)))).first()
|
||||
else:
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.name == channel))).first()
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_name == channel))).first()
|
||||
|
||||
if db_channel is None:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
@@ -95,7 +95,7 @@ async def send_message(
|
||||
# 立即提取所有需要的属性,避免后续延迟加载
|
||||
channel_id = db_channel.channel_id
|
||||
channel_type = db_channel.type
|
||||
channel_name = db_channel.name
|
||||
channel_name = db_channel.channel_name
|
||||
user_id = current_user.id
|
||||
|
||||
# 对于多人游戏房间,在发送消息前进行Redis键检查
|
||||
@@ -169,7 +169,7 @@ async def get_message(
|
||||
if channel.isdigit():
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_id == int(channel)))).first()
|
||||
else:
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.name == channel))).first()
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_name == channel))).first()
|
||||
|
||||
if db_channel is None:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
@@ -231,7 +231,7 @@ async def mark_as_read(
|
||||
if channel.isdigit():
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_id == int(channel)))).first()
|
||||
else:
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.name == channel))).first()
|
||||
db_channel = (await session.exec(select(ChatChannel).where(ChatChannel.channel_name == channel))).first()
|
||||
|
||||
if db_channel is None:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
|
||||
@@ -35,6 +35,7 @@ from app.database.score import (
|
||||
)
|
||||
from app.dependencies.api_version import APIVersion
|
||||
from app.dependencies.cache import UserCacheService
|
||||
from app.dependencies.client_verification import ClientVerificationService
|
||||
from app.dependencies.database import Database, Redis, get_redis, with_db
|
||||
from app.dependencies.fetcher import Fetcher, get_fetcher
|
||||
from app.dependencies.storage import StorageService
|
||||
@@ -123,14 +124,11 @@ async def _process_user(score_id: int, user_id: int, redis: Redis, fetcher: Fetc
|
||||
async def submit_score(
|
||||
background_task: BackgroundTasks,
|
||||
info: SoloScoreSubmissionInfo,
|
||||
beatmap: int,
|
||||
token: int,
|
||||
current_user: User,
|
||||
db: AsyncSession,
|
||||
redis: Redis,
|
||||
fetcher: Fetcher,
|
||||
item_id: int | None = None,
|
||||
room_id: int | None = None,
|
||||
):
|
||||
# 立即获取用户ID,避免后续的懒加载问题
|
||||
user_id = current_user.id
|
||||
@@ -154,6 +152,7 @@ async def submit_score(
|
||||
if not score:
|
||||
raise HTTPException(status_code=404, detail="Score not found")
|
||||
else:
|
||||
beatmap = score_token.beatmap_id
|
||||
try:
|
||||
cache_service = get_beatmap_cache_service(redis, fetcher)
|
||||
await cache_service.smart_preload_for_score(beatmap)
|
||||
@@ -172,8 +171,6 @@ async def submit_score(
|
||||
score_token,
|
||||
info,
|
||||
db,
|
||||
item_id,
|
||||
room_id,
|
||||
)
|
||||
await db.refresh(score_token)
|
||||
score_id = score.id
|
||||
@@ -419,6 +416,8 @@ async def get_user_all_beatmap_scores(
|
||||
async def create_solo_score(
|
||||
background_task: BackgroundTasks,
|
||||
db: Database,
|
||||
fetcher: Fetcher,
|
||||
verification_service: ClientVerificationService,
|
||||
beatmap_id: Annotated[int, Path(description="谱面 ID")],
|
||||
beatmap_hash: Annotated[str, Form(description="谱面文件哈希")],
|
||||
ruleset_id: Annotated[int, Form(..., description="ruleset 数字 ID (0-3)")],
|
||||
@@ -434,6 +433,21 @@ async def create_solo_score(
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid ruleset ID")
|
||||
|
||||
if not (
|
||||
client_version := await verification_service.validate_client_version(
|
||||
version_hash,
|
||||
)
|
||||
):
|
||||
logger.info(
|
||||
f"Client version check failed for user {current_user.id} on beatmap {beatmap_id} "
|
||||
f"(version hash: {version_hash})"
|
||||
)
|
||||
raise HTTPException(status_code=422, detail="invalid client hash")
|
||||
|
||||
beatmap = await Beatmap.get_or_fetch(db, fetcher, md5=beatmap_hash)
|
||||
if not beatmap or beatmap.id != beatmap_id:
|
||||
raise HTTPException(status_code=422, detail="invalid or missing beatmap_hash")
|
||||
|
||||
if not (result := gamemode.check_ruleset_version(ruleset_hash)):
|
||||
logger.info(
|
||||
f"Ruleset version check failed for user {current_user.id} on beatmap {beatmap_id} "
|
||||
@@ -454,6 +468,15 @@ async def create_solo_score(
|
||||
db.add(score_token)
|
||||
await db.commit()
|
||||
await db.refresh(score_token)
|
||||
logger.debug(
|
||||
"User {user_id} created solo score {score_token} for beatmap {beatmap_id} "
|
||||
"(mode: {mode}), using client {client_version}",
|
||||
user_id=user_id,
|
||||
score_token=score_token.id,
|
||||
beatmap_id=beatmap_id,
|
||||
mode=ruleset_id,
|
||||
client_version=str(client_version),
|
||||
)
|
||||
return ScoreTokenResp.from_db(score_token)
|
||||
|
||||
|
||||
@@ -474,7 +497,7 @@ async def submit_solo_score(
|
||||
redis: Redis,
|
||||
fetcher: Fetcher,
|
||||
):
|
||||
return await submit_score(background_task, info, beatmap_id, token, current_user, db, redis, fetcher)
|
||||
return await submit_score(background_task, info, token, current_user, db, redis, fetcher)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -489,8 +512,9 @@ async def create_playlist_score(
|
||||
background_task: BackgroundTasks,
|
||||
room_id: int,
|
||||
playlist_id: int,
|
||||
verification_service: ClientVerificationService,
|
||||
beatmap_id: Annotated[int, Form(description="谱面 ID")],
|
||||
beatmap_hash: Annotated[str, Form(description="游戏版本哈希")],
|
||||
beatmap_hash: Annotated[str, Form(description="谱面文件哈希")],
|
||||
ruleset_id: Annotated[int, Form(..., description="ruleset 数字 ID (0-3)")],
|
||||
current_user: ClientUser,
|
||||
version_hash: Annotated[str, Form(description="谱面版本哈希")] = "",
|
||||
@@ -501,6 +525,17 @@ async def create_playlist_score(
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid ruleset ID")
|
||||
|
||||
if not (
|
||||
client_version := await verification_service.validate_client_version(
|
||||
version_hash,
|
||||
)
|
||||
):
|
||||
logger.info(
|
||||
f"Client version check failed for user {current_user.id} on room {room_id}, playlist {playlist_id} "
|
||||
f"(version hash: {version_hash})"
|
||||
)
|
||||
raise HTTPException(status_code=422, detail="invalid client hash")
|
||||
|
||||
if not (result := gamemode.check_ruleset_version(ruleset_hash)):
|
||||
logger.info(
|
||||
f"Ruleset version check failed for user {current_user.id} on room {room_id}, playlist {playlist_id},"
|
||||
@@ -555,10 +590,22 @@ async def create_playlist_score(
|
||||
beatmap_id=beatmap_id,
|
||||
ruleset_id=GameMode.from_int(ruleset_id),
|
||||
playlist_item_id=playlist_id,
|
||||
room_id=room_id,
|
||||
)
|
||||
session.add(score_token)
|
||||
await session.commit()
|
||||
await session.refresh(score_token)
|
||||
logger.debug(
|
||||
"User {user_id} created playlist score {score_token} for beatmap {beatmap_id} "
|
||||
"(mode: {mode}, room {room_id}, item {playlist_id}), using client {client_version}",
|
||||
user_id=user_id,
|
||||
score_token=score_token.id,
|
||||
beatmap_id=beatmap_id,
|
||||
mode=ruleset_id,
|
||||
room_id=room_id,
|
||||
playlist_id=playlist_id,
|
||||
client_version=str(client_version),
|
||||
)
|
||||
return ScoreTokenResp.from_db(score_token)
|
||||
|
||||
|
||||
@@ -595,14 +642,11 @@ async def submit_playlist_score(
|
||||
score_resp = await submit_score(
|
||||
background_task,
|
||||
info,
|
||||
item.beatmap_id,
|
||||
token,
|
||||
current_user,
|
||||
session,
|
||||
redis,
|
||||
fetcher,
|
||||
item.id,
|
||||
room_id,
|
||||
)
|
||||
await process_playlist_best_score(
|
||||
room_id,
|
||||
@@ -753,7 +797,13 @@ async def show_playlist_score(
|
||||
]
|
||||
if completed:
|
||||
includes.append("scores_around")
|
||||
resp = await ScoreModel.transform(score_record.score, includes=includes)
|
||||
resp = await ScoreModel.transform(
|
||||
score_record.score,
|
||||
includes=includes,
|
||||
playlist_id=playlist_id,
|
||||
room_id=room_id,
|
||||
is_playlist=is_playlist,
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
import sys
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from app.config import settings
|
||||
@@ -8,6 +9,7 @@ from app.database import (
|
||||
BeatmapModel,
|
||||
BeatmapPlaycounts,
|
||||
BeatmapsetModel,
|
||||
FavouriteBeatmapset,
|
||||
User,
|
||||
)
|
||||
from app.database.beatmap_playcounts import BeatmapPlaycountsModel
|
||||
@@ -30,7 +32,7 @@ from app.utils import api_doc, utcnow
|
||||
from .router import router
|
||||
|
||||
from fastapi import BackgroundTasks, HTTPException, Path, Query, Request, Security
|
||||
from sqlmodel import exists, false, select
|
||||
from sqlmodel import exists, select, tuple_
|
||||
from sqlmodel.sql.expression import col
|
||||
|
||||
|
||||
@@ -140,20 +142,39 @@ async def get_users(
|
||||
async def get_user_events(
|
||||
session: Database,
|
||||
user_id: Annotated[int, Path(description="用户 ID")],
|
||||
limit: Annotated[int | None, Query(description="限制返回的活动数量")] = None,
|
||||
limit: Annotated[int, Query(description="限制返回的活动数量")] = 50,
|
||||
offset: Annotated[int | None, Query(description="活动日志的偏移量")] = None,
|
||||
current_user: User | None = Security(get_optional_user, scopes=["public"]),
|
||||
):
|
||||
db_user = await session.get(User, user_id)
|
||||
if db_user is None or not await visible_to_current_user(db_user, current_user, session):
|
||||
raise HTTPException(404, "User Not found")
|
||||
if offset is None:
|
||||
offset = 0
|
||||
if limit > 100:
|
||||
limit = 100
|
||||
|
||||
if offset == 0:
|
||||
cursor = sys.maxsize
|
||||
else:
|
||||
cursor = (
|
||||
await session.exec(
|
||||
select(Event.id)
|
||||
.where(Event.user_id == db_user.id, Event.created_at >= utcnow() - timedelta(days=30))
|
||||
.order_by(col(Event.id).desc())
|
||||
.limit(1)
|
||||
.offset(offset - 1)
|
||||
)
|
||||
).first()
|
||||
if cursor is None:
|
||||
return []
|
||||
|
||||
events = (
|
||||
await session.exec(
|
||||
select(Event)
|
||||
.where(Event.user_id == db_user.id, Event.created_at >= utcnow() - timedelta(days=30))
|
||||
.order_by(col(Event.created_at).desc())
|
||||
.where(Event.user_id == db_user.id, Event.created_at >= utcnow() - timedelta(days=30), Event.id < cursor)
|
||||
.order_by(col(Event.id).desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
).all()
|
||||
return events
|
||||
@@ -418,7 +439,7 @@ async def get_user_beatmapsets(
|
||||
return cached_result
|
||||
|
||||
user = await session.get(User, user_id)
|
||||
if not user or user.id == BANCHOBOT_ID:
|
||||
if not user or user.id == BANCHOBOT_ID or not await visible_to_current_user(user, current_user, session):
|
||||
raise HTTPException(404, detail="User not found")
|
||||
|
||||
if type in {
|
||||
@@ -433,10 +454,28 @@ async def get_user_beatmapsets(
|
||||
resp = []
|
||||
|
||||
elif type == BeatmapsetType.FAVOURITE:
|
||||
user = await session.get(User, user_id)
|
||||
if user is None or not await visible_to_current_user(user, current_user, session):
|
||||
raise HTTPException(404, detail="User not found")
|
||||
favourites = await user.awaitable_attrs.favourite_beatmapsets
|
||||
if offset == 0:
|
||||
cursor = sys.maxsize
|
||||
else:
|
||||
cursor = (
|
||||
await session.exec(
|
||||
select(FavouriteBeatmapset.id)
|
||||
.where(FavouriteBeatmapset.user_id == user_id)
|
||||
.order_by(col(FavouriteBeatmapset.id).desc())
|
||||
.limit(1)
|
||||
.offset(offset - 1)
|
||||
)
|
||||
).first()
|
||||
if cursor is None:
|
||||
return []
|
||||
favourites = (
|
||||
await session.exec(
|
||||
select(FavouriteBeatmapset)
|
||||
.where(FavouriteBeatmapset.user_id == user_id, FavouriteBeatmapset.id < cursor)
|
||||
.order_by(col(FavouriteBeatmapset.id).desc())
|
||||
.limit(limit)
|
||||
)
|
||||
).all()
|
||||
resp = [
|
||||
await BeatmapsetModel.transform(
|
||||
favourite.beatmapset, session=session, user=user, includes=beatmapset_includes
|
||||
@@ -445,16 +484,29 @@ async def get_user_beatmapsets(
|
||||
]
|
||||
|
||||
elif type == BeatmapsetType.MOST_PLAYED:
|
||||
user = await session.get(User, user_id)
|
||||
if user is None or not await visible_to_current_user(user, current_user, session):
|
||||
raise HTTPException(404, detail="User not found")
|
||||
|
||||
if offset == 0:
|
||||
cursor = sys.maxsize, sys.maxsize
|
||||
else:
|
||||
cursor = (
|
||||
await session.exec(
|
||||
select(BeatmapPlaycounts.playcount, BeatmapPlaycounts.id)
|
||||
.where(BeatmapPlaycounts.user_id == user_id)
|
||||
.order_by(col(BeatmapPlaycounts.playcount).desc(), col(BeatmapPlaycounts.id).desc())
|
||||
.limit(1)
|
||||
.offset(offset - 1)
|
||||
)
|
||||
).first()
|
||||
if cursor is None:
|
||||
return []
|
||||
cursor_pc, cursor_id = cursor
|
||||
most_played = await session.exec(
|
||||
select(BeatmapPlaycounts)
|
||||
.where(BeatmapPlaycounts.user_id == user_id)
|
||||
.order_by(col(BeatmapPlaycounts.playcount).desc())
|
||||
.where(
|
||||
BeatmapPlaycounts.user_id == user_id,
|
||||
tuple_(BeatmapPlaycounts.playcount, BeatmapPlaycounts.id) < tuple_(cursor_pc, cursor_id),
|
||||
)
|
||||
.order_by(col(BeatmapPlaycounts.playcount).desc(), col(BeatmapPlaycounts.id).desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
resp = [
|
||||
await BeatmapPlaycountsModel.transform(most_played_beatmap, user=user, includes=beatmapset_includes)
|
||||
@@ -487,7 +539,6 @@ async def get_user_beatmapsets(
|
||||
)
|
||||
@asset_proxy_response
|
||||
async def get_user_scores(
|
||||
request: Request,
|
||||
session: Database,
|
||||
api_version: APIVersion,
|
||||
background_task: BackgroundTasks,
|
||||
@@ -520,32 +571,93 @@ async def get_user_scores(
|
||||
raise HTTPException(404, detail="User not found")
|
||||
|
||||
gamemode = mode or db_user.playmode
|
||||
order_by = None
|
||||
where_clause = (col(Score.user_id) == db_user.id) & (col(Score.gamemode) == gamemode)
|
||||
includes = Score.USER_PROFILE_INCLUDES.copy()
|
||||
if not include_fails:
|
||||
where_clause &= col(Score.passed).is_(True)
|
||||
|
||||
scores = []
|
||||
if type == "pinned":
|
||||
where_clause &= Score.pinned_order > 0
|
||||
order_by = col(Score.pinned_order).asc()
|
||||
if offset == 0:
|
||||
cursor = 0, sys.maxsize
|
||||
else:
|
||||
cursor = (
|
||||
await session.exec(
|
||||
select(Score.pinned_order, Score.id)
|
||||
.where(where_clause)
|
||||
.order_by(col(Score.pinned_order).asc(), col(Score.id).desc())
|
||||
.limit(1)
|
||||
.offset(offset - 1)
|
||||
)
|
||||
).first()
|
||||
if cursor:
|
||||
cursor_pinned, cursor_id = cursor
|
||||
where_clause &= (col(Score.pinned_order) > cursor_pinned) | (
|
||||
(col(Score.pinned_order) == cursor_pinned) & (col(Score.id) < cursor_id)
|
||||
)
|
||||
scores = (
|
||||
await session.exec(
|
||||
select(Score)
|
||||
.where(where_clause)
|
||||
.order_by(col(Score.pinned_order).asc(), col(Score.id).desc())
|
||||
.limit(limit)
|
||||
)
|
||||
).all()
|
||||
|
||||
elif type == "best":
|
||||
where_clause &= exists().where(col(BestScore.score_id) == Score.id)
|
||||
order_by = col(Score.pp).desc()
|
||||
includes.append("weight")
|
||||
|
||||
if offset == 0:
|
||||
cursor = sys.maxsize, sys.maxsize
|
||||
else:
|
||||
cursor = (
|
||||
await session.exec(
|
||||
select(Score.pp, Score.id)
|
||||
.where(where_clause)
|
||||
.order_by(col(Score.pp).desc(), col(Score.id).desc())
|
||||
.limit(1)
|
||||
.offset(offset - 1)
|
||||
)
|
||||
).first()
|
||||
if cursor:
|
||||
cursor_pp, cursor_id = cursor
|
||||
where_clause &= tuple_(col(Score.pp), col(Score.id)) < tuple_(cursor_pp, cursor_id)
|
||||
scores = (
|
||||
await session.exec(
|
||||
select(Score).where(where_clause).order_by(col(Score.pp).desc(), col(Score.id).desc()).limit(limit)
|
||||
)
|
||||
).all()
|
||||
|
||||
elif type == "recent":
|
||||
where_clause &= Score.ended_at > utcnow() - timedelta(hours=24)
|
||||
order_by = col(Score.ended_at).desc()
|
||||
elif type == "firsts":
|
||||
where_clause &= false()
|
||||
|
||||
if type != "firsts":
|
||||
scores = (
|
||||
await session.exec(select(Score).where(where_clause).order_by(order_by).limit(limit).offset(offset))
|
||||
).all()
|
||||
if not scores:
|
||||
return []
|
||||
if offset == 0:
|
||||
cursor = datetime.max, sys.maxsize
|
||||
else:
|
||||
best_scores = await get_user_first_scores(session, db_user.id, gamemode, limit)
|
||||
cursor = (
|
||||
await session.exec(
|
||||
select(Score.ended_at, Score.id)
|
||||
.where(where_clause)
|
||||
.order_by(col(Score.ended_at).desc(), col(Score.id).desc())
|
||||
.limit(1)
|
||||
.offset(offset - 1)
|
||||
)
|
||||
).first()
|
||||
if cursor:
|
||||
cursor_date, cursor_id = cursor
|
||||
where_clause &= tuple_(col(Score.ended_at), col(Score.id)) < tuple_(cursor_date, cursor_id)
|
||||
scores = (
|
||||
await session.exec(
|
||||
select(Score)
|
||||
.where(where_clause)
|
||||
.order_by(col(Score.ended_at).desc(), col(Score.id).desc())
|
||||
.limit(limit)
|
||||
)
|
||||
).all()
|
||||
|
||||
elif type == "firsts":
|
||||
best_scores = await get_user_first_scores(session, db_user.id, gamemode, limit, offset)
|
||||
scores = [best_score.score for best_score in best_scores]
|
||||
|
||||
score_responses = [
|
||||
|
||||
131
app/service/client_verification_service.py
Normal file
131
app/service/client_verification_service.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""Service for verifying client versions against known valid versions."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from app.config import settings
|
||||
from app.log import logger
|
||||
from app.models.version import VersionCheckResult, VersionList
|
||||
from app.path import CONFIG_DIR
|
||||
|
||||
import aiofiles
|
||||
import httpx
|
||||
from httpx import AsyncClient
|
||||
|
||||
HASHES_DIR = CONFIG_DIR / "client_versions.json"
|
||||
|
||||
|
||||
class ClientVerificationService:
|
||||
"""A service to verify client versions against known valid versions.
|
||||
|
||||
Attributes:
|
||||
version_lists (list[VersionList]): A list of version lists fetched from remote sources.
|
||||
|
||||
Methods:
|
||||
init(): Initialize the service by loading version data from disk and refreshing from remote.
|
||||
refresh(): Fetch the latest version lists from configured URLs and store them locally.
|
||||
load_from_disk(): Load version lists from the local JSON file.
|
||||
validate_client_version(client_version: str) -> VersionCheckResult: Validate a given client version against the known versions.
|
||||
""" # noqa: E501
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.original_version_lists: dict[str, list[VersionList]] = {}
|
||||
self.versions: dict[str, tuple[str, str, str]] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def init(self) -> None:
|
||||
"""Initialize the service by loading version data from disk and refreshing from remote."""
|
||||
await self.load_from_disk(first_load=True)
|
||||
await self.refresh()
|
||||
await self.load_from_disk()
|
||||
|
||||
async def refresh(self) -> None:
|
||||
"""Fetch the latest version lists from configured URLs and store them locally."""
|
||||
lists: dict[str, list[VersionList]] = self.original_version_lists.copy()
|
||||
async with AsyncClient() as client:
|
||||
for url in settings.client_version_urls:
|
||||
try:
|
||||
resp = await client.get(url, timeout=10)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if len(data) == 0:
|
||||
logger.warning(f"Client version list from {url} is empty")
|
||||
continue
|
||||
lists[url] = data
|
||||
logger.info(f"Fetched client version list from {url}, total {len(data)} clients")
|
||||
except httpx.TimeoutException:
|
||||
logger.warning(f"Timeout when fetching client version list from {url}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch client version list from {url}: {e}")
|
||||
async with aiofiles.open(HASHES_DIR, "wb") as f:
|
||||
await f.write(json.dumps(lists).encode("utf-8"))
|
||||
|
||||
async def load_from_disk(self, first_load: bool = False) -> None:
|
||||
"""Load version lists from the local JSON file."""
|
||||
async with self._lock:
|
||||
self.versions.clear()
|
||||
try:
|
||||
if not HASHES_DIR.is_file() and not first_load:
|
||||
logger.warning("Client version list file does not exist on disk")
|
||||
return
|
||||
async with aiofiles.open(HASHES_DIR, "rb") as f:
|
||||
content = await f.read()
|
||||
self.original_version_lists = json.loads(content.decode("utf-8"))
|
||||
for version_list_group in self.original_version_lists.values():
|
||||
for version_list in version_list_group:
|
||||
for version_info in version_list["versions"]:
|
||||
for client_hash, os_name in version_info["hashes"].items():
|
||||
self.versions[client_hash] = (
|
||||
version_list["name"],
|
||||
version_info["version"],
|
||||
os_name,
|
||||
)
|
||||
if not first_load:
|
||||
if len(self.versions) == 0:
|
||||
logger.warning("Client version list is empty after loading from disk")
|
||||
else:
|
||||
logger.info(
|
||||
"Loaded client version list from disk, "
|
||||
f"total {len(self.versions)} clients, {len(self.versions)} versions"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to load client version list from disk: {e}")
|
||||
|
||||
async def validate_client_version(self, client_version: str) -> VersionCheckResult:
|
||||
"""Validate a given client version against the known versions.
|
||||
|
||||
Args:
|
||||
client_version (str): The client version string to validate.
|
||||
|
||||
Returns:
|
||||
VersionCheckResult: The result of the validation.
|
||||
"""
|
||||
if not settings.check_client_version:
|
||||
return VersionCheckResult(is_valid=True)
|
||||
async with self._lock:
|
||||
if client_version in self.versions:
|
||||
name, version, os_name = self.versions[client_version]
|
||||
return VersionCheckResult(is_valid=True, client_name=name, version=version, os=os_name)
|
||||
return VersionCheckResult(is_valid=False)
|
||||
|
||||
|
||||
_client_verification_service: ClientVerificationService | None = None
|
||||
|
||||
|
||||
def get_client_verification_service() -> ClientVerificationService:
|
||||
"""Get the singleton instance of ClientVerificationService.
|
||||
|
||||
Returns:
|
||||
ClientVerificationService: The singleton instance.
|
||||
"""
|
||||
global _client_verification_service
|
||||
if _client_verification_service is None:
|
||||
_client_verification_service = ClientVerificationService()
|
||||
return _client_verification_service
|
||||
|
||||
|
||||
async def init_client_verification_service() -> None:
|
||||
"""Initialize the ClientVerificationService singleton."""
|
||||
service = get_client_verification_service()
|
||||
logger.info("Initializing ClientVerificationService...")
|
||||
await service.init()
|
||||
@@ -6,6 +6,7 @@ from . import (
|
||||
database_cleanup,
|
||||
recalculate_banned_beatmap,
|
||||
recalculate_failed_score,
|
||||
update_client_version,
|
||||
)
|
||||
from .cache import start_cache_tasks, stop_cache_tasks
|
||||
from .calculate_all_user_rank import calculate_user_rank
|
||||
|
||||
13
app/tasks/update_client_version.py
Normal file
13
app/tasks/update_client_version.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from app.config import settings
|
||||
from app.dependencies.scheduler import get_scheduler
|
||||
from app.log import logger
|
||||
from app.service.client_verification_service import get_client_verification_service
|
||||
|
||||
if settings.check_client_version:
|
||||
|
||||
@get_scheduler().scheduled_job("interval", id="update_client_version", hours=2)
|
||||
async def update_client_version():
|
||||
logger.info("Updating client version lists...")
|
||||
client_verification_service = get_client_verification_service()
|
||||
await client_verification_service.refresh()
|
||||
await client_verification_service.load_from_disk()
|
||||
6
main.py
6
main.py
@@ -33,6 +33,7 @@ from app.router.redirect import redirect_router
|
||||
from app.router.v1 import api_v1_public_router
|
||||
from app.service.beatmap_download_service import download_service
|
||||
from app.service.beatmapset_update_service import init_beatmapset_update_service
|
||||
from app.service.client_verification_service import init_client_verification_service
|
||||
from app.service.email_queue import start_email_processor, stop_email_processor
|
||||
from app.service.redis_message_system import redis_message_system
|
||||
from app.service.subscribers.user_cache import user_online_subscriber
|
||||
@@ -68,6 +69,9 @@ async def lifespan(app: FastAPI): # noqa: ARG001
|
||||
load_achievements()
|
||||
await init_calculator()
|
||||
|
||||
if settings.check_client_version:
|
||||
await init_client_verification_service()
|
||||
|
||||
# init rate limiter
|
||||
await FastAPILimiter.init(redis_rate_limit_client)
|
||||
|
||||
@@ -158,7 +162,7 @@ v1 API 采用 API Key 鉴权,将 API Key 放入 Query `k` 中。
|
||||
|
||||
# 检查 New Relic 配置文件是否存在,如果存在则初始化 New Relic
|
||||
newrelic_config_path = Path("newrelic.ini")
|
||||
if newrelic_config_path.exists():
|
||||
if newrelic_config_path.is_file():
|
||||
try:
|
||||
import newrelic.agent
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""score_token: add room_id
|
||||
|
||||
Revision ID: 96c4f4b3f0ab
|
||||
Revises: d430db6fc051
|
||||
Create Date: 2025-12-20 11:39:02.640676
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "96c4f4b3f0ab"
|
||||
down_revision: str | Sequence[str] | None = "d430db6fc051"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("score_tokens", sa.Column("room_id", sa.Integer(), nullable=True))
|
||||
op.create_index("idx_playlist_room", "score_tokens", ["playlist_item_id", "room_id"], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index("idx_playlist_room", table_name="score_tokens")
|
||||
op.drop_column("score_tokens", "room_id")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,51 @@
|
||||
"""add union index for score
|
||||
|
||||
Revision ID: c5472f592d13
|
||||
Revises: 96c4f4b3f0ab
|
||||
Create Date: 2026-01-03 07:10:11.050661
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "c5472f592d13"
|
||||
down_revision: str | Sequence[str] | None = "96c4f4b3f0ab"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index("idx_score_user_mode_date", "scores", ["user_id", "gamemode", "ended_at", "id"], unique=False)
|
||||
op.create_index("idx_score_user_mode_pinned", "scores", ["user_id", "gamemode", "pinned_order", "id"], unique=False)
|
||||
op.create_index("idx_score_user_mode_pp", "scores", ["user_id", "gamemode", "pp", "id"], unique=False)
|
||||
op.create_index("idx_beatmap_playcounts_playcount_id", "beatmap_playcounts", ["playcount", "id"], unique=False)
|
||||
op.create_index(
|
||||
"ix_total_score_best_scores_user_mode_score",
|
||||
"total_score_best_scores",
|
||||
["user_id", "gamemode", "score_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_total_score_best_scores_beatmap_mode_score",
|
||||
"total_score_best_scores",
|
||||
["beatmap_id", "gamemode", "total_score"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index("idx_score_user_mode_pp", table_name="scores")
|
||||
op.drop_index("idx_score_user_mode_pinned", table_name="scores")
|
||||
op.drop_index("idx_score_user_mode_date", table_name="scores")
|
||||
op.drop_index("idx_beatmap_playcounts_playcount_id", table_name="beatmap_playcounts")
|
||||
op.drop_index("ix_total_score_best_scores_beatmap_mode_score", table_name="total_score_best_scores")
|
||||
op.drop_index("ix_total_score_best_scores_user_mode_score", table_name="total_score_best_scores")
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,40 +1,50 @@
|
||||
{
|
||||
"Sentakki": {
|
||||
"latest-version": "2025.1108.0",
|
||||
"latest-version": "2026.117.1",
|
||||
"versions": {
|
||||
"2025.1012.1": "66e02af2097f446246b146641295573a",
|
||||
"2025.1026.1": "f3d5757ed7f55b95e4582a033f9dd04d",
|
||||
"2025.1108.0": "c4b367c7ea8468e991f566b296673d78"
|
||||
"2025.1108.0": "c4b367c7ea8468e991f566b296673d78",
|
||||
"2025.1228.0": "ccb7103b857bd6618a4af1ee0d067c31",
|
||||
"2026.117.1": "4626f932db2294b028aac90b58e1e074"
|
||||
}
|
||||
},
|
||||
"tau": {
|
||||
"latest-version": "2025.1108.0",
|
||||
"latest-version": "2026.117.1",
|
||||
"versions": {
|
||||
"2025.1012.1": "3a2dd168c2e520a3620a5dfd7b3c0b73",
|
||||
"2025.1026.1": "470b456e4767719a49a1e0a349006179",
|
||||
"2025.1101.1": "0895877497e0c77743e7487760434473",
|
||||
"2025.1108.0": "b2ec1228096e2e36206559c3b87a4f16"
|
||||
"2025.1108.0": "b2ec1228096e2e36206559c3b87a4f16",
|
||||
"2025.1228.0": "9c3d0958480ec3f7640ec400a518c7ed",
|
||||
"2026.117.1": "b462d3901ef7dde05f675ccff2ce5c05"
|
||||
}
|
||||
},
|
||||
"rush": {
|
||||
"latest-version": "2025.1108.0",
|
||||
"latest-version": "2026.117.1",
|
||||
"versions": {
|
||||
"2025.1026.1": "df0c211c8c40f42feb119a3a11549a6f",
|
||||
"2025.1108.0": "96804cbeafe4729f778d8bbbd228b516"
|
||||
"2025.1108.0": "96804cbeafe4729f778d8bbbd228b516",
|
||||
"2025.1228.0": "586154c9759affd1e2787caf833f8c69",
|
||||
"2026.117.1": "5537a382139b71b6850024574ef78d2c"
|
||||
}
|
||||
},
|
||||
"hishigata": {
|
||||
"latest-version": "2025.1108.0",
|
||||
"latest-version": "2026.117.1",
|
||||
"versions": {
|
||||
"2025.1026.1": "af26c2946cd0b2258ac52f5cce91958c",
|
||||
"2025.1108.0": "880c1b05efcd97755a23cc1d646a5b96"
|
||||
"2025.1108.0": "880c1b05efcd97755a23cc1d646a5b96",
|
||||
"2025.1228.0": "59265f6e3e0f134d90545e70b6db80aa",
|
||||
"2026.117.1": "92fbc8b4c4ea8717f230ce5a1decabdc"
|
||||
}
|
||||
},
|
||||
"soyokaze": {
|
||||
"latest-version": "2025.1108.0",
|
||||
"latest-version": "2026.117.1",
|
||||
"versions": {
|
||||
"2025.1026.1": "fea5c97b8b436305ba98ef8b39b133b6",
|
||||
"2025.1108.0": "a531dd4bc50f6137f187839eb6db9482"
|
||||
"2025.1108.0": "a531dd4bc50f6137f187839eb6db9482",
|
||||
"2025.1228.0": "8ad95cd074a231cab49d00d24df6955c",
|
||||
"2026.117.1": "2dd43d9c428f1feb72abf19a279d9b78"
|
||||
}
|
||||
}
|
||||
}
|
||||
148
uv.lock
generated
148
uv.lock
generated
@@ -195,14 +195,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "apscheduler"
|
||||
version = "3.11.1"
|
||||
version = "3.11.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzlocal" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/81/192db4f8471de5bc1f0d098783decffb1e6e69c4f8b4bc6711094691950b/apscheduler-3.11.1.tar.gz", hash = "sha256:0db77af6400c84d1747fe98a04b8b58f0080c77d11d338c4f507a9752880f221", size = 108044, upload-time = "2025-10-31T18:55:42.819Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/12/3e4389e5920b4c1763390c6d371162f3784f86f85cd6d6c1bfe68eef14e2/apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41", size = 108683, upload-time = "2025-12-22T00:39:34.884Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/9f/d3c76f76c73fcc959d28e9def45b8b1cc3d7722660c5003b19c1022fd7f4/apscheduler-3.11.1-py3-none-any.whl", hash = "sha256:6162cb5683cb09923654fa9bdd3130c4be4bfda6ad8990971c9597ecd52965d2", size = 64278, upload-time = "2025-10-31T18:55:41.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -562,7 +562,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "datamodel-code-generator"
|
||||
version = "0.41.0"
|
||||
version = "0.49.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "argcomplete" },
|
||||
@@ -575,9 +575,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/a0/f3a08b0e2068eae40a170c8485d0a68bfe54210df4b8a67ee4fe27bae681/datamodel_code_generator-0.41.0.tar.gz", hash = "sha256:f5ded2baab48488c09ce91c564e486dee9145029c53016f1befb732313d66411", size = 473910, upload-time = "2025-12-05T16:50:19.049Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/92/738536021ea6b0ec7cc080bb0fa149b52e908b662ea03aac9462c7b02d7c/datamodel_code_generator-0.49.0.tar.gz", hash = "sha256:eb4a95ee21c10f7675982f8b7d92344971b9a5e4c34c89b54652f8576b3a604b", size = 695361, upload-time = "2025-12-25T02:19:50.164Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/11/9cec012b7764443a7b03bf3725ea56481a3f6d45c363a15406d57d048cf9/datamodel_code_generator-0.41.0-py3-none-any.whl", hash = "sha256:02caf2372d298844d6080fe805770161cac9aa720f143f349cee6ca76b50e5bf", size = 158701, upload-time = "2025-12-05T16:50:17.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/e4/c59fd088850a8d35085d7ba90fe70098096da152600a96b9ba260740b76c/datamodel_code_generator-0.49.0-py3-none-any.whl", hash = "sha256:0c24d21b7a3103a89a324be48bbcac3fed1fd2f22804de8589e589f2c93d9dc0", size = 214650, upload-time = "2025-12-25T02:19:48.255Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -625,7 +625,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.124.0"
|
||||
version = "0.128.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
@@ -633,9 +633,9 @@ dependencies = [
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/9c/11969bd3e3bc4aa3a711f83dd3720239d3565a934929c74fc32f6c9f3638/fastapi-0.124.0.tar.gz", hash = "sha256:260cd178ad75e6d259991f2fd9b0fee924b224850079df576a3ba604ce58f4e6", size = 357623, upload-time = "2025-12-06T13:11:35.692Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/29/9e1e82e16e9a1763d3b55bfbe9b2fa39d7175a1fd97685c482fa402e111d/fastapi-0.124.0-py3-none-any.whl", hash = "sha256:91596bdc6dde303c318f06e8d2bc75eafb341fc793a0c9c92c0bc1db1ac52480", size = 112505, upload-time = "2025-12-06T13:11:34.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1251,22 +1251,22 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "newrelic"
|
||||
version = "11.1.0"
|
||||
version = "11.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/81/9682239403ec4110a3d3cfe79fb78ad014909c25fd30f0c55a48c90ebe6b/newrelic-11.1.0.tar.gz", hash = "sha256:11b0c4f916f81c7ed0c2f4cd282215634d09882761ad0c8e9866f07c4b58e015", size = 1307829, upload-time = "2025-11-03T23:53:10.988Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/f5/efe719e766b7cccb8395bb768b3faa7c9313e390b30eb4950034c49d4cd6/newrelic-11.2.0.tar.gz", hash = "sha256:6dd9f303904220700ba8b25af2f622cd23a4b5071cc53b4309e90bf3dcdb7221", size = 1321580, upload-time = "2025-12-08T23:17:48.599Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/75/9569d66adf72b7e5e66ec4e79725896c31e60537df096562395b41ee9e44/newrelic-11.1.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6ec6c5f1767f306b484b13742464c98a5e1aacb54b399f6aca639a50656c587", size = 889879, upload-time = "2025-11-03T23:52:34.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7f/ec355728e9915acaf068d386f4997e1e6c602ea4f264e87b9304db3eaa3b/newrelic-11.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:384388783b78bb09afc594fa9f27338b6d322356a4fc1347e605eebad60a0928", size = 890524, upload-time = "2025-11-03T23:52:36.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/2e/02b042b0b8e7ae2e40c4bc63728d8f1a1e59658d04c6111537334d3c4c76/newrelic-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:569019b274f26f34e146fc52392fd4a8cc9750108242e0d5b6f2fbb09dd0b1c6", size = 888694, upload-time = "2025-11-03T23:52:38.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/59/e49144a9881b82954add6b875b541b53a30d2bb4ddc7b954496fc46bed1a/newrelic-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77c47efaba50c22461c85ee4d899028d56d97085ced660e84456bc45451805fa", size = 889273, upload-time = "2025-11-03T23:52:41.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/dc/9918cdd0e05013c955194fbf9338402bd9299e01c42f6407aa2d9fb7a3db/newrelic-11.1.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9479467720159f1e8032e8ec46ec8428424bd2ad1e595a112bd321b541ab2ac3", size = 889988, upload-time = "2025-11-03T23:52:43.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/1b/433dfc8c327e8d27c1b6dc9d10a663fa70934cb3ef4bc9b3890fc61653f9/newrelic-11.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726a7648db224fbac08fc781532864c01e05f5b8581212f8192acb7376ecb337", size = 890646, upload-time = "2025-11-03T23:52:46.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/5f/a0b4fd131da88c9288b213d5b27204ea3c23448f93eaeeb0d2977f2c0c84/newrelic-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e58d9859b7f978340987fc788b25498297f2537836a07544579ab146f891bd4b", size = 888810, upload-time = "2025-11-03T23:52:47.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/d1/d6c416771b05f5e0520074b3b91771b800818a31ae639cbd007cea8f03eb/newrelic-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:831a72e1693ad08e9baf98762ab5f01c264beb11935e3739d2152ae6fd4120c5", size = 889396, upload-time = "2025-11-03T23:52:49.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/66/fb523993831186220f6679b09a507b85a841043df13d51122eaefed7059e/newrelic-11.1.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be47fba30bd9486e1679a0a505c29a78c1f58c2e20ccdf1d38217e6292be9ff9", size = 888992, upload-time = "2025-11-03T23:52:51.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/ff/055f5e2fc03635c26c4120dc151b7109ecc4459e454507e64f62394f7aa4/newrelic-11.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91d1d4cbc79b76c4258ce0c38a14c649f270728c9d02e1a128612a0fdb9ac26e", size = 890430, upload-time = "2025-11-03T23:52:52.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/cc/fa2df87a2b64458e08c5763b5b5dfca633d0faa8bd282f43098824d72dba/newrelic-11.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:569192559dd912023e7b0e77b295a42895ebd402596730976441a717ef8d414e", size = 888599, upload-time = "2025-11-03T23:52:54.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/15/7b8a257bc7816a6f68762db4633e334b80a11fec4dc93bff1247734c11fd/newrelic-11.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9aeafc4c95ed513c5a3ae6b1ccf8fe5f6453f04125682b3b5bb2f59065278e36", size = 888501, upload-time = "2025-11-03T23:52:56.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/ee/6a6107c0d81977c3e86f7406b629d445613c739052e8dab571f649c49ba6/newrelic-11.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ad8110d9622b21db6103640a5754d2c8b7d03eba5fe7ee9abbc5c5637160e08", size = 896908, upload-time = "2025-12-08T23:17:12.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/87/a330367eb11c3503d4b4b4cb9f48b3a5145941accf896734c3bfb2b42ffc/newrelic-11.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092660d17d21b53ea12659f9ef56f8e44075eb980ee4a1b7340651295f89f016", size = 897554, upload-time = "2025-12-08T23:17:14.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/9c/af75018b9ce156b3cef046ba9b8df50ac6f09a03ab411eb083dc20c8346b/newrelic-11.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cded013ccad26decf430cc66401adbdcdff036389325dc3b3451723e097bd5e5", size = 895723, upload-time = "2025-12-08T23:17:16.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/5d/20eef6a2222dc243fd2cdeae82aa10df195ebe7cafd988f700370a349865/newrelic-11.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb1551edbad5faa58845b57e48c54468c5d3ce7aa193869c56cfe57b68c05267", size = 896302, upload-time = "2025-12-08T23:17:18.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/8f/4a982d8c2811cd79f61683f56f6dffbd5a3bab2069c836362627c17539b7/newrelic-11.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0a273a69456fc63bd2ceac8a16c44cce297bc78b37e73aa44ac58eeea0c6c1e6", size = 897017, upload-time = "2025-12-08T23:17:20.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/1f/454ca513f4cc7e01e1d0a11150bcc91db0d98b0048941d9f1fb2a016a290/newrelic-11.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:486e012dd4bec702df218dba2c77c14a01c4cfa03bd67a8993a002f088c51c82", size = 897675, upload-time = "2025-12-08T23:17:22.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f9/d98391da6ca75011356118b2da70053ea82edd62fe85f4422c2b2e13b2c9/newrelic-11.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:77fd587c18438546ab62b13a6f602ec95d92bf15caa95f27b0f368453f99c8e1", size = 895838, upload-time = "2025-12-08T23:17:24.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/01/5b9a3d3c9a7ce8a682e8bf0f95f31ed72264368d0bde9669620761ab773a/newrelic-11.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:90010f1e1355b5225803470f5fab01910f865c66bbbb4e896493c0b508592838", size = 896425, upload-time = "2025-12-08T23:17:26.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5f/ccb373ee01647a7962d27153002d16ce4ebe37f5f4cdedbf1e3dd584ec82/newrelic-11.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f34d750cd1f87a7d31172eaadb61418e729e3fe739b3a99b3720c0cc8cfacb85", size = 896021, upload-time = "2025-12-08T23:17:28.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/80/0723fa8fcd5cb4ddc2053f9838216db9c89d2b86097326cd15e8e93792a0/newrelic-11.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:745fdcc449c8d3f6041b4a56e9c03693710e6620b35790a0cb0f9641e248a2b2", size = 897460, upload-time = "2025-12-08T23:17:29.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/5d/1a548981ecf5b06bdee8bb484f6d7665df4ae320deeacbe8ee0d932f607c/newrelic-11.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2bc8f821c94e1f4beb899a2421d062a9739da519e58cd5429fc70a88a8c74bf6", size = 895628, upload-time = "2025-12-08T23:17:31.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/7c/a90b2f527e19236ff07e0dd7102badc688840b968ff621e225032ec1bf25/newrelic-11.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2a024bef9c0bdf72556f6f35e9fd9aaf1e9b8d7640cf2fa2c105b7ad3deccb9c", size = 895531, upload-time = "2025-12-08T23:17:32.85Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1395,7 +1395,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.5.0"
|
||||
version = "4.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
@@ -1404,9 +1404,9 @@ dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1633,11 +1633,11 @@ cryptography = [
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.20"
|
||||
version = "0.0.21"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1837,28 +1837,28 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.8"
|
||||
version = "0.14.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1920,44 +1920,50 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.44"
|
||||
version = "2.0.45"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/c4/59c7c9b068e6813c898b771204aad36683c96318ed12d4233e1b18762164/sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250", size = 2139675, upload-time = "2025-10-10T16:03:31.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/ae/eeb0920537a6f9c5a3708e4a5fc55af25900216bdb4847ec29cfddf3bf3a/sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29", size = 2127726, upload-time = "2025-10-10T16:03:35.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/d5/2ebbabe0379418eda8041c06b0b551f213576bfe4c2f09d77c06c07c8cc5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44", size = 3327603, upload-time = "2025-10-10T15:35:28.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/e5/5aa65852dadc24b7d8ae75b7efb8d19303ed6ac93482e60c44a585930ea5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1", size = 3337842, upload-time = "2025-10-10T15:43:45.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/92/648f1afd3f20b71e880ca797a960f638d39d243e233a7082c93093c22378/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7", size = 3264558, upload-time = "2025-10-10T15:35:29.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/cf/e27d7ee61a10f74b17740918e23cbc5bc62011b48282170dc4c66da8ec0f/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d", size = 3301570, upload-time = "2025-10-10T15:43:48.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/3d/3116a9a7b63e780fb402799b6da227435be878b6846b192f076d2f838654/sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4", size = 2103447, upload-time = "2025-10-10T15:03:21.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/83/24690e9dfc241e6ab062df82cc0df7f4231c79ba98b273fa496fb3dd78ed/sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e", size = 2130912, upload-time = "2025-10-10T15:03:24.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlmodel"
|
||||
version = "0.0.27"
|
||||
version = "0.0.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "sqlalchemy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/5a/693d90866233e837d182da76082a6d4c2303f54d3aaaa5c78e1238c5d863/sqlmodel-0.0.27.tar.gz", hash = "sha256:ad1227f2014a03905aef32e21428640848ac09ff793047744a73dfdd077ff620", size = 118053, upload-time = "2025-10-08T16:39:11.938Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4a/bc/162873c9b2466bb6bda1531b6023b5d8ecda4daffaaae54e6fec53e2c0a7/sqlmodel-0.0.30.tar.gz", hash = "sha256:076076976aac683bae3e10c791d9e2ec5fead57ab25f7e476dea36cdfe9505e1", size = 97581, upload-time = "2025-12-26T11:45:55.002Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/92/c35e036151fe53822893979f8a13e6f235ae8191f4164a79ae60a95d66aa/sqlmodel-0.0.27-py3-none-any.whl", hash = "sha256:667fe10aa8ff5438134668228dc7d7a08306f4c5c4c7e6ad3ad68defa0e7aa49", size = 29131, upload-time = "2025-10-08T16:39:10.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/f3/247bad31f13066a8186f2d4d1a43c75a441ed9f889d629d69d9820116dfc/sqlmodel-0.0.30-py3-none-any.whl", hash = "sha256:1b7f992170ff3145d98a72d41271335c8bb51d579c7254a1626f82bde4ec64ed", size = 29362, upload-time = "2025-12-26T11:45:53.298Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user