From 1009845d31bde7264be5ba779873cac6ac92957e Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 3 Jan 2026 15:19:55 +0300 Subject: [PATCH] commit --- pyproject.toml | 1 + .../bot/admin_dialogs/broadcast.py | 2 +- .../application/bot/admin_dialogs/tests.py | 58 +++++--------- .../bot/creator_dialogs/broadcast.py | 9 ++- .../application/bot/creator_dialogs/tests.py | 13 +++- src/trudex/infrastructure/utils/config.py | 8 +- .../infrastructure/utils/test_id_to_hash.py | 77 +++++++++++++++---- uv.lock | 32 ++++++++ 8 files changed, 131 insertions(+), 69 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 07ba0d3..cd614cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "apscheduler>=3.10.4", "pydantic>=2.10.5", "qrcode[pil]>=8.2", + "pycryptodome>=3.23.0", ] [dependency-groups] diff --git a/src/trudex/application/bot/admin_dialogs/broadcast.py b/src/trudex/application/bot/admin_dialogs/broadcast.py index 937ccfd..7f49806 100644 --- a/src/trudex/application/bot/admin_dialogs/broadcast.py +++ b/src/trudex/application/bot/admin_dialogs/broadcast.py @@ -49,7 +49,7 @@ async def on_broadcast_confirm(_callback: CallbackQuery, _button: Button, manage async def on_broadcast_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager): await _callback.answer("Рассылка отменена") - await manager.done() + await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK) async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager): diff --git a/src/trudex/application/bot/admin_dialogs/tests.py b/src/trudex/application/bot/admin_dialogs/tests.py index 6771860..2c07051 100644 --- a/src/trudex/application/bot/admin_dialogs/tests.py +++ b/src/trudex/application/bot/admin_dialogs/tests.py @@ -3,13 +3,11 @@ import functools from datetime import date, datetime from aiogram import Bot -from aiogram.enums import ContentType from aiogram.types import BufferedInputFile, CallbackQuery, Message from aiogram_dialog import Dialog, DialogManager, StartMode, Window from aiogram_dialog.widgets.input import MessageInput -from aiogram_dialog.widgets.kbd import (Button, Calendar, Column, Row, +from aiogram_dialog.widgets.kbd import (Button, Calendar, Column, ScrollingGroup, Select) -from aiogram_dialog.widgets.media import DynamicMedia from aiogram_dialog.widgets.text import Const, Format from dishka import FromDishka from dishka.integrations.aiogram_dialog import inject @@ -21,7 +19,7 @@ from trudex.infrastructure.database.dao.test import TestDAO from trudex.infrastructure.database.repo.test import TestRepository from trudex.infrastructure.utils.config import Config from trudex.infrastructure.utils.qr_generator import generate_qr_bytes -from trudex.infrastructure.utils.test_id_to_hash import generate_alpha_id +from trudex.infrastructure.utils.test_id_to_hash import encode_id @inject @@ -111,54 +109,40 @@ async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: Di await manager.switch_to(AdminTestsSG.tests_list) -async def on_share_test(_callback: CallbackQuery, _button: Button, manager: DialogManager): - await manager.switch_to(AdminTestsSG.share_test) +async def on_statistics(_callback: CallbackQuery, _button: Button, _manager: DialogManager): + await _callback.answer("🚧 В разработке") @inject -async def get_share_data(dialog_manager: DialogManager, config: FromDishka[Config], bot: FromDishka[Bot], **_kwargs): - test_id = dialog_manager.dialog_data.get("selected_test_id") +async def on_share_test(_callback: CallbackQuery, _button: Button, manager: DialogManager, config: FromDishka[Config], bot_inst: FromDishka[Bot]): + test_id = manager.dialog_data.get("selected_test_id") if not test_id: - return { - "share_link": "Ошибка: тест не найден" - } + await _callback.answer("Ошибка: тест не найден") + return - # Генерируем хэш и ссылку - test_hash = generate_alpha_id( + test_hash = encode_id( test_id, - config.security.test_hash_salt, - config.security.test_hash_length + config.security.encode_key, + config.security.encoded_string_length ) - bot_info = await bot.get_me() + bot_info = await bot_inst.get_me() bot_username = bot_info.username or "your_bot" share_link = f"https://t.me/{bot_username}?start={test_hash}" - # Генерируем QR-код в отдельном потоке loop = asyncio.get_running_loop() qr_bytes = await loop.run_in_executor( None, functools.partial(generate_qr_bytes, share_link) ) - - # Сохраняем в dialog_data для использования в media selector - dialog_manager.dialog_data["qr_bytes"] = qr_bytes - - return { - "share_link": share_link, - } + assert _callback.message is not None -async def qr_media_selector(data: dict, widget, manager: DialogManager): - """Селектор для получения QR-кода из dialog_data""" - qr_bytes = manager.dialog_data.get("qr_bytes") - if not qr_bytes: - return None - return { - "type": ContentType.PHOTO, - "media": BufferedInputFile(qr_bytes, filename="qr.png") - } + await _callback.message.answer_photo( + photo=BufferedInputFile(qr_bytes, filename="qr.png"), + caption=f"🔗 Поделиться тестом\n\n📎 Ссылка на тест:\n{share_link}\n\n💡 Отправьте эту ссылку или QR-код пользователям для прохождения теста" + ) async def on_edit_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager): @@ -362,6 +346,7 @@ tests_dialog = Dialog( id="toggle_active", on_click=on_toggle_active ), + Button(Const("📊 Статистика"), id="statistics", on_click=on_statistics), Button(Const("🔗 Поделиться"), id="share", on_click=on_share_test), Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu), Button(Const("◀️ Назад"), id="back", on_click=on_back_to_list), @@ -428,11 +413,4 @@ tests_dialog = Dialog( ), state=AdminTestsSG.edit_expires, ), - Window( - Format("🔗 Поделиться тестом\n\n📎 Ссылка на тест:\n{share_link}\n\n💡 Отправьте эту ссылку или QR-код пользователям для прохождения теста"), - DynamicMedia(selector=qr_media_selector), - Button(Const("◀️ Назад"), id="back", on_click=on_back_to_list), - state=AdminTestsSG.share_test, - getter=get_share_data, - ), ) diff --git a/src/trudex/application/bot/creator_dialogs/broadcast.py b/src/trudex/application/bot/creator_dialogs/broadcast.py index 5663b79..52878fc 100644 --- a/src/trudex/application/bot/creator_dialogs/broadcast.py +++ b/src/trudex/application/bot/creator_dialogs/broadcast.py @@ -1,12 +1,13 @@ from aiogram.types import CallbackQuery, Message -from aiogram_dialog import Dialog, DialogManager, Window +from aiogram_dialog import Dialog, DialogManager, StartMode, Window from aiogram_dialog.widgets.input import MessageInput -from aiogram_dialog.widgets.kbd import Button, Cancel, Row +from aiogram_dialog.widgets.kbd import Button, Row from aiogram_dialog.widgets.text import Const from dishka import FromDishka from dishka.integrations.aiogram_dialog import inject -from trudex.application.bot.creator_dialogs.states import CreatorBroadcastSG +from trudex.application.bot.creator_dialogs.states import (CreatorBroadcastSG, + CreatorMenuSG) from trudex.infrastructure.database.dao.user import UserDAO from trudex.infrastructure.utils.broadcast import broadcast_message @@ -48,7 +49,7 @@ async def on_broadcast_confirm(_callback: CallbackQuery, _button: Button, manage async def on_broadcast_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager): await _callback.answer("Рассылка отменена") - await manager.done() + await manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK) async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager): diff --git a/src/trudex/application/bot/creator_dialogs/tests.py b/src/trudex/application/bot/creator_dialogs/tests.py index e8c9f9b..914fecc 100644 --- a/src/trudex/application/bot/creator_dialogs/tests.py +++ b/src/trudex/application/bot/creator_dialogs/tests.py @@ -24,7 +24,7 @@ from trudex.infrastructure.database.dao.test import TestDAO from trudex.infrastructure.database.repo.test import TestRepository from trudex.infrastructure.utils.config import Config from trudex.infrastructure.utils.qr_generator import generate_qr_bytes -from trudex.infrastructure.utils.test_id_to_hash import generate_alpha_id +from trudex.infrastructure.utils.test_id_to_hash import encode_id @inject @@ -114,6 +114,10 @@ async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: Di await manager.switch_to(CreatorTestsSG.tests_list) +async def on_statistics(_callback: CallbackQuery, _button: Button, _manager: DialogManager): + await _callback.answer("🚧 В разработке") + + @inject async def on_share_test(_callback: CallbackQuery, _button: Button, manager: DialogManager, config: FromDishka[Config], bot_inst: FromDishka[Bot]): test_id = manager.dialog_data.get("selected_test_id") @@ -123,10 +127,10 @@ async def on_share_test(_callback: CallbackQuery, _button: Button, manager: Dial "share_link": "Ошибка: тест не найден" } - test_hash = generate_alpha_id( + test_hash = encode_id( test_id, - config.security.test_hash_salt, - config.security.test_hash_length + config.security.encode_key, + config.security.encoded_string_length ) bot_info = await bot_inst.get_me() @@ -348,6 +352,7 @@ tests_dialog = Dialog( id="toggle_active", on_click=on_toggle_active ), + Button(Const("📊 Статистика"), id="statistics", on_click=on_statistics), Button(Const("🔗 Поделиться"), id="share", on_click=on_share_test), Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu), Button(Const("◀️ Назад"), id="back", on_click=on_back_to_list), diff --git a/src/trudex/infrastructure/utils/config.py b/src/trudex/infrastructure/utils/config.py index 7fb8aa8..447882e 100644 --- a/src/trudex/infrastructure/utils/config.py +++ b/src/trudex/infrastructure/utils/config.py @@ -12,8 +12,8 @@ class BotConfig: @dataclass class SecurityConfig: - test_hash_salt: str - test_hash_length: int = 16 + encode_key: str + encoded_string_length: int = 8 @dataclass @@ -57,7 +57,7 @@ class Config: database=str(db_data["database"]) ), security=SecurityConfig( - test_hash_salt=str(security_data["test_hash_salt"]), - test_hash_length=int(security_data.get("test_hash_length", 16)) + encode_key=str(security_data["encode_key"]), + encoded_string_length=int(security_data.get("encoded_string_length", 8)) ) ) diff --git a/src/trudex/infrastructure/utils/test_id_to_hash.py b/src/trudex/infrastructure/utils/test_id_to_hash.py index 41ac700..2d9b035 100644 --- a/src/trudex/infrastructure/utils/test_id_to_hash.py +++ b/src/trudex/infrastructure/utils/test_id_to_hash.py @@ -1,25 +1,70 @@ -import hashlib import hmac +import hashlib import string -def generate_alpha_id(n: int, secret_key: str, length: int = 16) -> str: - data = str(n).encode('utf-8') - key = secret_key.encode('utf-8') +ALPHABET = string.ascii_letters + +def _feistel_round(val: int, key: bytes, rounds: int) -> int: + msg = f"{val}:{rounds}".encode() + h = hmac.new(key, msg, hashlib.sha256).digest() + return int.from_bytes(h[:4], 'big') + +def permute_id(n: int, key_str: str, bits: int) -> int: + key = key_str.encode() + split = bits // 2 + mask = (1 << split) - 1 - digest = hmac.new(key, data, hashlib.sha256).digest() - num = int.from_bytes(digest, byteorder='big') + left = (n >> split) & mask + right = n & mask - alphabet = string.ascii_letters - result = [] - - while num > 0: - num, rem = divmod(num, 52) - result.append(alphabet[rem]) + for i in range(6): + new_left = right + f_val = _feistel_round(right, key, i) + new_right = left ^ (f_val & mask) + left, right = new_left, new_right - encoded = "".join(result) + return (left << split) | right + +def unpermute_id(n: int, key_str: str, bits: int) -> int: + key = key_str.encode() + split = bits // 2 + mask = (1 << split) - 1 - if len(encoded) < length: - encoded = encoded.ljust(length, alphabet[0]) + left = (n >> split) & mask + right = n & mask + + for i in reversed(range(6)): + new_right = left + f_val = _feistel_round(left, key, i) + new_left = right ^ (f_val & mask) + left, right = new_left, new_right - return encoded[:length] + return (left << split) | right + + +def encode_id(n: int, key: str, length: int = 8) -> str: + bits = length * 5 + if length >= 8: bits = 44 + elif length == 7: bits = 38 + + permuted = permute_id(n, key, bits=bits) + + chars = [] + for _ in range(length): + permuted, rem = divmod(permuted, 52) + chars.append(ALPHABET[rem]) + + return "".join(chars) + +def decode_id(s: str, key: str) -> int: + num = 0 + for char in reversed(s): + num = num * 52 + ALPHABET.index(char) + + length = len(s) + bits = length * 5 + if length >= 8: bits = 44 + elif length == 7: bits = 38 + + return unpermute_id(num, key, bits=bits) diff --git a/uv.lock b/uv.lock index bb72a8d..c3874ba 100644 --- a/uv.lock +++ b/uv.lock @@ -603,6 +603,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -743,6 +773,7 @@ dependencies = [ { name = "asyncpg" }, { name = "dishka" }, { name = "httpx" }, + { name = "pycryptodome" }, { name = "pydantic" }, { name = "qrcode", extra = ["pil"] }, { name = "sqlalchemy" }, @@ -764,6 +795,7 @@ requires-dist = [ { name = "asyncpg", specifier = ">=0.31.0" }, { name = "dishka", specifier = ">=1.7.2" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "pycryptodome", specifier = ">=3.23.0" }, { name = "pydantic", specifier = ">=2.10.5" }, { name = "qrcode", extras = ["pil"], specifier = ">=8.2" }, { name = "sqlalchemy", specifier = ">=2.0.45" },