This commit is contained in:
2026-01-03 15:19:55 +03:00
parent 40255fc6d4
commit 1009845d31
8 changed files with 131 additions and 69 deletions
+1
View File
@@ -17,6 +17,7 @@ dependencies = [
"apscheduler>=3.10.4", "apscheduler>=3.10.4",
"pydantic>=2.10.5", "pydantic>=2.10.5",
"qrcode[pil]>=8.2", "qrcode[pil]>=8.2",
"pycryptodome>=3.23.0",
] ]
[dependency-groups] [dependency-groups]
@@ -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): async def on_broadcast_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await _callback.answer("Рассылка отменена") 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): async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager):
@@ -3,13 +3,11 @@ import functools
from datetime import date, datetime from datetime import date, datetime
from aiogram import Bot from aiogram import Bot
from aiogram.enums import ContentType
from aiogram.types import BufferedInputFile, CallbackQuery, Message from aiogram.types import BufferedInputFile, CallbackQuery, Message
from aiogram_dialog import Dialog, DialogManager, StartMode, Window from aiogram_dialog import Dialog, DialogManager, StartMode, Window
from aiogram_dialog.widgets.input import MessageInput 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) ScrollingGroup, Select)
from aiogram_dialog.widgets.media import DynamicMedia
from aiogram_dialog.widgets.text import Const, Format from aiogram_dialog.widgets.text import Const, Format
from dishka import FromDishka from dishka import FromDishka
from dishka.integrations.aiogram_dialog import inject 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.database.repo.test import TestRepository
from trudex.infrastructure.utils.config import Config from trudex.infrastructure.utils.config import Config
from trudex.infrastructure.utils.qr_generator import generate_qr_bytes 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 @inject
@@ -111,54 +109,40 @@ async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: Di
await manager.switch_to(AdminTestsSG.tests_list) await manager.switch_to(AdminTestsSG.tests_list)
async def on_share_test(_callback: CallbackQuery, _button: Button, manager: DialogManager): async def on_statistics(_callback: CallbackQuery, _button: Button, _manager: DialogManager):
await manager.switch_to(AdminTestsSG.share_test) await _callback.answer("🚧 В разработке")
@inject @inject
async def get_share_data(dialog_manager: DialogManager, config: FromDishka[Config], bot: FromDishka[Bot], **_kwargs): async def on_share_test(_callback: CallbackQuery, _button: Button, manager: DialogManager, config: FromDishka[Config], bot_inst: FromDishka[Bot]):
test_id = dialog_manager.dialog_data.get("selected_test_id") test_id = manager.dialog_data.get("selected_test_id")
if not test_id: if not test_id:
return { await _callback.answer("Ошибка: тест не найден")
"share_link": "Ошибка: тест не найден" return
}
# Генерируем хэш и ссылку test_hash = encode_id(
test_hash = generate_alpha_id(
test_id, test_id,
config.security.test_hash_salt, config.security.encode_key,
config.security.test_hash_length 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" bot_username = bot_info.username or "your_bot"
share_link = f"https://t.me/{bot_username}?start={test_hash}" share_link = f"https://t.me/{bot_username}?start={test_hash}"
# Генерируем QR-код в отдельном потоке
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
qr_bytes = await loop.run_in_executor( qr_bytes = await loop.run_in_executor(
None, None,
functools.partial(generate_qr_bytes, share_link) functools.partial(generate_qr_bytes, share_link)
) )
# Сохраняем в dialog_data для использования в media selector assert _callback.message is not None
dialog_manager.dialog_data["qr_bytes"] = qr_bytes
return { await _callback.message.answer_photo(
"share_link": share_link, photo=BufferedInputFile(qr_bytes, filename="qr.png"),
} caption=f"<b>🔗 Поделиться тестом</b>\n\n📎 <b>Ссылка на тест:</b>\n<code>{share_link}</code>\n\n💡 Отправьте эту ссылку или QR-код пользователям для прохождения теста"
)
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")
}
async def on_edit_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager): async def on_edit_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager):
@@ -362,6 +346,7 @@ tests_dialog = Dialog(
id="toggle_active", id="toggle_active",
on_click=on_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="share", on_click=on_share_test),
Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu), Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_list), Button(Const("◀️ Назад"), id="back", on_click=on_back_to_list),
@@ -428,11 +413,4 @@ tests_dialog = Dialog(
), ),
state=AdminTestsSG.edit_expires, state=AdminTestsSG.edit_expires,
), ),
Window(
Format("<b>🔗 Поделиться тестом</b>\n\n📎 <b>Ссылка на тест:</b>\n<code>{share_link}</code>\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,
),
) )
@@ -1,12 +1,13 @@
from aiogram.types import CallbackQuery, Message 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.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 aiogram_dialog.widgets.text import Const
from dishka import FromDishka from dishka import FromDishka
from dishka.integrations.aiogram_dialog import inject 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.database.dao.user import UserDAO
from trudex.infrastructure.utils.broadcast import broadcast_message 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): async def on_broadcast_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await _callback.answer("Рассылка отменена") 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): async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager):
@@ -24,7 +24,7 @@ from trudex.infrastructure.database.dao.test import TestDAO
from trudex.infrastructure.database.repo.test import TestRepository from trudex.infrastructure.database.repo.test import TestRepository
from trudex.infrastructure.utils.config import Config from trudex.infrastructure.utils.config import Config
from trudex.infrastructure.utils.qr_generator import generate_qr_bytes 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 @inject
@@ -114,6 +114,10 @@ async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: Di
await manager.switch_to(CreatorTestsSG.tests_list) await manager.switch_to(CreatorTestsSG.tests_list)
async def on_statistics(_callback: CallbackQuery, _button: Button, _manager: DialogManager):
await _callback.answer("🚧 В разработке")
@inject @inject
async def on_share_test(_callback: CallbackQuery, _button: Button, manager: DialogManager, config: FromDishka[Config], bot_inst: FromDishka[Bot]): 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") 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": "Ошибка: тест не найден" "share_link": "Ошибка: тест не найден"
} }
test_hash = generate_alpha_id( test_hash = encode_id(
test_id, test_id,
config.security.test_hash_salt, config.security.encode_key,
config.security.test_hash_length config.security.encoded_string_length
) )
bot_info = await bot_inst.get_me() bot_info = await bot_inst.get_me()
@@ -348,6 +352,7 @@ tests_dialog = Dialog(
id="toggle_active", id="toggle_active",
on_click=on_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="share", on_click=on_share_test),
Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu), Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_list), Button(Const("◀️ Назад"), id="back", on_click=on_back_to_list),
+4 -4
View File
@@ -12,8 +12,8 @@ class BotConfig:
@dataclass @dataclass
class SecurityConfig: class SecurityConfig:
test_hash_salt: str encode_key: str
test_hash_length: int = 16 encoded_string_length: int = 8
@dataclass @dataclass
@@ -57,7 +57,7 @@ class Config:
database=str(db_data["database"]) database=str(db_data["database"])
), ),
security=SecurityConfig( security=SecurityConfig(
test_hash_salt=str(security_data["test_hash_salt"]), encode_key=str(security_data["encode_key"]),
test_hash_length=int(security_data.get("test_hash_length", 16)) encoded_string_length=int(security_data.get("encoded_string_length", 8))
) )
) )
@@ -1,25 +1,70 @@
import hashlib
import hmac import hmac
import hashlib
import string import string
def generate_alpha_id(n: int, secret_key: str, length: int = 16) -> str: ALPHABET = string.ascii_letters
data = str(n).encode('utf-8')
key = secret_key.encode('utf-8')
digest = hmac.new(key, data, hashlib.sha256).digest() def _feistel_round(val: int, key: bytes, rounds: int) -> int:
num = int.from_bytes(digest, byteorder='big') msg = f"{val}:{rounds}".encode()
h = hmac.new(key, msg, hashlib.sha256).digest()
return int.from_bytes(h[:4], 'big')
alphabet = string.ascii_letters def permute_id(n: int, key_str: str, bits: int) -> int:
result = [] key = key_str.encode()
split = bits // 2
mask = (1 << split) - 1
while num > 0: left = (n >> split) & mask
num, rem = divmod(num, 52) right = n & mask
result.append(alphabet[rem])
encoded = "".join(result) 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
if len(encoded) < length: return (left << split) | right
encoded = encoded.ljust(length, alphabet[0])
return encoded[:length] def unpermute_id(n: int, key_str: str, bits: int) -> int:
key = key_str.encode()
split = bits // 2
mask = (1 << split) - 1
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 (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)
Generated
+32
View File
@@ -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" }, { 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]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.5"
@@ -743,6 +773,7 @@ dependencies = [
{ name = "asyncpg" }, { name = "asyncpg" },
{ name = "dishka" }, { name = "dishka" },
{ name = "httpx" }, { name = "httpx" },
{ name = "pycryptodome" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "qrcode", extra = ["pil"] }, { name = "qrcode", extra = ["pil"] },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
@@ -764,6 +795,7 @@ requires-dist = [
{ name = "asyncpg", specifier = ">=0.31.0" }, { name = "asyncpg", specifier = ">=0.31.0" },
{ name = "dishka", specifier = ">=1.7.2" }, { name = "dishka", specifier = ">=1.7.2" },
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "pycryptodome", specifier = ">=3.23.0" },
{ name = "pydantic", specifier = ">=2.10.5" }, { name = "pydantic", specifier = ">=2.10.5" },
{ name = "qrcode", extras = ["pil"], specifier = ">=8.2" }, { name = "qrcode", extras = ["pil"], specifier = ">=8.2" },
{ name = "sqlalchemy", specifier = ">=2.0.45" }, { name = "sqlalchemy", specifier = ">=2.0.45" },