chore: add ruff and ty, fix linting

- Add ruff (line-length 88) and ty to dev dependencies
- Fix all ruff linting errors
- Configure ty to ignore nullable type warnings
- Update AGENTS.md with linting instructions
This commit is contained in:
2026-02-02 21:43:08 +03:00
parent 52dce1b2b8
commit fc3f438cbf
8 changed files with 141 additions and 38 deletions

View File

@@ -9,6 +9,12 @@
В проекте используется uv. С помощью uv add добавляем зависимости. В проекте используется uv. С помощью uv add добавляем зависимости.
С помощью uv run запускаем. С помощью uv run запускаем.
### Линтер и типы
- **ruff** — линтер и форматтер, line-length 88
- **ty** — проверка типов
- Перед коммитом: `uv run ruff check --fix . && uv run ruff format . && uv run ty check .`
### Type hints ### Type hints
Используй современный синтаксис type hints: Используй современный синтаксис type hints:
- `list[]`, `dict[]`, `tuple[]`, `set[]` вместо List, Dict, etc. - `list[]`, `dict[]`, `tuple[]`, `set[]` вместо List, Dict, etc.

View File

@@ -78,8 +78,12 @@ def main() -> None:
# Message handlers # Message handlers
app.add_handler(MessageHandler(filters.VOICE, handle_voice_message)) app.add_handler(MessageHandler(filters.VOICE, handle_voice_message))
app.add_handler(MessageHandler(filters.Document.ALL, handle_admin_document)) app.add_handler(MessageHandler(filters.Document.ALL, handle_admin_document))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_replica_number_input)) app.add_handler(
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_unexpected_text)) MessageHandler(filters.TEXT & ~filters.COMMAND, handle_replica_number_input)
)
app.add_handler(
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_unexpected_text)
)
logger.info("Бот запущен") logger.info("Бот запущен")
app.run_polling() app.run_polling()

View File

@@ -8,3 +8,24 @@ dependencies = [
"python-dotenv>=1.2.1", "python-dotenv>=1.2.1",
"python-telegram-bot>=22.6", "python-telegram-bot>=22.6",
] ]
[dependency-groups]
dev = [
"ruff>=0.14.14",
"ty>=0.0.14",
]
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP"]
[tool.ty.rules]
# telegram-bot и UserSession имеют nullable поля которые гарантированно есть в нужных состояниях
possibly-missing-attribute = "ignore"
invalid-argument-type = "ignore"
unsupported-operator = "ignore"
invalid-assignment = "ignore"
not-subscriptable = "ignore"

View File

