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 run запускаем.
### Линтер и типы
- **ruff** — линтер и форматтер, line-length 88
- **ty** — проверка типов
- Перед коммитом: `uv run ruff check --fix . && uv run ruff format . && uv run ty check .`
### Type hints
Используй современный синтаксис type hints:
- `list[]`, `dict[]`, `tuple[]`, `set[]` вместо List, Dict, etc.

View File

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

View File

@@ -8,3 +8,24 @@ dependencies = [
"python-dotenv>=1.2.1",
"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
from collections.abc import Generator
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Generator
from src.config import DB_PATH
from src.logger import logger
@@ -171,7 +171,8 @@ def get_or_create_user(telegram_id: int) -> User:
)
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,),
)
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:
"""Создаёт реплики для сценария. replicas: [(speaker_id, replica_index, text), ...]"""
"""Создаёт реплики. replicas: [(speaker_id, replica_index, text), ...]"""
with get_connection() as conn:
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)
for speaker_id, idx, text in replicas
@@ -300,7 +302,8 @@ def get_track_speaker_ids(scenario_id: str) -> list[int]:
"""Получает список speaker_id (дорожек) в сценарии."""
with get_connection() as conn:
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,),
)
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:
cursor = conn.execute(
"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),
)
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:
cursor = conn.execute(
"SELECT id, user_id, scenario_id, replica_index, created_at FROM recordings "
"WHERE user_id = ? AND scenario_id = ? ORDER BY replica_index",
"SELECT id, user_id, scenario_id, replica_index, created_at "
"FROM recordings WHERE user_id = ? AND scenario_id = ? "
"ORDER BY replica_index",
(user_id, scenario_id),
)
return [

View File

@@ -1,5 +1,5 @@
from collections.abc import Callable
from functools import wraps
from typing import Callable
from telegram import Update
from telegram.ext import ContextTypes
@@ -69,5 +69,3 @@ def require_state(*states: UserState):
return wrapper
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))
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:
"""Форматирует статистику для админки."""
stats = get_stats()
@@ -314,8 +323,7 @@ async def handle_rerecord_previous(
replica_text = get_current_replica_text(session)
await query.edit_message_text(f"{FIRST_REPLICA_INSTRUCTIONS}\n\n{replica_text}")
else:
replica_text = get_current_replica_text(session)
text = f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
text = format_replica_message(session)
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
session.last_bot_message_id = query.message.message_id
@@ -371,10 +379,7 @@ async def handle_cancel_restart(
"""Обработчик отмены рестарта."""
query = update.callback_query
session.state = UserState.SHOW_REPLICA
replica_text = get_current_replica_text(session)
text = (
f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
)
text = format_replica_message(session)
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
session.last_bot_message_id = query.message.message_id
upsert_user_session(session)
@@ -430,10 +435,7 @@ async def handle_rerecord_last(
track_length = get_track_length(session.scenario_id, session.speaker_id)
session.state = UserState.SHOW_REPLICA
session.replica_index = track_length - 1
replica_text = get_current_replica_text(session)
text = (
f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
)
text = format_replica_message(session)
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
session.last_bot_message_id = query.message.message_id
upsert_user_session(session)
@@ -497,8 +499,7 @@ async def handle_exit_admin(
replica_text = get_current_replica_text(session)
await query.edit_message_text(f"{FIRST_REPLICA_INSTRUCTIONS}\n\n{replica_text}")
elif session.state == UserState.SHOW_REPLICA:
replica_text = get_current_replica_text(session)
text = f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
text = format_replica_message(session)
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
elif session.state == UserState.CONFIRM_SAVE:
await query.edit_message_text(
@@ -547,10 +548,8 @@ async def handle_confirm_upload(
"SELECT telegram_id FROM users WHERE id = ?", (waiting_user_id,)
).fetchone()
if row:
await context.bot.send_message(
row[0],
"🎉 Появился новый сценарий для озвучивания! Используйте /start для продолжения.",
)
msg = "🎉 Появился новый сценарий! Используйте /start"
await context.bot.send_message(row[0], msg)
except Exception:
pass
@@ -619,8 +618,7 @@ async def handle_voice_message(
)
else:
session.state = UserState.SHOW_REPLICA
replica_text = get_current_replica_text(session)
text = f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
text = format_replica_message(session)
msg_id = await send_message_and_save(
update, context, session, text, get_show_replica_keyboard()
)
@@ -631,7 +629,10 @@ async def handle_voice_message(
@with_user_and_session
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:
"""Обработчик ввода номера реплики."""
if not session or session.state != UserState.ASK_REPLICA_NUMBER:
@@ -715,14 +716,21 @@ VOICE_EXPECTED_TEXT = "❌ Пожалуйста, отправьте голосо
@with_user_and_session
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:
"""Обработчик неожиданных текстовых сообщений."""
if not session:
await update.message.reply_text("Используйте /start для начала работы с ботом.")
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:
await update.message.reply_text(VOICE_EXPECTED_TEXT)
# В других состояниях игнорируем текст (например, INTRO, NO_MORE_SCENARIOS)

View File

@@ -9,7 +9,6 @@ from src.database import (
get_connection,
get_replicas_for_track,
get_scenario,
get_track_speaker_ids,
)
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]:
# Пользователь уже записывает эту дорожку — пропускаем
continue
# Пользователь записывает другую дорожку в этом сценарии — пропускаем весь сценарий
# Пользователь записывает другую дорожку в этом сценарии
continue
if key not in track_recordings:

62
uv.lock generated
View File

@@ -37,12 +37,24 @@ dependencies = [
{ name = "python-telegram-bot" },
]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
{ name = "ty" },
]
[package.metadata]
requires-dist = [
{ name = "python-dotenv", specifier = ">=1.2.1" },
{ 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]]
name = "h11"
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" },
]
[[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]]
name = "typing-extensions"
version = "4.15.0"