feat(calculator): support generate PerformanceAttributes & DifficultyAttributes from JSON Schema (#59)
Prepare for custom rulesets. Schema Genetator: https://github.com/GooGuTeam/custom-rulesets/tree/main/CustomRulesetMetadataGenerator ```bash dotnet -- schemas path/to/rulesets -o schema.json ``` ```bash python scripts/generate_ruleset_attributes.py schema.json ```
This commit is contained in:
@@ -6,7 +6,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- "app/config.py"
|
- "app/config.py"
|
||||||
- ".github/scripts/generate_config_doc.py"
|
- "scripts/generate_config_doc.py"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
- name: Generate Markdown
|
- name: Generate Markdown
|
||||||
run: |
|
run: |
|
||||||
cd project
|
cd project
|
||||||
python ./.github/scripts/generate_config_doc.py ${{ github.sha }} > ../wiki/Configuration.md
|
python ./scripts/generate_config_doc.py ${{ github.sha }} > ../wiki/Configuration.md
|
||||||
|
|
||||||
- name: Commit and push to Wiki
|
- name: Commit and push to Wiki
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import abc
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from app.models.mods import APIMod
|
from app.models.mods import APIMod
|
||||||
from app.models.performance import BeatmapAttributes, PerformanceAttributes
|
from app.models.performance import DifficultyAttributes, PerformanceAttributes
|
||||||
from app.models.score import GameMode
|
from app.models.score import GameMode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -33,5 +33,5 @@ class PerformanceCalculator(abc.ABC):
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def calculate_difficulty(
|
async def calculate_difficulty(
|
||||||
self, beatmap_raw: str, mods: list[APIMod] | None = None, gamemode: GameMode | None = None
|
self, beatmap_raw: str, mods: list[APIMod] | None = None, gamemode: GameMode | None = None
|
||||||
) -> BeatmapAttributes:
|
) -> DifficultyAttributes:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from app.models.mods import APIMod
|
from app.models.mods import APIMod
|
||||||
from app.models.performance import (
|
from app.models.performance import (
|
||||||
DIFFICULTY_CLASS,
|
DifficultyAttributes,
|
||||||
PERFORMANCE_CLASS,
|
DifficultyAttributesUnion,
|
||||||
BeatmapAttributes,
|
|
||||||
PerformanceAttributes,
|
PerformanceAttributes,
|
||||||
|
PerformanceAttributesUnion,
|
||||||
)
|
)
|
||||||
from app.models.score import GameMode
|
from app.models.score import GameMode
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ from ._base import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from httpx import AsyncClient, HTTPError
|
from httpx import AsyncClient, HTTPError
|
||||||
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from app.database.score import Score
|
from app.database.score import Score
|
||||||
@@ -56,8 +57,7 @@ class PerformanceCalculator(BasePerformanceCalculator):
|
|||||||
)
|
)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
raise PerformanceError(f"Failed to calculate performance: {resp.text}")
|
raise PerformanceError(f"Failed to calculate performance: {resp.text}")
|
||||||
data = resp.json()
|
return TypeAdapter(PerformanceAttributesUnion).validate_json(resp.text)
|
||||||
return PERFORMANCE_CLASS.get(score.gamemode, PerformanceAttributes).model_validate(data)
|
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
raise PerformanceError(f"Failed to calculate performance: {e}") from e
|
raise PerformanceError(f"Failed to calculate performance: {e}") from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -65,7 +65,7 @@ class PerformanceCalculator(BasePerformanceCalculator):
|
|||||||
|
|
||||||
async def calculate_difficulty(
|
async def calculate_difficulty(
|
||||||
self, beatmap_raw: str, mods: list[APIMod] | None = None, gamemode: GameMode | None = None
|
self, beatmap_raw: str, mods: list[APIMod] | None = None, gamemode: GameMode | None = None
|
||||||
) -> BeatmapAttributes:
|
) -> DifficultyAttributes:
|
||||||
# https://github.com/GooGuTeam/osu-performance-server#post-difficulty
|
# https://github.com/GooGuTeam/osu-performance-server#post-difficulty
|
||||||
async with AsyncClient() as client:
|
async with AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
@@ -79,9 +79,7 @@ class PerformanceCalculator(BasePerformanceCalculator):
|
|||||||
)
|
)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
raise DifficultyError(f"Failed to calculate difficulty: {resp.text}")
|
raise DifficultyError(f"Failed to calculate difficulty: {resp.text}")
|
||||||
data = resp.json()
|
return TypeAdapter(DifficultyAttributesUnion).validate_json(resp.text)
|
||||||
ruleset_id = data.pop("ruleset", "osu")
|
|
||||||
return DIFFICULTY_CLASS.get(GameMode(ruleset_id), BeatmapAttributes).model_validate(data)
|
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
raise DifficultyError(f"Failed to calculate difficulty: {e}") from e
|
raise DifficultyError(f"Failed to calculate difficulty: {e}") from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ from typing import TYPE_CHECKING
|
|||||||
from app.calculator import clamp
|
from app.calculator import clamp
|
||||||
from app.models.mods import APIMod, parse_enum_to_str
|
from app.models.mods import APIMod, parse_enum_to_str
|
||||||
from app.models.performance import (
|
from app.models.performance import (
|
||||||
DIFFICULTY_CLASS,
|
DifficultyAttributes,
|
||||||
PERFORMANCE_CLASS,
|
|
||||||
BeatmapAttributes,
|
|
||||||
ManiaPerformanceAttributes,
|
ManiaPerformanceAttributes,
|
||||||
OsuBeatmapAttributes,
|
OsuDifficultyAttributes,
|
||||||
OsuPerformanceAttributes,
|
OsuPerformanceAttributes,
|
||||||
PerformanceAttributes,
|
PerformanceAttributes,
|
||||||
TaikoBeatmapAttributes,
|
TaikoDifficultyAttributes,
|
||||||
TaikoPerformanceAttributes,
|
TaikoPerformanceAttributes,
|
||||||
)
|
)
|
||||||
from app.models.score import GameMode
|
from app.models.score import GameMode
|
||||||
@@ -38,6 +36,16 @@ except ImportError:
|
|||||||
" gu: uv add git+https://github.com/GooGuTeam/gu-pp-py.git"
|
" gu: uv add git+https://github.com/GooGuTeam/gu-pp-py.git"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PERFORMANCE_CLASS = {
|
||||||
|
GameMode.OSU: OsuPerformanceAttributes,
|
||||||
|
GameMode.TAIKO: TaikoPerformanceAttributes,
|
||||||
|
GameMode.MANIA: ManiaPerformanceAttributes,
|
||||||
|
}
|
||||||
|
DIFFICULTY_CLASS = {
|
||||||
|
GameMode.OSU: OsuDifficultyAttributes,
|
||||||
|
GameMode.TAIKO: TaikoDifficultyAttributes,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PerformanceCalculator(BasePerformanceCalculator):
|
class PerformanceCalculator(BasePerformanceCalculator):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -75,6 +83,10 @@ class PerformanceCalculator(BasePerformanceCalculator):
|
|||||||
flashlight=attr.pp_flashlight or 0,
|
flashlight=attr.pp_flashlight or 0,
|
||||||
effective_miss_count=attr.effective_miss_count or 0,
|
effective_miss_count=attr.effective_miss_count or 0,
|
||||||
speed_deviation=attr.speed_deviation,
|
speed_deviation=attr.speed_deviation,
|
||||||
|
combo_based_estimated_miss_count=0,
|
||||||
|
score_based_estimated_miss_count=0,
|
||||||
|
aim_estimated_slider_breaks=0,
|
||||||
|
speed_estimated_slider_breaks=0,
|
||||||
)
|
)
|
||||||
elif attr_class is TaikoPerformanceAttributes:
|
elif attr_class is TaikoPerformanceAttributes:
|
||||||
return TaikoPerformanceAttributes(
|
return TaikoPerformanceAttributes(
|
||||||
@@ -120,11 +132,11 @@ class PerformanceCalculator(BasePerformanceCalculator):
|
|||||||
raise CalculateError(f"Unknown error: {e}") from e
|
raise CalculateError(f"Unknown error: {e}") from e
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _diff_attr_to_model(cls, diff: rosu.DifficultyAttributes, gamemode: GameMode) -> BeatmapAttributes:
|
def _diff_attr_to_model(cls, diff: rosu.DifficultyAttributes, gamemode: GameMode) -> DifficultyAttributes:
|
||||||
attr_class = DIFFICULTY_CLASS.get(gamemode, BeatmapAttributes)
|
attr_class = DIFFICULTY_CLASS.get(gamemode, DifficultyAttributes)
|
||||||
|
|
||||||
if attr_class is OsuBeatmapAttributes:
|
if attr_class is OsuDifficultyAttributes:
|
||||||
return OsuBeatmapAttributes(
|
return OsuDifficultyAttributes(
|
||||||
star_rating=diff.stars,
|
star_rating=diff.stars,
|
||||||
max_combo=diff.max_combo,
|
max_combo=diff.max_combo,
|
||||||
aim_difficulty=diff.aim or 0,
|
aim_difficulty=diff.aim or 0,
|
||||||
@@ -135,23 +147,29 @@ class PerformanceCalculator(BasePerformanceCalculator):
|
|||||||
aim_difficult_strain_count=diff.aim_difficult_strain_count or 0,
|
aim_difficult_strain_count=diff.aim_difficult_strain_count or 0,
|
||||||
speed_difficult_strain_count=diff.speed_difficult_strain_count or 0,
|
speed_difficult_strain_count=diff.speed_difficult_strain_count or 0,
|
||||||
flashlight_difficulty=diff.flashlight or 0,
|
flashlight_difficulty=diff.flashlight or 0,
|
||||||
|
aim_top_weighted_slider_factor=0,
|
||||||
|
speed_top_weighted_slider_factor=0,
|
||||||
|
nested_score_per_object=0,
|
||||||
|
legacy_score_base_multiplier=0,
|
||||||
|
maximum_legacy_combo_score=0,
|
||||||
)
|
)
|
||||||
elif attr_class is TaikoBeatmapAttributes:
|
elif attr_class is TaikoDifficultyAttributes:
|
||||||
return TaikoBeatmapAttributes(
|
return TaikoDifficultyAttributes(
|
||||||
star_rating=diff.stars,
|
star_rating=diff.stars,
|
||||||
max_combo=diff.max_combo,
|
max_combo=diff.max_combo,
|
||||||
rhythm_difficulty=diff.rhythm or 0,
|
rhythm_difficulty=diff.rhythm or 0,
|
||||||
mono_stamina_factor=diff.stamina or 0,
|
mono_stamina_factor=diff.stamina or 0,
|
||||||
|
consistency_factor=0,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return BeatmapAttributes(
|
return DifficultyAttributes(
|
||||||
star_rating=diff.stars,
|
star_rating=diff.stars,
|
||||||
max_combo=diff.max_combo,
|
max_combo=diff.max_combo,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def calculate_difficulty(
|
async def calculate_difficulty(
|
||||||
self, beatmap_raw: str, mods: list[APIMod] | None = None, gamemode: GameMode | None = None
|
self, beatmap_raw: str, mods: list[APIMod] | None = None, gamemode: GameMode | None = None
|
||||||
) -> BeatmapAttributes:
|
) -> DifficultyAttributes:
|
||||||
try:
|
try:
|
||||||
map = rosu.Beatmap(content=beatmap_raw)
|
map = rosu.Beatmap(content=beatmap_raw)
|
||||||
if gamemode is not None:
|
if gamemode is not None:
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ from app.database.beatmap_tags import BeatmapTagVote
|
|||||||
from app.database.failtime import FailTime, FailTimeResp
|
from app.database.failtime import FailTime, FailTimeResp
|
||||||
from app.models.beatmap import BeatmapRankStatus
|
from app.models.beatmap import BeatmapRankStatus
|
||||||
from app.models.mods import APIMod
|
from app.models.mods import APIMod
|
||||||
from app.models.performance import DIFFICULTY_CLASS, BeatmapAttributes
|
from app.models.performance import DifficultyAttributesUnion
|
||||||
from app.models.score import GameMode
|
from app.models.score import GameMode
|
||||||
|
|
||||||
from .beatmap_playcounts import BeatmapPlaycounts
|
from .beatmap_playcounts import BeatmapPlaycounts
|
||||||
from .beatmapset import Beatmapset, BeatmapsetResp
|
from .beatmapset import Beatmapset, BeatmapsetResp
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, TypeAdapter
|
||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
from sqlalchemy import Column, DateTime
|
from sqlalchemy import Column, DateTime
|
||||||
from sqlmodel import VARCHAR, Field, Relationship, SQLModel, col, exists, func, select
|
from sqlmodel import VARCHAR, Field, Relationship, SQLModel, col, exists, func, select
|
||||||
@@ -246,12 +246,10 @@ async def calculate_beatmap_attributes(
|
|||||||
mods_: list[APIMod],
|
mods_: list[APIMod],
|
||||||
redis: Redis,
|
redis: Redis,
|
||||||
fetcher: "Fetcher",
|
fetcher: "Fetcher",
|
||||||
):
|
) -> DifficultyAttributesUnion:
|
||||||
attr_class = DIFFICULTY_CLASS.get(ruleset, BeatmapAttributes)
|
|
||||||
|
|
||||||
key = f"beatmap:{beatmap_id}:{ruleset}:{hashlib.sha256(str(mods_).encode()).hexdigest()}:attributes"
|
key = f"beatmap:{beatmap_id}:{ruleset}:{hashlib.sha256(str(mods_).encode()).hexdigest()}:attributes"
|
||||||
if await redis.exists(key):
|
if await redis.exists(key):
|
||||||
return attr_class.model_validate_json(await redis.get(key))
|
return TypeAdapter(DifficultyAttributesUnion).validate_json(await redis.get(key))
|
||||||
resp = await fetcher.get_or_fetch_beatmap_raw(redis, beatmap_id)
|
resp = await fetcher.get_or_fetch_beatmap_raw(redis, beatmap_id)
|
||||||
|
|
||||||
attr = await get_calculator().calculate_difficulty(resp, mods_, ruleset)
|
attr = await get_calculator().calculate_difficulty(resp, mods_, ruleset)
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
from app.models.score import GameMode
|
# Version: 2025.10.19
|
||||||
|
# Auto-generated by scripts/generate_ruleset_attributes.py.
|
||||||
|
# Schema generated by https://github.com/GooGuTeam/custom-rulesets
|
||||||
|
# Do not edit this file directly.
|
||||||
|
# ruff: noqa: E501
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, RootModel
|
||||||
|
|
||||||
|
|
||||||
|
class OsuAttributeModels(RootModel[Any]):
|
||||||
|
root: Any = Field(..., title="osu! Attribute models")
|
||||||
|
|
||||||
|
|
||||||
class PerformanceAttributes(BaseModel):
|
class PerformanceAttributes(BaseModel):
|
||||||
pp: float
|
pp: float = Field(..., description="Calculated score performance points.")
|
||||||
|
|
||||||
|
|
||||||
|
class DifficultyAttributes(BaseModel):
|
||||||
|
star_rating: float = Field(..., description="The combined star rating of all skills.")
|
||||||
|
max_combo: int = Field(..., description="The maximum achievable combo.")
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/ppy/osu/blob/9ebc5b0a35452e50bd408af1db62cfc22a57b1f4/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
|
|
||||||
class OsuPerformanceAttributes(PerformanceAttributes):
|
class OsuPerformanceAttributes(PerformanceAttributes):
|
||||||
aim: float
|
aim: float
|
||||||
speed: float
|
speed: float
|
||||||
@@ -15,67 +30,111 @@ class OsuPerformanceAttributes(PerformanceAttributes):
|
|||||||
flashlight: float
|
flashlight: float
|
||||||
effective_miss_count: float
|
effective_miss_count: float
|
||||||
speed_deviation: float | None = None
|
speed_deviation: float | None = None
|
||||||
|
combo_based_estimated_miss_count: float
|
||||||
# 2025 Q3 update
|
score_based_estimated_miss_count: float | None = None
|
||||||
# combo_based_estimated_miss_count: int
|
aim_estimated_slider_breaks: float
|
||||||
# score_based_estimated_miss_count: int | None = None
|
speed_estimated_slider_breaks: float
|
||||||
# aim_estimated_slider_breaks: int
|
|
||||||
# speed_estimated_slider_breaks: int
|
|
||||||
|
class OsuDifficultyAttributes(DifficultyAttributes):
|
||||||
|
aim_difficulty: float = Field(..., description="The difficulty corresponding to the aim skill.")
|
||||||
|
aim_difficult_slider_count: float = Field(..., description="The number of Sliders weighted by difficulty.")
|
||||||
|
speed_difficulty: float = Field(..., description="The difficulty corresponding to the speed skill.")
|
||||||
|
speed_note_count: float = Field(
|
||||||
|
..., description="The number of clickable objects weighted by difficulty.\nRelated to SpeedDifficulty"
|
||||||
|
)
|
||||||
|
flashlight_difficulty: float = Field(..., description="The difficulty corresponding to the flashlight skill.")
|
||||||
|
slider_factor: float = Field(
|
||||||
|
...,
|
||||||
|
description="Describes how much of AimDifficulty is contributed to by hitcircles or sliders.\nA value closer to 1.0 indicates most of AimDifficulty is contributed by hitcircles.\nA value closer to 0.0 indicates most of AimDifficulty is contributed by sliders.",
|
||||||
|
)
|
||||||
|
aim_top_weighted_slider_factor: float = Field(
|
||||||
|
...,
|
||||||
|
description="Describes how much of AimDifficultStrainCount is contributed to by hitcircles or sliders\nA value closer to 0.0 indicates most of AimDifficultStrainCount is contributed by hitcircles\nA value closer to Infinity indicates most of AimDifficultStrainCount is contributed by sliders",
|
||||||
|
)
|
||||||
|
speed_top_weighted_slider_factor: float = Field(
|
||||||
|
...,
|
||||||
|
description="Describes how much of SpeedDifficultStrainCount is contributed to by hitcircles or sliders\nA value closer to 0.0 indicates most of SpeedDifficultStrainCount is contributed by hitcircles\nA value closer to Infinity indicates most of SpeedDifficultStrainCount is contributed by sliders",
|
||||||
|
)
|
||||||
|
aim_difficult_strain_count: float
|
||||||
|
speed_difficult_strain_count: float
|
||||||
|
nested_score_per_object: float
|
||||||
|
legacy_score_base_multiplier: float
|
||||||
|
maximum_legacy_combo_score: float
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/ppy/osu/blob/9ebc5b0a35452e50bd408af1db62cfc22a57b1f4/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs
|
|
||||||
class TaikoPerformanceAttributes(PerformanceAttributes):
|
class TaikoPerformanceAttributes(PerformanceAttributes):
|
||||||
difficulty: float
|
difficulty: float
|
||||||
accuracy: float
|
accuracy: float
|
||||||
estimated_unstable_rate: float | None = None
|
estimated_unstable_rate: float | None = None
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/ppy/osu/blob/9ebc5b0a35452e50bd408af1db62cfc22a57b1f4/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs
|
class TaikoDifficultyAttributes(DifficultyAttributes):
|
||||||
|
rhythm_difficulty: float = Field(..., description="The difficulty corresponding to the rhythm skill.")
|
||||||
|
mono_stamina_factor: float = Field(
|
||||||
|
...,
|
||||||
|
description="The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty.",
|
||||||
|
)
|
||||||
|
consistency_factor: float = Field(..., description="The factor corresponding to the consistency of a map.")
|
||||||
|
|
||||||
|
|
||||||
|
CatchPerformanceAttributes = PerformanceAttributes
|
||||||
|
|
||||||
|
|
||||||
|
CatchDifficultyAttributes = DifficultyAttributes
|
||||||
|
|
||||||
|
|
||||||
class ManiaPerformanceAttributes(PerformanceAttributes):
|
class ManiaPerformanceAttributes(PerformanceAttributes):
|
||||||
difficulty: float
|
difficulty: float
|
||||||
|
|
||||||
|
|
||||||
PERFORMANCE_CLASS: dict[GameMode, type[PerformanceAttributes]] = {
|
ManiaDifficultyAttributes = DifficultyAttributes
|
||||||
GameMode.OSU: OsuPerformanceAttributes,
|
|
||||||
GameMode.MANIA: ManiaPerformanceAttributes,
|
|
||||||
GameMode.TAIKO: TaikoPerformanceAttributes,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BeatmapAttributes(BaseModel):
|
HishigataPerformanceAttributes = PerformanceAttributes
|
||||||
star_rating: float
|
|
||||||
max_combo: int
|
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/ppy/osu/blob/9ebc5b0a35452e50bd408af1db62cfc22a57b1f4/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
|
HishigataDifficultyAttributes = DifficultyAttributes
|
||||||
class OsuBeatmapAttributes(BeatmapAttributes):
|
|
||||||
|
|
||||||
|
RushPerformanceAttributes = PerformanceAttributes
|
||||||
|
|
||||||
|
|
||||||
|
RushDifficultyAttributes = DifficultyAttributes
|
||||||
|
|
||||||
|
|
||||||
|
SentakkiPerformanceAttributes = PerformanceAttributes
|
||||||
|
|
||||||
|
|
||||||
|
SentakkiDifficultyAttributes = DifficultyAttributes
|
||||||
|
|
||||||
|
|
||||||
|
SoyokazePerformanceAttributes = PerformanceAttributes
|
||||||
|
|
||||||
|
|
||||||
|
SoyokazeDifficultyAttributes = DifficultyAttributes
|
||||||
|
|
||||||
|
|
||||||
|
class TauPerformanceAttribute(PerformanceAttributes):
|
||||||
|
aim: float
|
||||||
|
speed: float
|
||||||
|
accuracy: float
|
||||||
|
complexity: float
|
||||||
|
effective_miss_count: float
|
||||||
|
|
||||||
|
|
||||||
|
class TauDifficultyAttributes(DifficultyAttributes):
|
||||||
aim_difficulty: float
|
aim_difficulty: float
|
||||||
aim_difficult_slider_count: float
|
|
||||||
speed_difficulty: float
|
speed_difficulty: float
|
||||||
speed_note_count: float
|
complexity_difficulty: float
|
||||||
flashlight_difficulty: float | None = None
|
approach_rate: float
|
||||||
slider_factor: float
|
slider_factor: float
|
||||||
aim_difficult_strain_count: float
|
overall_difficulty: float
|
||||||
speed_difficult_strain_count: float
|
|
||||||
|
|
||||||
# 2025 Q3 update
|
|
||||||
# aim_top_weighted_slider_factor: float
|
|
||||||
# speed_top_weighted_slider_factor: float
|
|
||||||
# nested_score_per_object: float
|
|
||||||
# legacy_score_base_multiplier: float
|
|
||||||
# maximum_legacy_combo_score: float
|
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/ppy/osu/blob/9ebc5b0a35452e50bd408af1db62cfc22a57b1f4/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
|
PerformanceAttributesUnion = (
|
||||||
class TaikoBeatmapAttributes(BeatmapAttributes):
|
OsuPerformanceAttributes | TaikoPerformanceAttributes | ManiaPerformanceAttributes | PerformanceAttributes
|
||||||
rhythm_difficulty: float
|
)
|
||||||
mono_stamina_factor: float
|
DifficultyAttributesUnion = (
|
||||||
|
OsuDifficultyAttributes | TaikoDifficultyAttributes | TauDifficultyAttributes | DifficultyAttributes
|
||||||
# 2025 Q3 update
|
)
|
||||||
# consistency_factor: float
|
|
||||||
|
|
||||||
|
|
||||||
DIFFICULTY_CLASS: dict[GameMode, type[BeatmapAttributes]] = {
|
|
||||||
GameMode.OSU: OsuBeatmapAttributes,
|
|
||||||
GameMode.TAIKO: TaikoBeatmapAttributes,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from app.dependencies.database import Database, Redis
|
|||||||
from app.dependencies.fetcher import Fetcher
|
from app.dependencies.fetcher import Fetcher
|
||||||
from app.models.beatmap import BeatmapRankStatus, Genre, Language
|
from app.models.beatmap import BeatmapRankStatus, Genre, Language
|
||||||
from app.models.mods import int_to_mods
|
from app.models.mods import int_to_mods
|
||||||
from app.models.performance import OsuBeatmapAttributes
|
from app.models.performance import OsuDifficultyAttributes
|
||||||
from app.models.score import GameMode
|
from app.models.score import GameMode
|
||||||
|
|
||||||
from .router import AllStrModel, router
|
from .router import AllStrModel, router
|
||||||
@@ -196,7 +196,7 @@ async def get_beatmaps(
|
|||||||
)
|
)
|
||||||
aim_diff = None
|
aim_diff = None
|
||||||
speed_diff = None
|
speed_diff = None
|
||||||
if isinstance(attrs, OsuBeatmapAttributes):
|
if isinstance(attrs, OsuDifficultyAttributes):
|
||||||
aim_diff = attrs.aim_difficulty
|
aim_diff = attrs.aim_difficulty
|
||||||
speed_diff = attrs.speed_difficulty
|
speed_diff = attrs.speed_difficulty
|
||||||
results.append(await V1Beatmap.from_db(session, beatmap, aim_diff, speed_diff))
|
results.append(await V1Beatmap.from_db(session, beatmap, aim_diff, speed_diff))
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ from app.dependencies.fetcher import Fetcher
|
|||||||
from app.dependencies.user import get_current_user
|
from app.dependencies.user import get_current_user
|
||||||
from app.helpers.asset_proxy_helper import asset_proxy_response
|
from app.helpers.asset_proxy_helper import asset_proxy_response
|
||||||
from app.models.mods import APIMod, int_to_mods
|
from app.models.mods import APIMod, int_to_mods
|
||||||
from app.models.performance import BeatmapAttributes, OsuBeatmapAttributes, TaikoBeatmapAttributes
|
from app.models.performance import (
|
||||||
|
DifficultyAttributes,
|
||||||
|
DifficultyAttributesUnion,
|
||||||
|
)
|
||||||
from app.models.score import (
|
from app.models.score import (
|
||||||
GameMode,
|
GameMode,
|
||||||
)
|
)
|
||||||
@@ -127,7 +130,7 @@ async def batch_get_beatmaps(
|
|||||||
"/beatmaps/{beatmap_id}/attributes",
|
"/beatmaps/{beatmap_id}/attributes",
|
||||||
tags=["谱面"],
|
tags=["谱面"],
|
||||||
name="计算谱面属性",
|
name="计算谱面属性",
|
||||||
response_model=BeatmapAttributes | OsuBeatmapAttributes | TaikoBeatmapAttributes,
|
response_model=DifficultyAttributesUnion,
|
||||||
description=("计算谱面指定 mods / ruleset 下谱面的难度属性 (难度/PP 相关属性)。"),
|
description=("计算谱面指定 mods / ruleset 下谱面的难度属性 (难度/PP 相关属性)。"),
|
||||||
)
|
)
|
||||||
async def get_beatmap_attributes(
|
async def get_beatmap_attributes(
|
||||||
@@ -166,7 +169,7 @@ async def get_beatmap_attributes(
|
|||||||
f"{hashlib.md5(str(mods_).encode(), usedforsecurity=False).hexdigest()}:attributes"
|
f"{hashlib.md5(str(mods_).encode(), usedforsecurity=False).hexdigest()}:attributes"
|
||||||
)
|
)
|
||||||
if await redis.exists(key):
|
if await redis.exists(key):
|
||||||
return BeatmapAttributes.model_validate_json(await redis.get(key)) # pyright: ignore[reportArgumentType]
|
return DifficultyAttributes.model_validate_json(await redis.get(key)) # pyright: ignore[reportArgumentType]
|
||||||
try:
|
try:
|
||||||
return await calculate_beatmap_attributes(beatmap_id, ruleset, mods_, redis, fetcher)
|
return await calculate_beatmap_attributes(beatmap_id, ruleset, mods_, redis, fetcher)
|
||||||
except HTTPStatusError:
|
except HTTPStatusError:
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ ignore = [
|
|||||||
[tool.ruff.lint.extend-per-file-ignores]
|
[tool.ruff.lint.extend-per-file-ignores]
|
||||||
"tools/*.py" = ["PTH", "INP001"]
|
"tools/*.py" = ["PTH", "INP001"]
|
||||||
"migrations/**/*.py" = ["INP001"]
|
"migrations/**/*.py" = ["INP001"]
|
||||||
".github/**/*.py" = ["INP001"]
|
"scripts/**/*.py" = ["INP001"]
|
||||||
"app/achievements/*.py" = ["INP001", "ARG"]
|
"app/achievements/*.py" = ["INP001", "ARG"]
|
||||||
"app/router/**/*.py" = ["ARG001"]
|
"app/router/**/*.py" = ["ARG001"]
|
||||||
|
|
||||||
@@ -125,6 +125,7 @@ osupyparser = { git = "https://github.com/MingxuanGame/osupyparser.git" }
|
|||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
"datamodel-code-generator>=0.35.0",
|
||||||
"pre-commit>=4.2.0",
|
"pre-commit>=4.2.0",
|
||||||
"pyright>=1.1.404",
|
"pyright>=1.1.404",
|
||||||
"ruff>=0.12.4",
|
"ruff>=0.12.4",
|
||||||
|
|||||||
64
scripts/generate_ruleset_attributes.py
Normal file
64
scripts/generate_ruleset_attributes.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from datamodel_code_generator import DataModelType, generate
|
||||||
|
from datamodel_code_generator.format import Formatter
|
||||||
|
|
||||||
|
OUTPUT_FILE = Path("app/models/performance.py")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_model(schema_file: Path, version: str = ""):
|
||||||
|
temp_file = OUTPUT_FILE.parent / "_temp.py"
|
||||||
|
if temp_file.exists():
|
||||||
|
temp_file.unlink()
|
||||||
|
temp_file.touch()
|
||||||
|
|
||||||
|
version = version or datetime.datetime.now().strftime("%Y.%m.%d")
|
||||||
|
|
||||||
|
generate(
|
||||||
|
input_=schema_file,
|
||||||
|
output=temp_file,
|
||||||
|
output_model_type=DataModelType.PydanticV2BaseModel,
|
||||||
|
disable_future_imports=True,
|
||||||
|
formatters=[Formatter.RUFF_CHECK, Formatter.RUFF_FORMAT],
|
||||||
|
custom_file_header=(
|
||||||
|
f"# Version: {version}\n"
|
||||||
|
"# Auto-generated by scripts/generate_ruleset_attributes.py.\n"
|
||||||
|
"# Schema generated by https://github.com/GooGuTeam/custom-rulesets\n"
|
||||||
|
"# Do not edit this file directly.\n"
|
||||||
|
"# ruff: noqa: E501\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
code = temp_file.read_text()
|
||||||
|
output = code
|
||||||
|
performance_attributes_classes = re.findall(r"class (\w+PerformanceAttributes)", code)
|
||||||
|
performance_attributes_classes.append("PerformanceAttributes")
|
||||||
|
output += "PerformanceAttributesUnion = " + " | ".join(performance_attributes_classes) + "\n"
|
||||||
|
difficulty_attributes_classes = re.findall(r"class (\w+DifficultyAttributes)", code)
|
||||||
|
difficulty_attributes_classes.append("DifficultyAttributes")
|
||||||
|
output += "DifficultyAttributesUnion = " + " | ".join(difficulty_attributes_classes) + "\n"
|
||||||
|
OUTPUT_FILE.write_text(output)
|
||||||
|
temp_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
["uv", "run", "ruff", "check", "--fix", "app/models/performance.py"], # nosec B607 # noqa: S607
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["uv", "run", "ruff", "format", "app/models/performance.py"], # nosec B607 # noqa: S607
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 1:
|
||||||
|
print("Usage: python scripts/generate_ruleset_attributes.py <schema_file> [version]")
|
||||||
|
sys.exit(1)
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
generate_model(Path(sys.argv[1]), sys.argv[2])
|
||||||
|
else:
|
||||||
|
generate_model(Path(sys.argv[1]))
|
||||||
144
uv.lock
generated
144
uv.lock
generated
@@ -190,6 +190,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004, upload-time = "2024-11-24T19:39:24.442Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004, upload-time = "2024-11-24T19:39:24.442Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argcomplete"
|
||||||
|
version = "3.6.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/16/0f/861e168fc813c56a78b35f3c30d91c6757d1fd185af1110f1aec784b35d0/argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf", size = 73403, upload-time = "2025-04-03T04:57:03.52Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708, upload-time = "2025-04-03T04:57:01.591Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "25.3.0"
|
version = "25.3.0"
|
||||||
@@ -265,6 +274,31 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" },
|
{ url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "25.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "mypy-extensions" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pathspec" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "pytokens" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4b/43/20b5c90612d7bdb2bdbcceeb53d588acca3bb8f0e4c5d5c751a2c8fdd55a/black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619", size = 648393, upload-time = "2025-09-19T00:27:37.758Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/8e/319cfe6c82f7e2d5bfb4d3353c6cc85b523d677ff59edc61fdb9ee275234/black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0", size = 1742012, upload-time = "2025-09-19T00:33:08.678Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/cc/f562fe5d0a40cd2a4e6ae3f685e4c36e365b1f7e494af99c26ff7f28117f/black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4", size = 1581421, upload-time = "2025-09-19T00:35:25.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/67/6db6dff1ebc8965fd7661498aea0da5d7301074b85bba8606a28f47ede4d/black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e", size = 1655619, upload-time = "2025-09-19T00:30:49.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/10/3faef9aa2a730306cf469d76f7f155a8cc1f66e74781298df0ba31f8b4c8/black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a", size = 1342481, upload-time = "2025-09-19T00:31:29.625Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/99/3acfea65f5e79f45472c45f87ec13037b506522719cd9d4ac86484ff51ac/black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175", size = 1742165, upload-time = "2025-09-19T00:34:10.402Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/18/799285282c8236a79f25d590f0222dbd6850e14b060dfaa3e720241fd772/black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f", size = 1581259, upload-time = "2025-09-19T00:32:49.685Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/ce/883ec4b6303acdeca93ee06b7622f1fa383c6b3765294824165d49b1a86b/black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831", size = 1655583, upload-time = "2025-09-19T00:30:44.505Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/17/5c253aa80a0639ccc427a5c7144534b661505ae2b5a10b77ebe13fa25334/black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357", size = 1343428, upload-time = "2025-09-19T00:32:13.839Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bleach"
|
name = "bleach"
|
||||||
version = "6.2.0"
|
version = "6.2.0"
|
||||||
@@ -511,6 +545,26 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/3e/7c/15ad426257615f9be8caf7f97990cf3dcbb5b8dd7ed7e0db581a1c4759dd/cryptography-46.0.2-cp38-abi3-win_arm64.whl", hash = "sha256:91447f2b17e83c9e0c89f133119d83f94ce6e0fb55dd47da0a959316e6e9cfa1", size = 2918153, upload-time = "2025-10-01T00:28:51.003Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/7c/15ad426257615f9be8caf7f97990cf3dcbb5b8dd7ed7e0db581a1c4759dd/cryptography-46.0.2-cp38-abi3-win_arm64.whl", hash = "sha256:91447f2b17e83c9e0c89f133119d83f94ce6e0fb55dd47da0a959316e6e9cfa1", size = 2918153, upload-time = "2025-10-01T00:28:51.003Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "datamodel-code-generator"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "argcomplete" },
|
||||||
|
{ name = "black" },
|
||||||
|
{ name = "genson" },
|
||||||
|
{ name = "inflect" },
|
||||||
|
{ name = "isort" },
|
||||||
|
{ name = "jinja2" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/70/e1/dbf7c2edb1b1db1f4fd472ee92f985ec97d58902512013d9c4584108329c/datamodel_code_generator-0.35.0.tar.gz", hash = "sha256:46805fa2515d3871f6bfafce9aa63128e735a7a6a4cfcbf9c27b3794ee4ea846", size = 459915, upload-time = "2025-10-09T19:26:49.837Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/ef/0ed17459fe6076219fcd45f69a0bb4bd1cb041b39095ca2946808a9b5f04/datamodel_code_generator-0.35.0-py3-none-any.whl", hash = "sha256:c356d1e4a555f86667a4262db03d4598a30caeda8f51786555fd269c8abb806b", size = 121436, upload-time = "2025-10-09T19:26:48.437Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "distlib"
|
name = "distlib"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -691,6 +745,7 @@ dependencies = [
|
|||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "datamodel-code-generator" },
|
||||||
{ name = "pre-commit" },
|
{ name = "pre-commit" },
|
||||||
{ name = "pyright" },
|
{ name = "pyright" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
@@ -735,12 +790,22 @@ requires-dist = [
|
|||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "datamodel-code-generator", specifier = ">=0.35.0" },
|
||||||
{ name = "pre-commit", specifier = ">=4.2.0" },
|
{ name = "pre-commit", specifier = ">=4.2.0" },
|
||||||
{ name = "pyright", specifier = ">=1.1.404" },
|
{ name = "pyright", specifier = ">=1.1.404" },
|
||||||
{ name = "ruff", specifier = ">=0.12.4" },
|
{ name = "ruff", specifier = ">=0.12.4" },
|
||||||
{ name = "types-aioboto3", extras = ["aioboto3", "essential"], specifier = ">=15.0.0" },
|
{ name = "types-aioboto3", extras = ["aioboto3", "essential"], specifier = ">=15.0.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "genson"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c5/cf/2303c8ad276dcf5ee2ad6cf69c4338fd86ef0f471a5207b069adf7a393cf/genson-1.3.0.tar.gz", hash = "sha256:e02db9ac2e3fd29e65b5286f7135762e2cd8a986537c075b06fc5f1517308e37", size = 34919, upload-time = "2024-05-15T22:08:49.123Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/5c/e226de133afd8bb267ec27eead9ae3d784b95b39a287ed404caab39a5f50/genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7", size = 21470, upload-time = "2024-05-15T22:08:47.056Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "3.2.4"
|
version = "3.2.4"
|
||||||
@@ -851,6 +916,28 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inflect"
|
||||||
|
version = "7.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "more-itertools" },
|
||||||
|
{ name = "typeguard" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/78/c6/943357d44a21fd995723d07ccaddd78023eace03c1846049a2645d4324a3/inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f", size = 73751, upload-time = "2024-12-28T17:11:18.897Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isort"
|
||||||
|
version = "6.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1e/82/fa43935523efdfcce6abbae9da7f372b627b27142c3419fcf13bf5b0c397/isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481", size = 824325, upload-time = "2025-10-01T16:26:45.027Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/cc/9b681a170efab4868a032631dea1e8446d8ec718a7f657b94d49d1a12643/isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784", size = 94329, upload-time = "2025-10-01T16:26:43.291Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jinja2"
|
name = "jinja2"
|
||||||
version = "3.1.6"
|
version = "3.1.6"
|
||||||
@@ -1027,6 +1114,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/62/33/09601f476fd9d494e967f15c1e05aa1e35bdf5ee54555596e05e5c9ec8c9/maxminddb-2.8.2-cp314-cp314t-win_arm64.whl", hash = "sha256:929a00528db82ffa5aa928a9cd1a972e8f93c36243609c25574dfd920c21533b", size = 33990, upload-time = "2025-07-25T20:31:23.367Z" },
|
{ url = "https://files.pythonhosted.org/packages/62/33/09601f476fd9d494e967f15c1e05aa1e35bdf5ee54555596e05e5c9ec8c9/maxminddb-2.8.2-cp314-cp314t-win_arm64.whl", hash = "sha256:929a00528db82ffa5aa928a9cd1a972e8f93c36243609c25574dfd920c21533b", size = 33990, upload-time = "2025-07-25T20:31:23.367Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "more-itertools"
|
||||||
|
version = "10.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.6.4"
|
version = "6.6.4"
|
||||||
@@ -1090,6 +1186,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" },
|
{ url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "newrelic"
|
name = "newrelic"
|
||||||
version = "11.0.0"
|
version = "11.0.0"
|
||||||
@@ -1120,6 +1225,15 @@ name = "osupyparser"
|
|||||||
version = "1.0.8"
|
version = "1.0.8"
|
||||||
source = { git = "https://github.com/MingxuanGame/osupyparser.git#f55794b775f61d8f0ea2f6bbc54a03436235df15" }
|
source = { git = "https://github.com/MingxuanGame/osupyparser.git#f55794b775f61d8f0ea2f6bbc54a03436235df15" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "25.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "passlib"
|
name = "passlib"
|
||||||
version = "1.7.4"
|
version = "1.7.4"
|
||||||
@@ -1134,6 +1248,15 @@ bcrypt = [
|
|||||||
{ name = "bcrypt" },
|
{ name = "bcrypt" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "11.3.0"
|
version = "11.3.0"
|
||||||
@@ -1456,6 +1579,15 @@ 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/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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytokens"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d4/c2/dbadcdddb412a267585459142bfd7cc241e6276db69339353ae6e241ab2b/pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43", size = 15368, upload-time = "2025-10-15T08:02:42.738Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/5a/c269ea6b348b6f2c32686635df89f32dbe05df1088dd4579302a6f8f99af/pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8", size = 12038, upload-time = "2025-10-15T08:02:41.694Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
@@ -1714,6 +1846,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" },
|
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typeguard"
|
||||||
|
version = "4.4.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/68/71c1a15b5f65f40e91b65da23b8224dad41349894535a97f63a52e462196/typeguard-4.4.4.tar.gz", hash = "sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74", size = 75203, upload-time = "2025-06-18T09:56:07.624Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl", hash = "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e", size = 34874, upload-time = "2025-06-18T09:56:05.999Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-aioboto3"
|
name = "types-aioboto3"
|
||||||
version = "15.1.0"
|
version = "15.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user