Python 포맷팅과 pre-commit 설정

맥락:
- Python 코드만 대상으로 Ruff 기반 format/lint와 pre-commit hook을 도입한다.
- Cython game.pyx는 Ruff 대상에서 제외해 포맷터 충돌을 피한다.

변경:
- ruff와 pre-commit dev dependency 및 Ruff 설정을 추가했다.
- pre-commit config와 Python format/check 스크립트를 추가했다.
- Ruff format/check --fix 결과로 Python import 정렬과 포맷을 적용했다.

확인:
- uv sync --extra gui
- uv run pre-commit run --all-files
- scripts/check-python.sh
- uv run lost-cities-classic
This commit is contained in:
2026-05-06 19:59:07 +09:00
parent b43bb5917f
commit 12b59211b2
14 changed files with 282 additions and 123 deletions
+7
View File
@@ -0,0 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.12
hooks:
- id: ruff-format
- id: ruff
args: [--fix]
+13
View File
@@ -23,7 +23,9 @@ lost-cities-classic = "coolrl_lost_cities.games.classic:main"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"cython>=3.0", "cython>=3.0",
"pre-commit>=4.0.0",
"pytest>=9.0.3", "pytest>=9.0.3",
"ruff>=0.8.0",
] ]
[build-system] [build-system]
@@ -40,3 +42,14 @@ include = ["coolrl_lost_cities*"]
"fixtures/*.json", "fixtures/*.json",
"docs/*.md", "docs/*.md",
] ]
[tool.ruff]
line-length = 100
target-version = "py311"
extend-exclude = [
"src/coolrl_lost_cities/games/classic/game.pyx",
]
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
ignore = ["E501"]
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
uv run ruff check .
uv run pytest tests/games/classic
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
uv run ruff format .
@@ -2,10 +2,10 @@ from __future__ import annotations
import json import json
import logging import logging
from pathlib import Path
import random import random
import subprocess import subprocess
import tempfile import tempfile
from pathlib import Path
from ..game import Card, LostCitiesConfig, build_deck from ..game import Card, LostCitiesConfig, build_deck
from ..interfaces import BackendName, Snapshot from ..interfaces import BackendName, Snapshot
@@ -68,8 +68,7 @@ class RustLostCitiesBackend:
fixture = { fixture = {
"config": self.config.to_snapshot(), "config": self.config.to_snapshot(),
"initial_deck": [card.to_snapshot() for card in self.initial_deck], "initial_deck": [card.to_snapshot() for card in self.initial_deck],
"steps": [{"action": None}] "steps": [{"action": None}] + [{"action": action} for action in self.actions],
+ [{"action": action} for action in self.actions],
} }
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as handle: with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as handle:
json.dump(fixture, handle) json.dump(fixture, handle)
@@ -1,8 +1,8 @@
from __future__ import annotations from __future__ import annotations
import logging
from dataclasses import dataclass from dataclasses import dataclass
from functools import lru_cache from functools import lru_cache
import logging
from ..game import Card, GameState, LostCitiesConfig from ..game import Card, GameState, LostCitiesConfig
from ..interfaces import BotInput, LostCitiesBot from ..interfaces import BotInput, LostCitiesBot
@@ -94,10 +94,7 @@ def derive_heuristic_config(
config: LostCitiesConfig, config: LostCitiesConfig,
params: SafeHeuristicParams, params: SafeHeuristicParams,
) -> DerivedHeuristicConfig: ) -> DerivedHeuristicConfig:
max_color_sum = sum( max_color_sum = sum(config.min_rank + rank - 1 for rank in range(1, config.n_ranks + 1))
config.min_rank + rank - 1
for rank in range(1, config.n_ranks + 1)
)
break_even_sum = -config.expedition_penalty break_even_sum = -config.expedition_penalty
# In small tiers, break-even can be impossible. Do not set impossible targets. # In small tiers, break-even can be impossible. Do not set impossible targets.
@@ -270,9 +267,7 @@ class SafeHeuristicBot(LostCitiesBot):
continue continue
# Handshakes need a stronger support than normal opening. # Handshakes need a stronger support than normal opening.
required_sum = ( required_sum = derived.open_target_sum * self.params.handshake_target_multiplier
derived.open_target_sum * self.params.handshake_target_multiplier
)
if number_sum < required_sum: if number_sum < required_sum:
continue continue
@@ -368,9 +363,7 @@ class SafeHeuristicBot(LostCitiesBot):
numeric_value = self._num(state, card) numeric_value = self._num(state, card)
current_sum = sum( current_sum = sum(
self._num(state, played) self._num(state, played) for played in expedition if not played.is_handshake
for played in expedition
if not played.is_handshake
) )
followups = [ followups = [
@@ -384,8 +377,8 @@ class SafeHeuristicBot(LostCitiesBot):
) )
] ]
projected_sum = current_sum + numeric_value + sum( projected_sum = (
self._num(state, followup) for followup in followups current_sum + numeric_value + sum(self._num(state, followup) for followup in followups)
) )
value = 0.0 value = 0.0
@@ -434,14 +427,17 @@ class SafeHeuristicBot(LostCitiesBot):
if deck_left <= derived.late_open_block_threshold: if deck_left <= derived.late_open_block_threshold:
return False return False
return self._opening_plan_value( return (
state=state, self._opening_plan_value(
player=player, state=state,
color=color, player=player,
opening_card=opening_card, color=color,
derived=derived, opening_card=opening_card,
deck_left=deck_left, derived=derived,
) > 0.0 deck_left=deck_left,
)
> 0.0
)
def _opening_plan_value( def _opening_plan_value(
self, self,
@@ -456,16 +452,10 @@ class SafeHeuristicBot(LostCitiesBot):
numbers = [ numbers = [
card card
for card in state.hands[player] for card in state.hands[player]
if ( if (card.color == color and not card.is_handshake and card.rank >= opening_card.rank)
card.color == color
and not card.is_handshake
and card.rank >= opening_card.rank
)
] ]
handshakes = [ handshakes = [
card card for card in state.hands[player] if card.color == color and card.is_handshake
for card in state.hands[player]
if card.color == color and card.is_handshake
] ]
opened_colors = sum(1 for expedition in state.expeditions[player] if expedition) opened_colors = sum(1 for expedition in state.expeditions[player] if expedition)
number_sum = sum(self._num(state, card) for card in numbers) number_sum = sum(self._num(state, card) for card in numbers)
@@ -481,15 +471,12 @@ class SafeHeuristicBot(LostCitiesBot):
) )
speculative_open = ( speculative_open = (
opened_colors <= 2 opened_colors <= 2
and and len(numbers) >= 2
len(numbers) >= 2
and number_sum >= 0.65 * derived.open_target_sum and number_sum >= 0.65 * derived.open_target_sum
and bool(high_cards) and bool(high_cards)
) )
single_late_open = ( single_late_open = (
deck_left <= derived.mid_deck_threshold deck_left <= derived.mid_deck_threshold and len(numbers) >= 1 and opening_value >= 8
and len(numbers) >= 1
and opening_value >= 8
) )
exceptional_open = ( exceptional_open = (
len(numbers) >= derived.min_open_cards + 1 len(numbers) >= derived.min_open_cards + 1
@@ -525,11 +512,7 @@ class SafeHeuristicBot(LostCitiesBot):
return 0.0 return 0.0
if exceptional_open: if exceptional_open:
return ( return (
10.0 10.0 + 0.3 * number_sum + 1.0 * len(numbers) + 0.7 * high_count - new_color_penalty
+ 0.3 * number_sum
+ 1.0 * len(numbers)
+ 0.7 * high_count
- new_color_penalty
) )
if single_late_open: if single_late_open:
return 1.5 + 0.2 * opening_value - new_color_penalty return 1.5 + 0.2 * opening_value - new_color_penalty
@@ -549,16 +532,10 @@ class SafeHeuristicBot(LostCitiesBot):
numbers = [ numbers = [
card card
for card in state.hands[player] for card in state.hands[player]
if ( if (card.color == color and not card.is_handshake and card.rank >= opening_card.rank)
card.color == color
and not card.is_handshake
and card.rank >= opening_card.rank
)
] ]
handshakes = [ handshakes = [
card card for card in state.hands[player] if card.color == color and card.is_handshake
for card in state.hands[player]
if card.color == color and card.is_handshake
] ]
number_sum = sum(self._num(state, card) for card in numbers) number_sum = sum(self._num(state, card) for card in numbers)
@@ -624,9 +601,7 @@ class SafeHeuristicBot(LostCitiesBot):
deck_left=deck_left, deck_left=deck_left,
) )
color_numbers = [ color_numbers = [
other other for other in hand if other.color == card.color and not other.is_handshake
for other in hand
if other.color == card.color and not other.is_handshake
] ]
number_sum = sum(self._num(state, other) for other in color_numbers) number_sum = sum(self._num(state, other) for other in color_numbers)
@@ -786,9 +761,7 @@ class SafeHeuristicBot(LostCitiesBot):
) )
] ]
number_sum = sum(self._num(state, other) for other in playable_numbers) number_sum = sum(self._num(state, other) for other in playable_numbers)
required_sum = ( required_sum = derived.open_target_sum * self.params.handshake_target_multiplier
derived.open_target_sum * self.params.handshake_target_multiplier
)
if ( if (
len(playable_numbers) < derived.min_handshake_numeric_cards len(playable_numbers) < derived.min_handshake_numeric_cards
or number_sum < required_sum or number_sum < required_sum
@@ -874,14 +847,10 @@ class SafeHeuristicBot(LostCitiesBot):
if other.color == color and not other.is_handshake if other.color == color and not other.is_handshake
] ]
same_color_handshakes = [ same_color_handshakes = [
other other for other in state.hands[player] if other.color == color and other.is_handshake
for other in state.hands[player]
if other.color == color and other.is_handshake
] ]
future_numbers = [ future_numbers = [
other other for other in same_color_numbers if other is not card and other.rank >= card.rank
for other in same_color_numbers
if other is not card and other.rank >= card.rank
] ]
value = 0.0 value = 0.0
@@ -936,11 +905,7 @@ class SafeHeuristicBot(LostCitiesBot):
numbers = [ numbers = [
other other
for other in state.hands[player] for other in state.hands[player]
if ( if (other.color == card.color and not other.is_handshake and other.rank >= card.rank)
other.color == card.color
and not other.is_handshake
and other.rank >= card.rank
)
] ]
numbers.append(card) numbers.append(card)
@@ -1067,9 +1032,7 @@ class SafeHeuristicBot(LostCitiesBot):
value += 0.25 * self._num(state, card) value += 0.25 * self._num(state, card)
playable_cards = [ playable_cards = [
card card for card in hand if card.color == color and state.can_play_card(player, card)
for card in hand
if card.color == color and state.can_play_card(player, card)
] ]
playable_numbers = [card for card in playable_cards if not card.is_handshake] playable_numbers = [card for card in playable_cards if not card.is_handshake]
@@ -1139,11 +1102,7 @@ class SafeHeuristicBot(LostCitiesBot):
return [ return [
card card
for card in state.hands[player] for card in state.hands[player]
if ( if (card.color == color and not card.is_handshake and state.can_play_card(player, card))
card.color == color
and not card.is_handshake
and state.can_play_card(player, card)
)
] ]
def _bonus_potential( def _bonus_potential(
@@ -34,10 +34,7 @@ class Snapshot:
return score_expedition(self.expeditions[player][color], self.config) return score_expedition(self.expeditions[player][color], self.config)
def total_score(self, player: int) -> int: def total_score(self, player: int) -> int:
return sum( return sum(self.expedition_score(player, color) for color in range(self.config.n_colors))
self.expedition_score(player, color)
for color in range(self.config.n_colors)
)
def score_diff(self, player: int = 0) -> int: def score_diff(self, player: int = 0) -> int:
return self.total_score(player) - self.total_score(1 - player) return self.total_score(player) - self.total_score(1 - player)
+13 -9
View File
@@ -1,10 +1,11 @@
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
from coolrl_lost_cities.games.classic.bots import ( from coolrl_lost_cities.games.classic.bots import (
LostCitiesBot, LostCitiesBot,
RandomBot, RandomBot,
SafeHeuristicBot, SafeHeuristicBot,
play_game, play_game,
) )
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
from coolrl_lost_cities.games.classic.bots.heuristic import draw_from_discard_action from coolrl_lost_cities.games.classic.bots.heuristic import draw_from_discard_action
@@ -162,14 +163,17 @@ def test_safe_heuristic_avoids_opening_weak_fifth_color() -> None:
state.hands[0] = [weak_open, Card(color=4, rank=7), Card(color=0, rank=6)] state.hands[0] = [weak_open, Card(color=4, rank=7), Card(color=0, rank=6)]
state.sort_hand(0) state.sort_hand(0)
assert bot._should_open_expedition( assert (
state=state, bot._should_open_expedition(
player=0, state=state,
color=4, player=0,
opening_card=weak_open, color=4,
derived=bot._derived(state), opening_card=weak_open,
deck_left=config.deck_size, derived=bot._derived(state),
) is False deck_left=config.deck_size,
)
is False
)
def test_safe_heuristic_prefers_followup_on_started_expedition() -> None: def test_safe_heuristic_prefers_followup_on_started_expedition() -> None:
+5 -16
View File
@@ -3,10 +3,9 @@ import random
from pathlib import Path from pathlib import Path
import pytest import pytest
import coolrl_lost_cities.games.classic as classic
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
import coolrl_lost_cities.games.classic as classic
FIXTURE_DIR = Path(classic.__file__).resolve().parent / "fixtures" FIXTURE_DIR = Path(classic.__file__).resolve().parent / "fixtures"
@@ -43,13 +42,9 @@ def test_new_game_from_deck_uses_explicit_internal_deck_order() -> None:
def test_snapshot_roundtrip_preserves_json_state() -> None: def test_snapshot_roundtrip_preserves_json_state() -> None:
state = GameState.new_game(LostCitiesConfig(seed=5)) state = GameState.new_game(LostCitiesConfig(seed=5))
first_action = next( first_action = next(index for index, legal in enumerate(state.unified_legal_mask()) if legal)
index for index, legal in enumerate(state.unified_legal_mask()) if legal
)
state.apply_unified_action(first_action) state.apply_unified_action(first_action)
second_action = next( second_action = next(index for index, legal in enumerate(state.unified_legal_mask()) if legal)
index for index, legal in enumerate(state.unified_legal_mask()) if legal
)
state.apply_unified_action(second_action) state.apply_unified_action(second_action)
payload = json.loads(json.dumps(state.to_snapshot())) payload = json.loads(json.dumps(state.to_snapshot()))
@@ -108,11 +103,7 @@ def test_random_games_preserve_python_core_invariants() -> None:
steps = 0 steps = 0
while not state.terminal: while not state.terminal:
state.validate_invariants() state.validate_invariants()
legal = [ legal = [index for index, is_legal in enumerate(state.unified_legal_mask()) if is_legal]
index
for index, is_legal in enumerate(state.unified_legal_mask())
if is_legal
]
state.apply_unified_action(rng.choice(legal)) state.apply_unified_action(rng.choice(legal))
steps += 1 steps += 1
assert steps < 1000 assert steps < 1000
@@ -129,8 +120,6 @@ def test_same_seed_and_action_sequence_are_deterministic() -> None:
if left.terminal: if left.terminal:
break break
action = next( action = next(index for index, is_legal in enumerate(left.unified_legal_mask()) if is_legal)
index for index, is_legal in enumerate(left.unified_legal_mask()) if is_legal
)
left.apply_unified_action(action) left.apply_unified_action(action)
right.apply_unified_action(action) right.apply_unified_action(action)
+3 -3
View File
@@ -1,7 +1,7 @@
import numpy as np import numpy as np
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
from coolrl_lost_cities.games.classic.env import LostCitiesEnv from coolrl_lost_cities.games.classic.env import LostCitiesEnv
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
def test_env_observation_uses_fixed_unified_mask() -> None: def test_env_observation_uses_fixed_unified_mask() -> None:
@@ -17,8 +17,8 @@ def test_env_observation_uses_fixed_unified_mask() -> None:
assert env.phase == "draw" assert env.phase == "draw"
assert obs["legal_mask"].shape == (config.action_size,) assert obs["legal_mask"].shape == (config.action_size,)
assert np.all(obs["legal_mask"][:config.card_action_size] == 0) assert np.all(obs["legal_mask"][: config.card_action_size] == 0)
assert np.any(obs["legal_mask"][config.card_action_size:]) assert np.any(obs["legal_mask"][config.card_action_size :])
def test_env_step_accepts_legacy_draw_action_ids() -> None: def test_env_step_accepts_legacy_draw_action_ids() -> None:
+4 -3
View File
@@ -1,6 +1,7 @@
from coolrl_lost_cities.games.classic.bots import RandomBot
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig
from coolrl_lost_cities.games.classic.bots import RandomBot
def test_legal_mask_has_action_in_nonterminal_phases() -> None: def test_legal_mask_has_action_in_nonterminal_phases() -> None:
state = GameState.new_game(LostCitiesConfig(seed=3)) state = GameState.new_game(LostCitiesConfig(seed=3))
@@ -39,8 +40,8 @@ def test_unified_legal_mask_has_fixed_shape_across_phases() -> None:
mask = state.unified_legal_mask() mask = state.unified_legal_mask()
assert state.phase == "draw" assert state.phase == "draw"
assert len(mask) == config.action_size assert len(mask) == config.action_size
assert all(value is False for value in mask[:config.card_action_size]) assert all(value is False for value in mask[: config.card_action_size])
assert any(mask[config.card_action_size:]) assert any(mask[config.card_action_size :])
def test_random_fuzz_invariants() -> None: def test_random_fuzz_invariants() -> None:
+7 -2
View File
@@ -1,6 +1,11 @@
import pytest import pytest
from coolrl_lost_cities.games.classic.game import (
from coolrl_lost_cities.games.classic.game import Card, GameState, IllegalMoveError, LostCitiesConfig, build_deck Card,
GameState,
IllegalMoveError,
LostCitiesConfig,
build_deck,
)
def test_deck_generation_count() -> None: def test_deck_generation_count() -> None:
+5 -11
View File
@@ -1,13 +1,13 @@
import json import json
from pathlib import Path
import random import random
import subprocess import subprocess
from pathlib import Path
from coolrl_lost_cities.games.classic.game import GameState, LostCitiesConfig, build_deck
import coolrl_lost_cities.games.classic as classic import coolrl_lost_cities.games.classic as classic
from coolrl_lost_cities.games.classic.game import GameState, LostCitiesConfig, build_deck
from coolrl_lost_cities.games.classic.backends.rust import RUST_CORE_DIR from coolrl_lost_cities.games.classic.backends.rust import RUST_CORE_DIR
LOST_CITIES_DIR = Path(classic.__file__).resolve().parent LOST_CITIES_DIR = Path(classic.__file__).resolve().parent
FIXTURE_DIR = LOST_CITIES_DIR / "fixtures" FIXTURE_DIR = LOST_CITIES_DIR / "fixtures"
@@ -81,11 +81,7 @@ def test_rust_randomized_fixture_traces_match_python_core(tmp_path: Path) -> Non
for _ in range(64): for _ in range(64):
if state.terminal: if state.terminal:
break break
legal = [ legal = [index for index, is_legal in enumerate(state.unified_legal_mask()) if is_legal]
index
for index, is_legal in enumerate(state.unified_legal_mask())
if is_legal
]
action = rng.choice(legal) action = rng.choice(legal)
state.apply_unified_action(action) state.apply_unified_action(action)
steps.append({"action": action}) steps.append({"action": action})
@@ -154,9 +150,7 @@ def test_rust_grpc_contract_is_checked_from_python() -> None:
def test_rust_core_has_no_native_tests_left() -> None: def test_rust_core_has_no_native_tests_left() -> None:
rust_files = [ rust_files = [
path path for path in (RUST_CORE_DIR / "src").rglob("*.rs") if "target" not in path.parts
for path in (RUST_CORE_DIR / "src").rglob("*.rs")
if "target" not in path.parts
] ]
for path in rust_files: for path in rust_files:
text = path.read_text() text = path.read_text()
Generated
+182
View File
@@ -2,6 +2,15 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.11" requires-python = ">=3.11"
[[package]]
name = "cfgv"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@@ -28,7 +37,9 @@ gui = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "cython" }, { name = "cython" },
{ name = "pre-commit" },
{ name = "pytest" }, { name = "pytest" },
{ name = "ruff" },
] ]
[package.metadata] [package.metadata]
@@ -42,7 +53,9 @@ provides-extras = ["gui"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "cython", specifier = ">=3.0" }, { name = "cython", specifier = ">=3.0" },
{ name = "pre-commit", specifier = ">=4.0.0" },
{ name = "pytest", specifier = ">=9.0.3" }, { name = "pytest", specifier = ">=9.0.3" },
{ name = "ruff", specifier = ">=0.8.0" },
] ]
[[package]] [[package]]
@@ -79,6 +92,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" }, { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" },
] ]
[[package]]
name = "distlib"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
[[package]]
name = "filelock"
version = "3.29.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
]
[[package]]
name = "identify"
version = "2.6.19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.3.0" version = "2.3.0"
@@ -88,6 +128,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
] ]
[[package]]
name = "nodeenv"
version = "1.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
]
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.4.4" version = "2.4.4"
@@ -176,6 +225,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
] ]
[[package]]
name = "platformdirs"
version = "4.9.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" version = "1.6.0"
@@ -185,6 +243,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
] ]
[[package]]
name = "pre-commit"
version = "4.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
{ name = "identify" },
{ name = "nodeenv" },
{ name = "pyyaml" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
]
[[package]] [[package]]
name = "pygame" name = "pygame"
version = "2.6.1" version = "2.6.1"
@@ -291,6 +365,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
] ]
[[package]]
name = "python-discovery"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f", size = 33124, upload-time = "2026-05-05T14:38:38.539Z" },
]
[[package]] [[package]]
name = "python-i18n" name = "python-i18n"
version = "0.3.9" version = "0.3.9"
@@ -299,3 +386,98 @@ sdist = { url = "https://files.pythonhosted.org/packages/fe/32/d9ba976458c9503ec
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/73/9a0b2974dd9a3d50788d235f10c4d73c2efcd22926036309645fc2f0db0c/python_i18n-0.3.9-py3-none-any.whl", hash = "sha256:bda5b8d889ebd51973e22e53746417bd32783c9bd6780fd27cadbb733915651d", size = 13750, upload-time = "2020-08-26T14:31:26.266Z" }, { url = "https://files.pythonhosted.org/packages/5a/73/9a0b2974dd9a3d50788d235f10c4d73c2efcd22926036309645fc2f0db0c/python_i18n-0.3.9-py3-none-any.whl", hash = "sha256:bda5b8d889ebd51973e22e53746417bd32783c9bd6780fd27cadbb733915651d", size = 13750, upload-time = "2020-08-26T14:31:26.266Z" },
] ]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "ruff"
version = "0.15.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" },
{ url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" },
{ url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" },
{ url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" },
{ url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" },
{ url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" },
{ url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" },
{ url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" },
{ url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" },
{ url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" },
{ url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" },
{ url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" },
{ url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" },
{ url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" },
{ url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" },
{ url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" },
]
[[package]]
name = "virtualenv"
version = "21.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "platformdirs" },
{ name = "python-discovery" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" },
]