From 2c81e227497d76ce5f8d2a3da12bad4ee0e16fa4 Mon Sep 17 00:00:00 2001 From: MingxuanGame Date: Sat, 25 Oct 2025 19:10:53 +0800 Subject: [PATCH] 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 ``` --- .../workflows/generate-configuration-doc.yml | 4 +- app/calculators/performance/_base.py | 4 +- .../performance/performance_server.py | 16 +- app/calculators/performance/rosu.py | 44 +++-- app/database/beatmap.py | 10 +- app/models/performance.py | 153 ++++++++++++------ app/router/v1/beatmap.py | 4 +- app/router/v2/beatmap.py | 9 +- pyproject.toml | 3 +- .../generate_config_doc.py | 0 scripts/generate_ruleset_attributes.py | 64 ++++++++ uv.lock | 144 +++++++++++++++++ 12 files changed, 370 insertions(+), 85 deletions(-) rename {.github/scripts => scripts}/generate_config_doc.py (100%) create mode 100644 scripts/generate_ruleset_attributes.py diff --git a/.github/workflows/generate-configuration-doc.yml b/.github/workflows/generate-configuration-doc.yml index a5f94f5..ac24a2b 100644 --- a/.github/workflows/generate-configuration-doc.yml +++ b/.github/workflows/generate-configuration-doc.yml @@ -6,7 +6,7 @@ on: - main paths: - "app/config.py" - - ".github/scripts/generate_config_doc.py" + - "scripts/generate_config_doc.py" workflow_dispatch: permissions: @@ -42,7 +42,7 @@ jobs: - name: Generate Markdown run: | 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 run: | diff --git a/app/calculators/performance/_base.py b/app/calculators/performance/_base.py index 4aaf481..3f7bf4e 100644 --- a/app/calculators/performance/_base.py +++ b/app/calculators/performance/_base.py @@ -2,7 +2,7 @@ import abc from typing import TYPE_CHECKING 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 if TYPE_CHECKING: @@ -33,5 +33,5 @@ class PerformanceCalculator(abc.ABC): @abc.abstractmethod async def calculate_difficulty( self, beatmap_raw: str, mods: list[APIMod] | None = None, gamemode: GameMode | None = None - ) -> BeatmapAttributes: + ) -> DifficultyAttributes: raise NotImplementedError diff --git a/app/calculators/performance/performance_server.py b/app/calculators/performance/performance_server.py index 20480ee..956f9c6 100644 --- a/app/calculators/performance/performance_server.py +++ b/app/calculators/performance/performance_server.py @@ -2,10 +2,10 @@ from typing import TYPE_CHECKING from app.models.mods import APIMod from app.models.performance import ( - DIFFICULTY_CLASS, - PERFORMANCE_CLASS, - BeatmapAttributes, + DifficultyAttributes, + DifficultyAttributesUnion, PerformanceAttributes, + PerformanceAttributesUnion, ) from app.models.score import GameMode @@ -17,6 +17,7 @@ from ._base import ( ) from httpx import AsyncClient, HTTPError +from pydantic import TypeAdapter if TYPE_CHECKING: from app.database.score import Score @@ -56,8 +57,7 @@ class PerformanceCalculator(BasePerformanceCalculator): ) if resp.status_code != 200: raise PerformanceError(f"Failed to calculate performance: {resp.text}") - data = resp.json() - return PERFORMANCE_CLASS.get(score.gamemode, PerformanceAttributes).model_validate(data) + return TypeAdapter(PerformanceAttributesUnion).validate_json(resp.text) except HTTPError as e: raise PerformanceError(f"Failed to calculate performance: {e}") from e except Exception as e: @@ -65,7 +65,7 @@ class PerformanceCalculator(BasePerformanceCalculator): async def calculate_difficulty( self, beatmap_raw: str, mods: list[APIMod] | None = None, gamemode: GameMode | None = None - ) -> BeatmapAttributes: + ) -> DifficultyAttributes: # https://github.com/GooGuTeam/osu-performance-server#post-difficulty async with AsyncClient() as client: try: @@ -79,9 +79,7 @@ class PerformanceCalculator(BasePerformanceCalculator): ) if resp.status_code != 200: raise DifficultyError(f"Failed to calculate difficulty: {resp.text}") - data = resp.json() - ruleset_id = data.pop("ruleset", "osu") - return DIFFICULTY_CLASS.get(GameMode(ruleset_id), BeatmapAttributes).model_validate(data) + return TypeAdapter(DifficultyAttributesUnion).validate_json(resp.text) except HTTPError as e: raise DifficultyError(f"Failed to calculate difficulty: {e}") from e except Exception as e: diff --git a/app/calculators/performance/rosu.py b/app/calculators/performance/rosu.py index 244e1f9..f2d1ca9 100644 --- a/app/calculators/performance/rosu.py +++ b/app/calculators/performance/rosu.py @@ -5,14 +5,12 @@ from typing import TYPE_CHECKING from app.calculator import clamp from app.models.mods import APIMod, parse_enum_to_str from app.models.performance import ( - DIFFICULTY_CLASS, - PERFORMANCE_CLASS, - BeatmapAttributes, + DifficultyAttributes, ManiaPerformanceAttributes, - OsuBeatmapAttributes, + OsuDifficultyAttributes, OsuPerformanceAttributes, PerformanceAttributes, - TaikoBeatmapAttributes, + TaikoDifficultyAttributes, TaikoPerformanceAttributes, ) from app.models.score import GameMode @@ -38,6 +36,16 @@ except ImportError: " 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): @classmethod @@ -75,6 +83,10 @@ class PerformanceCalculator(BasePerformanceCalculator): flashlight=attr.pp_flashlight or 0, effective_miss_count=attr.effective_miss_count or 0, 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: return TaikoPerformanceAttributes( @@ -120,11 +132,11 @@ class PerformanceCalculator(BasePerformanceCalculator): raise CalculateError(f"Unknown error: {e}") from e @classmethod - def _diff_attr_to_model(cls, diff: rosu.DifficultyAttributes, gamemode: GameMode) -> BeatmapAttributes: - attr_class = DIFFICULTY_CLASS.get(gamemode, BeatmapAttributes) + def _diff_attr_to_model(cls, diff: rosu.DifficultyAttributes, gamemode: GameMode) -> DifficultyAttributes: + attr_class = DIFFICULTY_CLASS.get(gamemode, DifficultyAttributes) - if attr_class is OsuBeatmapAttributes: - return OsuBeatmapAttributes( + if attr_class is OsuDifficultyAttributes: + return OsuDifficultyAttributes( star_rating=diff.stars, max_combo=diff.max_combo, aim_difficulty=diff.aim or 0, @@ -135,23 +147,29 @@ class PerformanceCalculator(BasePerformanceCalculator): aim_difficult_strain_count=diff.aim_difficult_strain_count or 0, speed_difficult_strain_count=diff.speed_difficult_strain_count 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: - return TaikoBeatmapAttributes( + elif attr_class is TaikoDifficultyAttributes: + return TaikoDifficultyAttributes( star_rating=diff.stars, max_combo=diff.max_combo, rhythm_difficulty=diff.rhythm or 0, mono_stamina_factor=diff.stamina or 0, + consistency_factor=0, ) else: - return BeatmapAttributes( + return DifficultyAttributes( star_rating=diff.stars, max_combo=diff.max_combo, ) async def calculate_difficulty( self, beatmap_raw: str, mods: list[APIMod] | None = None, gamemode: GameMode | None = None - ) -> BeatmapAttributes: + ) -> DifficultyAttributes: try: map = rosu.Beatmap(content=beatmap_raw) if gamemode is not None: diff --git a/app/database/beatmap.py b/app/database/beatmap.py index 072902a..93aa260 100644 --- a/app/database/beatmap.py +++ b/app/database/beatmap.py @@ -8,13 +8,13 @@ from app.database.beatmap_tags import BeatmapTagVote from app.database.failtime import FailTime, FailTimeResp from app.models.beatmap import BeatmapRankStatus 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 .beatmap_playcounts import BeatmapPlaycounts from .beatmapset import Beatmapset, BeatmapsetResp -from pydantic import BaseModel +from pydantic import BaseModel, TypeAdapter from redis.asyncio import Redis from sqlalchemy import Column, DateTime from sqlmodel import VARCHAR, Field, Relationship, SQLModel, col, exists, func, select @@ -246,12 +246,10 @@ async def calculate_beatmap_attributes( mods_: list[APIMod], redis: Redis, fetcher: "Fetcher", -): - attr_class = DIFFICULTY_CLASS.get(ruleset, BeatmapAttributes) - +) -> DifficultyAttributesUnion: key = f"beatmap:{beatmap_id}:{ruleset}:{hashlib.sha256(str(mods_).encode()).hexdigest()}:attributes" 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) attr = await get_calculator().calculate_difficulty(resp, mods_, ruleset) diff --git a/app/models/performance.py b/app/models/performance.py index f75d364..612edb3 100644 --- a/app/models/performance.py +++ b/app/models/performance.py @@ -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): - 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): aim: float speed: float @@ -15,67 +30,111 @@ class OsuPerformanceAttributes(PerformanceAttributes): flashlight: float effective_miss_count: float speed_deviation: float | None = None - - # 2025 Q3 update - # combo_based_estimated_miss_count: int - # score_based_estimated_miss_count: int | None = None - # aim_estimated_slider_breaks: int - # speed_estimated_slider_breaks: int + combo_based_estimated_miss_count: float + score_based_estimated_miss_count: float | None = None + aim_estimated_slider_breaks: float + speed_estimated_slider_breaks: float + + +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): difficulty: float accuracy: float 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): difficulty: float -PERFORMANCE_CLASS: dict[GameMode, type[PerformanceAttributes]] = { - GameMode.OSU: OsuPerformanceAttributes, - GameMode.MANIA: ManiaPerformanceAttributes, - GameMode.TAIKO: TaikoPerformanceAttributes, -} +ManiaDifficultyAttributes = DifficultyAttributes -class BeatmapAttributes(BaseModel): - star_rating: float - max_combo: int +HishigataPerformanceAttributes = PerformanceAttributes -# https://github.com/ppy/osu/blob/9ebc5b0a35452e50bd408af1db62cfc22a57b1f4/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs -class OsuBeatmapAttributes(BeatmapAttributes): +HishigataDifficultyAttributes = DifficultyAttributes + + +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_difficult_slider_count: float speed_difficulty: float - speed_note_count: float - flashlight_difficulty: float | None = None + complexity_difficulty: float + approach_rate: float slider_factor: float - aim_difficult_strain_count: 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 + overall_difficulty: float -# https://github.com/ppy/osu/blob/9ebc5b0a35452e50bd408af1db62cfc22a57b1f4/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs -class TaikoBeatmapAttributes(BeatmapAttributes): - rhythm_difficulty: float - mono_stamina_factor: float - - # 2025 Q3 update - # consistency_factor: float - - -DIFFICULTY_CLASS: dict[GameMode, type[BeatmapAttributes]] = { - GameMode.OSU: OsuBeatmapAttributes, - GameMode.TAIKO: TaikoBeatmapAttributes, -} +PerformanceAttributesUnion = ( + OsuPerformanceAttributes | TaikoPerformanceAttributes | ManiaPerformanceAttributes | PerformanceAttributes +) +DifficultyAttributesUnion = ( + OsuDifficultyAttributes | TaikoDifficultyAttributes | TauDifficultyAttributes | DifficultyAttributes +) diff --git a/app/router/v1/beatmap.py b/app/router/v1/beatmap.py index a440d37..4e4dd97 100644 --- a/app/router/v1/beatmap.py +++ b/app/router/v1/beatmap.py @@ -10,7 +10,7 @@ from app.dependencies.database import Database, Redis from app.dependencies.fetcher import Fetcher from app.models.beatmap import BeatmapRankStatus, Genre, Language 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 .router import AllStrModel, router @@ -196,7 +196,7 @@ async def get_beatmaps( ) aim_diff = None speed_diff = None - if isinstance(attrs, OsuBeatmapAttributes): + if isinstance(attrs, OsuDifficultyAttributes): aim_diff = attrs.aim_difficulty speed_diff = attrs.speed_difficulty results.append(await V1Beatmap.from_db(session, beatmap, aim_diff, speed_diff)) diff --git a/app/router/v2/beatmap.py b/app/router/v2/beatmap.py index 31cf816..8ebd7ec 100644 --- a/app/router/v2/beatmap.py +++ b/app/router/v2/beatmap.py @@ -11,7 +11,10 @@ from app.dependencies.fetcher import Fetcher from app.dependencies.user import get_current_user from app.helpers.asset_proxy_helper import asset_proxy_response 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 ( GameMode, ) @@ -127,7 +130,7 @@ async def batch_get_beatmaps( "/beatmaps/{beatmap_id}/attributes", tags=["谱面"], name="计算谱面属性", - response_model=BeatmapAttributes | OsuBeatmapAttributes | TaikoBeatmapAttributes, + response_model=DifficultyAttributesUnion, description=("计算谱面指定 mods / ruleset 下谱面的难度属性 (难度/PP 相关属性)。"), ) async def get_beatmap_attributes( @@ -166,7 +169,7 @@ async def get_beatmap_attributes( f"{hashlib.md5(str(mods_).encode(), usedforsecurity=False).hexdigest()}:attributes" ) 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: return await calculate_beatmap_attributes(beatmap_id, ruleset, mods_, redis, fetcher) except HTTPStatusError: diff --git a/pyproject.toml b/pyproject.toml index 01750c9..a78779f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ ignore = [ [tool.ruff.lint.extend-per-file-ignores] "tools/*.py" = ["PTH", "INP001"] "migrations/**/*.py" = ["INP001"] -".github/**/*.py" = ["INP001"] +"scripts/**/*.py" = ["INP001"] "app/achievements/*.py" = ["INP001", "ARG"] "app/router/**/*.py" = ["ARG001"] @@ -125,6 +125,7 @@ osupyparser = { git = "https://github.com/MingxuanGame/osupyparser.git" } [dependency-groups] dev = [ + "datamodel-code-generator>=0.35.0", "pre-commit>=4.2.0", "pyright>=1.1.404", "ruff>=0.12.4", diff --git a/.github/scripts/generate_config_doc.py b/scripts/generate_config_doc.py similarity index 100% rename from .github/scripts/generate_config_doc.py rename to scripts/generate_config_doc.py diff --git a/scripts/generate_ruleset_attributes.py b/scripts/generate_ruleset_attributes.py new file mode 100644 index 0000000..9a7cf63 --- /dev/null +++ b/scripts/generate_ruleset_attributes.py @@ -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 [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])) diff --git a/uv.lock b/uv.lock index b29b286..01f6b3a 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, ] +[[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]] name = "attrs" 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" }, ] +[[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]] name = "bleach" 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" }, ] +[[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]] name = "distlib" version = "0.4.0" @@ -691,6 +745,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "datamodel-code-generator" }, { name = "pre-commit" }, { name = "pyright" }, { name = "ruff" }, @@ -735,12 +790,22 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "datamodel-code-generator", specifier = ">=0.35.0" }, { name = "pre-commit", specifier = ">=4.2.0" }, { name = "pyright", specifier = ">=1.1.404" }, { name = "ruff", specifier = ">=0.12.4" }, { 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]] name = "greenlet" 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" }, ] +[[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]] name = "jinja2" 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" }, ] +[[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]] name = "multidict" 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" }, ] +[[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]] name = "newrelic" version = "11.0.0" @@ -1120,6 +1225,15 @@ name = "osupyparser" version = "1.0.8" 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]] name = "passlib" version = "1.7.4" @@ -1134,6 +1248,15 @@ 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]] name = "pillow" 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" }, ] +[[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]] name = "pyyaml" 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" }, ] +[[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]] name = "types-aioboto3" version = "15.1.0"