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:
MingxuanGame
2025-10-25 19:10:53 +08:00
committed by GitHub
parent f792d146b5
commit 2c81e22749
12 changed files with 370 additions and 85 deletions

View File

@@ -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: |

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)

View File

@@ -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,
}

View File

@@ -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))

View File

@@ -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:

View File

@@ -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",

View 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
View File

@@ -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"