diff --git a/AGENTS.md b/AGENTS.md index aad281c..2b3a553 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/main.py b/main.py index 463dac2..65a86ff 100644 --- a/main.py +++ b/main.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml index 564e2a8..75d9ed5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/database.py b/src/database.py index 371a67b..9484611 100644 --- a/src/database.py +++ b/src/database.py @@ -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 [ diff --git a/src/decorators.py b/src/decorators.py index 34b2d78..8f78bea 100644 --- a/src/decorators.py +++ b/src/decorators.py @@ -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 - - diff --git a/src/handlers.py b/src/handlers.py index d8c17b9..8e3c04b 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -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) diff --git a/src/scenarios.py b/src/scenarios.py index 9aa1308..07e0679 100644 --- a/src/scenarios.py +++ b/src/scenarios.py @@ -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: diff --git a/uv.lock b/uv.lock index 18c85c4..dec0f26 100644 --- a/uv.lock +++ b/uv.lock @@ -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"