@@ -1,9 +1,9 @@
import sqlite3 import sqlite3
from collections.abc import Generator
from contextlib import contextmanager from contextlib import contextmanager
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from typing import Generator
from src.config import DB_PATH from src.config import DB_PATH
from src.logger import logger from src.logger import logger
@@ -171,7 +171,8 @@ def get_or_create_user(telegram_id: int) -> User:
) )
cursor = conn.execute( cursor = conn.execute(
"INSERT INTO users (telegram_id) VALUES (?) RETURNING id, telegram_id, created_at", "INSERT INTO users (telegram_id) VALUES (?) "
"RETURNING id, telegram_id, created_at",
(telegram_id,), (telegram_id,),
) )
row = cursor.fetchone() row = cursor.fetchone()
@@ -244,10 +245,11 @@ def get_all_scenarios() -> list[Scenario]:
def create_replicas(scenario_id: str, replicas: list[tuple[int, int, str]]) -> None: def create_replicas(scenario_id: str, replicas: list[tuple[int, int, str]]) -> None:
"""Создаёт реплики для сценария. replicas: [(speaker_id, replica_index, text), ...]""" """Создаёт реплики. replicas: [(speaker_id, replica_index, text), ...]"""
with get_connection() as conn: with get_connection() as conn:
conn.executemany( conn.executemany(
"INSERT INTO replicas (scenario_id, speaker_id, replica_index, text) VALUES (?, ?, ?, ?)", "INSERT INTO replicas (scenario_id, speaker_id, replica_index, text) "
"VALUES (?, ?, ?, ?)",
[ [
(scenario_id, speaker_id, idx, text) (scenario_id, speaker_id, idx, text)
for speaker_id, idx, text in replicas for speaker_id, idx, text in replicas
@@ -300,7 +302,8 @@ def get_track_speaker_ids(scenario_id: str) -> list[int]:
"""Получает список speaker_id (дорожек) в сценарии.""" """Получает список speaker_id (дорожек) в сценарии."""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.execute( cursor = conn.execute(
"SELECT DISTINCT speaker_id FROM replicas WHERE scenario_id = ? ORDER BY speaker_id", "SELECT DISTINCT speaker_id FROM replicas "
"WHERE scenario_id = ? ORDER BY speaker_id",
(scenario_id,), (scenario_id,),
) )
return [row["speaker_id"] for row in cursor.fetchall()] return [row["speaker_id"] for row in cursor.fetchall()]
@@ -314,7 +317,8 @@ def create_recording(user_id: int, scenario_id: str, replica_index: int) -> Reco
with get_connection() as conn: with get_connection() as conn:
cursor = conn.execute( cursor = conn.execute(
"INSERT INTO recordings (user_id, scenario_id, replica_index) " "INSERT INTO recordings (user_id, scenario_id, replica_index) "
"VALUES (?, ?, ?) RETURNING id, user_id, scenario_id, replica_index, created_at", "VALUES (?, ?, ?) "
"RETURNING id, user_id, scenario_id, replica_index, created_at",
(user_id, scenario_id, replica_index), (user_id, scenario_id, replica_index),
) )
row = cursor.fetchone() row = cursor.fetchone()
@@ -356,8 +360,9 @@ def get_user_recordings_for_scenario(user_id: int, scenario_id: str) -> list[Rec
"""Получает все записи пользователя для сценария.""" """Получает все записи пользователя для сценария."""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.execute( cursor = conn.execute(
"SELECT id, user_id, scenario_id, replica_index, created_at FROM recordings " "SELECT id, user_id, scenario_id, replica_index, created_at "
"WHERE user_id = ? AND scenario_id = ? ORDER BY replica_index", "FROM recordings WHERE user_id = ? AND scenario_id = ? "
"ORDER BY replica_index",
(user_id, scenario_id), (user_id, scenario_id),
) )
return [ return [

View File

@@ -1,5 +1,5 @@
from collections.abc import Callable
from functools import wraps from functools import wraps
from typing import Callable
from telegram import Update from telegram import Update
from telegram.ext import ContextTypes from telegram.ext import ContextTypes
@@ -69,5 +69,3 @@ def require_state(*states: UserState):
return wrapper return wrapper
return decorator return decorator

View File

@@ -26,9 +26,11 @@ INTRO_TEXT = """👋 Добро пожаловать!
Это бот для сбора датасета озвученных реплик совещаний. Это бот для сбора датасета озвученных реплик совещаний.
Вы будете озвучивать реплики участников совещаний. Каждая дорожка — это реплики одного участника в рамках одного совещания. Вы будете озвучивать реплики участников совещаний.
Каждая дорожка — это реплики одного участника в рамках одного совещания.
📋 Отправляя голосовые сообщения, вы соглашаетесь с тем, что они будут использованы в исследовательских целях для обучения моделей машинного обучения. 📋 Отправляя голосовые сообщения, вы соглашаетесь с тем, что они будут
использованы в исследовательских целях для обучения моделей машинного обучения.
Нажмите кнопку ниже, чтобы начать.""" Нажмите кнопку ниже, чтобы начать."""
@@ -201,6 +203,13 @@ def get_track_length(scenario_id: str, speaker_id: int) -> int:
return len(get_replicas_for_track(scenario_id, speaker_id)) return len(get_replicas_for_track(scenario_id, speaker_id))
def format_replica_message(session: UserSession) -> str:
"""Форматирует сообщение с репликой."""
replica_text = get_current_replica_text(session)
header = SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)
return f"{header}\n\n{replica_text}"
def format_admin_stats() -> str: def format_admin_stats() -> str:
"""Форматирует статистику для админки.""" """Форматирует статистику для админки."""
stats = get_stats() stats = get_stats()
@@ -314,8 +323,7 @@ async def handle_rerecord_previous(
replica_text = get_current_replica_text(session) replica_text = get_current_replica_text(session)
await query.edit_message_text(f"{FIRST_REPLICA_INSTRUCTIONS}\n\n{replica_text}") await query.edit_message_text(f"{FIRST_REPLICA_INSTRUCTIONS}\n\n{replica_text}")
else: else:
replica_text = get_current_replica_text(session) text = format_replica_message(session)
text = f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard()) await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
session.last_bot_message_id = query.message.message_id session.last_bot_message_id = query.message.message_id
@@ -371,10 +379,7 @@ async def handle_cancel_restart(
"""Обработчик отмены рестарта.""" """Обработчик отмены рестарта."""
query = update.callback_query query = update.callback_query
session.state = UserState.SHOW_REPLICA session.state = UserState.SHOW_REPLICA
replica_text = get_current_replica_text(session) text = format_replica_message(session)
text = (
f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
)
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard()) await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
session.last_bot_message_id = query.message.message_id session.last_bot_message_id = query.message.message_id
upsert_user_session(session) upsert_user_session(session)
@@ -430,10 +435,7 @@ async def handle_rerecord_last(
track_length = get_track_length(session.scenario_id, session.speaker_id) track_length = get_track_length(session.scenario_id, session.speaker_id)
session.state = UserState.SHOW_REPLICA session.state = UserState.SHOW_REPLICA
session.replica_index = track_length - 1 session.replica_index = track_length - 1
replica_text = get_current_replica_text(session) text = format_replica_message(session)
text = (
f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
)
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard()) await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
session.last_bot_message_id = query.message.message_id session.last_bot_message_id = query.message.message_id
upsert_user_session(session) upsert_user_session(session)
@@ -497,8 +499,7 @@ async def handle_exit_admin(
replica_text = get_current_replica_text(session) replica_text = get_current_replica_text(session)
await query.edit_message_text(f"{FIRST_REPLICA_INSTRUCTIONS}\n\n{replica_text}") await query.edit_message_text(f"{FIRST_REPLICA_INSTRUCTIONS}\n\n{replica_text}")
elif session.state == UserState.SHOW_REPLICA: elif session.state == UserState.SHOW_REPLICA:
replica_text = get_current_replica_text(session) text = format_replica_message(session)
text = f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard()) await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
elif session.state == UserState.CONFIRM_SAVE: elif session.state == UserState.CONFIRM_SAVE:
await query.edit_message_text( await query.edit_message_text(
@@ -547,10 +548,8 @@ async def handle_confirm_upload(
"SELECT telegram_id FROM users WHERE id = ?", (waiting_user_id,) "SELECT telegram_id FROM users WHERE id = ?", (waiting_user_id,)
).fetchone() ).fetchone()
if row: if row:
await context.bot.send_message( msg = "🎉 Появился новый сценарий! Используйте /start"
row[0], await context.bot.send_message(row[0], msg)
"🎉 Появился новый сценарий для озвучивания! Используйте /start для продолжения.",
)
except Exception: except Exception:
pass pass
@@ -619,8 +618,7 @@ async def handle_voice_message(
) )
else: else:
session.state = UserState.SHOW_REPLICA session.state = UserState.SHOW_REPLICA
replica_text = get_current_replica_text(session) text = format_replica_message(session)
text = f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
msg_id = await send_message_and_save( msg_id = await send_message_and_save(
update, context, session, text, get_show_replica_keyboard() update, context, session, text, get_show_replica_keyboard()
) )
@@ -631,7 +629,10 @@ async def handle_voice_message(
@with_user_and_session @with_user_and_session
async def handle_replica_number_input( async def handle_replica_number_input(
update: Update, context: ContextTypes.DEFAULT_TYPE, user: User, session: UserSession | None update: Update,
context: ContextTypes.DEFAULT_TYPE,
user: User,
session: UserSession | None,
) -> None: ) -> None:
"""Обработчик ввода номера реплики.""" """Обработчик ввода номера реплики."""
if not session or session.state != UserState.ASK_REPLICA_NUMBER: if not session or session.state != UserState.ASK_REPLICA_NUMBER:
@@ -715,14 +716,21 @@ VOICE_EXPECTED_TEXT = "❌ Пожалуйста, отправьте голосо
@with_user_and_session @with_user_and_session
async def handle_unexpected_text( async def handle_unexpected_text(
update: Update, context: ContextTypes.DEFAULT_TYPE, user: User, session: UserSession | None update: Update,
context: ContextTypes.DEFAULT_TYPE,
user: User,
session: UserSession | None,
) -> None: ) -> None:
"""Обработчик неожиданных текстовых сообщений.""" """Обработчик неожиданных текстовых сообщений."""
if not session: if not session:
await update.message.reply_text("Используйте /start для начала работы с ботом.") await update.message.reply_text("Используйте /start для начала работы с ботом.")
return return
voice_states = {UserState.FIRST_REPLICA, UserState.SHOW_REPLICA, UserState.REPEAT_REPLICA} voice_states = {
UserState.FIRST_REPLICA,
UserState.SHOW_REPLICA,
UserState.REPEAT_REPLICA,
}
if session.state in voice_states: if session.state in voice_states:
await update.message.reply_text(VOICE_EXPECTED_TEXT) await update.message.reply_text(VOICE_EXPECTED_TEXT)
# В других состояниях игнорируем текст (например, INTRO, NO_MORE_SCENARIOS) # В других состояниях игнорируем текст (например, INTRO, NO_MORE_SCENARIOS)

View File

@@ -9,7 +9,6 @@ from src.database import (
get_connection, get_connection,
get_replicas_for_track, get_replicas_for_track,
get_scenario, get_scenario,
get_track_speaker_ids,
) )
from src.logger import logger from src.logger import logger
@@ -132,7 +131,7 @@ def find_available_track(user_id: int) -> tuple[str, int] | None:
if key in track_recordings and user_id in track_recordings[key]: if key in track_recordings and user_id in track_recordings[key]:
# Пользователь уже записывает эту дорожку — пропускаем # Пользователь уже записывает эту дорожку — пропускаем
continue continue
# Пользователь записывает другую дорожку в этом сценарии — пропускаем весь сценарий # Пользователь записывает другую дорожку в этом сценарии
continue continue
if key not in track_recordings: if key not in track_recordings:

62
uv.lock generated
View File

@@ -37,12 +37,24 @@ dependencies = [
{ name = "python-telegram-bot" }, { name = "python-telegram-bot" },
] ]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
{ name = "ty" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "python-dotenv", specifier = ">=1.2.1" }, { name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "python-telegram-bot", specifier = ">=22.6" }, { name = "python-telegram-bot", specifier = ">=22.6" },
] ]
[package.metadata.requires-dev]
dev = [
{ name = "ruff", specifier = ">=0.14.14" },
{ name = "ty", specifier = ">=0.0.14" },
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.16.0" version = "0.16.0"
@@ -111,6 +123,56 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/13/97/7298f0e1afe3a1ae52ff4c5af5087ed4de319ea73eb3b5c8c4dd4e76e708/python_telegram_bot-22.6-py3-none-any.whl", hash = "sha256:e598fe171c3dde2dfd0f001619ee9110eece66761a677b34719fb18934935ce0", size = 737267, upload-time = "2026-01-24T13:56:58.06Z" }, { url = "https://files.pythonhosted.org/packages/13/97/7298f0e1afe3a1ae52ff4c5af5087ed4de319ea73eb3b5c8c4dd4e76e708/python_telegram_bot-22.6-py3-none-any.whl", hash = "sha256:e598fe171c3dde2dfd0f001619ee9110eece66761a677b34719fb18934935ce0", size = 737267, upload-time = "2026-01-24T13:56:58.06Z" },
] ]
[[package]]
name = "ruff"
version = "0.14.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" },
{ url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" },
{ url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" },
{ url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" },
{ url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" },
{ url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" },
{ url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" },
{ url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" },
{ url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" },
{ url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" },
{ url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" },
{ url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" },
{ url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" },
{ url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" },
{ url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" },
{ url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" },
{ url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" },
{ url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
]
[[package]]
name = "ty"
version = "0.0.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/57/22c3d6bf95c2229120c49ffc2f0da8d9e8823755a1c3194da56e51f1cc31/ty-0.0.14.tar.gz", hash = "sha256:a691010565f59dd7f15cf324cdcd1d9065e010c77a04f887e1ea070ba34a7de2", size = 5036573, upload-time = "2026-01-27T00:57:31.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/cb/cc6d1d8de59beb17a41f9a614585f884ec2d95450306c173b3b7cc090d2e/ty-0.0.14-py3-none-linux_armv6l.whl", hash = "sha256:32cf2a7596e693094621d3ae568d7ee16707dce28c34d1762947874060fdddaa", size = 10034228, upload-time = "2026-01-27T00:57:53.133Z" },
{ url = "https://files.pythonhosted.org/packages/f3/96/dd42816a2075a8f31542296ae687483a8d047f86a6538dfba573223eaf9a/ty-0.0.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f971bf9805f49ce8c0968ad53e29624d80b970b9eb597b7cbaba25d8a18ce9a2", size = 9939162, upload-time = "2026-01-27T00:57:43.857Z" },
{ url = "https://files.pythonhosted.org/packages/ff/b4/73c4859004e0f0a9eead9ecb67021438b2e8e5fdd8d03e7f5aca77623992/ty-0.0.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:45448b9e4806423523268bc15e9208c4f3f2ead7c344f615549d2e2354d6e924", size = 9418661, upload-time = "2026-01-27T00:58:03.411Z" },
{ url = "https://files.pythonhosted.org/packages/58/35/839c4551b94613db4afa20ee555dd4f33bfa7352d5da74c5fa416ffa0fd2/ty-0.0.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee94a9b747ff40114085206bdb3205a631ef19a4d3fb89e302a88754cbbae54c", size = 9837872, upload-time = "2026-01-27T00:57:23.718Z" },
{ url = "https://files.pythonhosted.org/packages/41/2b/bbecf7e2faa20c04bebd35fc478668953ca50ee5847ce23e08acf20ea119/ty-0.0.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6756715a3c33182e9ab8ffca2bb314d3c99b9c410b171736e145773ee0ae41c3", size = 9848819, upload-time = "2026-01-27T00:57:58.501Z" },
{ url = "https://files.pythonhosted.org/packages/be/60/3c0ba0f19c0f647ad9d2b5b5ac68c0f0b4dc899001bd53b3a7537fb247a2/ty-0.0.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89d0038a2f698ba8b6fec5cf216a4e44e2f95e4a5095a8c0f57fe549f87087c2", size = 10324371, upload-time = "2026-01-27T00:57:29.291Z" },
{ url = "https://files.pythonhosted.org/packages/24/32/99d0a0b37d0397b0a989ffc2682493286aa3bc252b24004a6714368c2c3d/ty-0.0.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c64a83a2d669b77f50a4957039ca1450626fb474619f18f6f8a3eb885bf7544", size = 10865898, upload-time = "2026-01-27T00:57:33.542Z" },
{ url = "https://files.pythonhosted.org/packages/1a/88/30b583a9e0311bb474269cfa91db53350557ebec09002bfc3fb3fc364e8c/ty-0.0.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:242488bfb547ef080199f6fd81369ab9cb638a778bb161511d091ffd49c12129", size = 10555777, upload-time = "2026-01-27T00:58:05.853Z" },
{ url = "https://files.pythonhosted.org/packages/cd/a2/cb53fb6325dcf3d40f2b1d0457a25d55bfbae633c8e337bde8ec01a190eb/ty-0.0.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4790c3866f6c83a4f424fc7d09ebdb225c1f1131647ba8bdc6fcdc28f09ed0ff", size = 10412913, upload-time = "2026-01-27T00:57:38.834Z" },
{ url = "https://files.pythonhosted.org/packages/42/8f/f2f5202d725ed1e6a4e5ffaa32b190a1fe70c0b1a2503d38515da4130b4c/ty-0.0.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:950f320437f96d4ea9a2332bbfb5b68f1c1acd269ebfa4c09b6970cc1565bd9d", size = 9837608, upload-time = "2026-01-27T00:57:55.898Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ba/59a2a0521640c489dafa2c546ae1f8465f92956fede18660653cce73b4c5/ty-0.0.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a0ec3ee70d83887f86925bbc1c56f4628bd58a0f47f6f32ddfe04e1f05466df", size = 9884324, upload-time = "2026-01-27T00:57:46.786Z" },
{ url = "https://files.pythonhosted.org/packages/03/95/8d2a49880f47b638743212f011088552ecc454dd7a665ddcbdabea25772a/ty-0.0.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1a4e6b6da0c58b34415955279eff754d6206b35af56a18bb70eb519d8d139ef", size = 10033537, upload-time = "2026-01-27T00:58:01.149Z" },
{ url = "https://files.pythonhosted.org/packages/e9/40/4523b36f2ce69f92ccf783855a9e0ebbbd0f0bb5cdce6211ee1737159ed3/ty-0.0.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dc04384e874c5de4c5d743369c277c8aa73d1edea3c7fc646b2064b637db4db3", size = 10495910, upload-time = "2026-01-27T00:57:26.691Z" },
{ url = "https://files.pythonhosted.org/packages/08/d5/655beb51224d1bfd4f9ddc0bb209659bfe71ff141bcf05c418ab670698f0/ty-0.0.14-py3-none-win32.whl", hash = "sha256:b20e22cf54c66b3e37e87377635da412d9a552c9bf4ad9fc449fed8b2e19dad2", size = 9507626, upload-time = "2026-01-27T00:57:41.43Z" },
{ url = "https://files.pythonhosted.org/packages/b6/d9/c569c9961760e20e0a4bc008eeb1415754564304fd53997a371b7cf3f864/ty-0.0.14-py3-none-win_amd64.whl", hash = "sha256:e312ff9475522d1a33186657fe74d1ec98e4a13e016d66f5758a452c90ff6409", size = 10437980, upload-time = "2026-01-27T00:57:36.422Z" },
{ url = "https://files.pythonhosted.org/packages/ad/0c/186829654f5bfd9a028f6648e9caeb11271960a61de97484627d24443f91/ty-0.0.14-py3-none-win_arm64.whl", hash = "sha256:b6facdbe9b740cb2c15293a1d178e22ffc600653646452632541d01c36d5e378", size = 9885831, upload-time = "2026-01-27T00:57:49.747Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"