Compare commits

...

4 Commits

Author SHA1 Message Date
807dbee647 feat: добавлены библиотеки Celery и Redis
All checks were successful
Lint project / lint (push) Successful in 2m22s
- Добавлены зависимости Celery и Redis в `pyproject.toml`.
- Обновлена документация `README.md`, добавлены новые пункты о Celery, Redis, itsdangerous, smtplib и Jinja2.
- Добавлено описание переменных окружения в `.env.example` для Celery, Redis и электронной почты.
2025-03-20 13:44:12 +04:00
5a2491ec91 feat: добавлен шаблон электронной почты подтверждения регистрации
- Создан новый файл `templates/confirmation_email.html`.
- Добавлен HTML-код для отображения страницы подтверждения регистрации.
- Включена ссылка на подтверждение через переменную `{{ confirmation_url }}`.
2025-03-20 13:43:56 +04:00
7cfb2e734a **feat: добавлен модуль конфигурации Celery и расширены настройки приложения**
- Создан новый файл `celery_config.py` в пакете `lkeep.core`, содержащий класс для конфигурации Celery.
- В файле `settings.py` добавлены новые классы `EmailSettings` и `RedisSettings` с соответствующими настройками.
- Обновлён класс `Settings` для включения новых настроек: `email_settings`, `redis_settings`.
- Добавлен экземпляр Celery в модуль `__init__.py` для доступа из других частей приложения.
2025-03-20 13:43:40 +04:00
217af1cd06 feat: добавлены задачи для отправки email и обновлены сервисы аутентификации
- Созданы две задачи с использованием Celery: `send_text_confirmation_email` и `send_confirmation_email`.
- Обновлена модель бизнес-логики `UserService`: добавлен метод `confirm_user` для подтверждения пользователя.
- Обновлена модель менеджера `UserManager`: добавлен асинхронный метод `confirm_user` для обновления состояния пользователя в базе данных.
- Добавлена новая роутер-функция для подтверждения регистрации по ссылке `/register_confirm`.
2025-03-20 13:43:22 +04:00
12 changed files with 625 additions and 17 deletions

View File

@ -1,7 +1,22 @@
# Переменные для базы данных
DB_NAME=db_name DB_NAME=db_name
DB_USER=db_user DB_USER=db_user
DB_PASSWORD=db_password DB_PASSWORD=db_password
DB_HOST=localhost DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_ECHO=True DB_ECHO=True
# Системные переменные
SECRET_KEY=1234567890abcdefghigklmnopqrstuvwxyz SECRET_KEY=1234567890abcdefghigklmnopqrstuvwxyz
FRONTEND_URL=http://127.0.0.1:8000/api/v1
# Переменные для почты
EMAIL_HOST=smtp.yandex.ru
EMAIL_PORT=465
EMAIL_USERNAME=info@yandex.ru
EMAIL_PASSWORD=12345
# Переменные для Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DB=0

View File

@ -21,6 +21,15 @@ PostgreSQL, Poetry, Pydantic и других.
- **uvicorn** — высокопроизводительный ASGI-сервер для обработки HTTP-запросов. - **uvicorn** — высокопроизводительный ASGI-сервер для обработки HTTP-запросов.
- **pydantic-settings** — библиотека для работы с конфигурациями и переменными окружения с использованием Pydantic. - **pydantic-settings** — библиотека для работы с конфигурациями и переменными окружения с использованием Pydantic.
- **passlib** — библиотека для безопасного хеширования паролей и других данных. - **passlib** — библиотека для безопасного хеширования паролей и других данных.
- **celery** — распределённая система для выполнения фоновых задач и управления очередями, позволяющая выполнять задачи
асинхронно.
- **redis** — высокопроизводительное in-memory хранилище, используемое для кэширования данных и как брокер сообщений для
Celery.
- **itsdangerous** — библиотека для безопасного создания и проверки подписанных данных, что помогает защитить токены и
другую чувствительную информацию.
- **smtplib** — стандартный модуль Python для отправки электронной почты через протокол SMTP.
- **jinja2** — современный и гибкий шаблонизатор, который позволяет динамически генерировать HTML и другие текстовые
форматы.
## Репозитории ## Репозитории
@ -37,6 +46,8 @@ PostgreSQL, Poetry, Pydantic и других.
3. [FastAPI 3. Подключение к SQLAlchemy и генератор сессий](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-3-podklyuchenie-k-sqlalchemy-i-generator-s/) 3. [FastAPI 3. Подключение к SQLAlchemy и генератор сессий](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-3-podklyuchenie-k-sqlalchemy-i-generator-s/)
4. [FastAPI 4. Модель пользователя, миксины и Alembic](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-4-model-polzovatelya-i-alembic/) 4. [FastAPI 4. Модель пользователя, миксины и Alembic](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-4-model-polzovatelya-i-alembic/)
5. [FastAPI 5. Приложение аутентификации и Pydantic схемы](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-5-prilozhenie-autentifikacii-i-pydantic-sh/) 5. [FastAPI 5. Приложение аутентификации и Pydantic схемы](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-5-prilozhenie-autentifikacii-i-pydantic-sh/)
6. [FastAPI 6. Пользовательский сервис и маршруты регистрации](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-6-polzovatelskij-servis-i-marshruty-regist/)
7. [FastAPI 7. Электронная почта, подтверждение регистрации, Celery и Redis](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-7-elektronnaya-pochta-podtverzhdenie-registracii-celery-i-redis/)
## Установка ## Установка

View File

@ -7,7 +7,7 @@ https://pressanybutton.ru/category/servis-na-fastapi/
""" """
from fastapi import Depends, HTTPException from fastapi import Depends, HTTPException
from sqlalchemy import insert from sqlalchemy import insert, update
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from lkeep.apps.auth.schemas import CreateUser, UserReturnData from lkeep.apps.auth.schemas import CreateUser, UserReturnData
@ -24,8 +24,6 @@ class UserManager:
""" """
Инициализирует экземпляр класса. Инициализирует экземпляр класса.
:param model: Модель, используемая для работы с данными.
:type model: Type[User]
:param db: Зависимость от базы данных. По умолчанию используется Depends(DBDependency). :param db: Зависимость от базы данных. По умолчанию используется Depends(DBDependency).
:type db: DBDependency :type db: DBDependency
""" """
@ -42,7 +40,7 @@ class UserManager:
:rtype: UserReturnData :rtype: UserReturnData
:raises HTTPException: Если пользователь уже существует. :raises HTTPException: Если пользователь уже существует.
""" """
async with self.db.db_session as session: async with self.db.db_session() as session:
query = insert(self.model).values(**user.model_dump()).returning(self.model) query = insert(self.model).values(**user.model_dump()).returning(self.model)
try: try:
@ -52,5 +50,17 @@ class UserManager:
await session.commit() await session.commit()
user_data = await result.scalar_one() user_data = result.scalar_one()
return UserReturnData(**user_data.__dict__) return UserReturnData(**user_data.__dict__)
async def confirm_user(self, email: str) -> None:
"""
Асинхронный метод для подтверждения пользователя по электронной почте.
:param email: Электронная почта пользователя, которого нужно подтвердить.
:type email: str
"""
async with self.db.db_session() as session:
query = update(self.model).where(self.model.email == email).values(is_verified=True, is_active=True)
await session.execute(query)
await session.commit()

View File

@ -29,3 +29,18 @@ async def registration(user: RegisterUser, service: UserService = Depends(UserSe
:raises HTTPException 400: Если данные пользователя некорректны. :raises HTTPException 400: Если данные пользователя некорректны.
""" """
return await service.register_user(user=user) return await service.register_user(user=user)
@auth_router.get(path="/register_confirm", status_code=status.HTTP_200_OK)
async def confirm_registration(token: str, service: UserService = Depends(UserService)) -> dict[str, str]:
"""
Подтверждает регистрацию пользователя по ссылке.
:param token: Токен подтверждения регистрации, полученный после отправки на электронную почту.
:type token: str
:raises HTTPException: Если токен недействителен или срок действия истек.
:return: Словарь с сообщением о успешной подтверждении электронной почты.
:rtype: dict[str, str]
"""
await service.confirm_user(token=token)
return {"message": "Электронная почта подтверждена"}

View File

@ -6,11 +6,14 @@
https://pressanybutton.ru/category/servis-na-fastapi/ https://pressanybutton.ru/category/servis-na-fastapi/
""" """
from fastapi import Depends from fastapi import Depends, HTTPException
from itsdangerous import BadSignature, URLSafeTimedSerializer
from lkeep.apps.auth.handlers import AuthHandler from lkeep.apps.auth.handlers import AuthHandler
from lkeep.apps.auth.managers import UserManager from lkeep.apps.auth.managers import UserManager
from lkeep.apps.auth.schemas import CreateUser, RegisterUser, UserReturnData from lkeep.apps.auth.schemas import CreateUser, RegisterUser, UserReturnData
from lkeep.apps.auth.tasks import send_confirmation_email
from lkeep.core.settings import settings
class UserService: class UserService:
@ -31,18 +34,39 @@ class UserService:
""" """
self.manager = manager self.manager = manager
self.handler = handler self.handler = handler
self.serializer = URLSafeTimedSerializer(secret_key=settings.secret_key.get_secret_value())
async def register_user(self, user: RegisterUser) -> UserReturnData: async def register_user(self, user: RegisterUser) -> UserReturnData:
""" """
Регистрирует нового пользователя в системе. Регистрирует нового пользователя в системе.
:param user: Данные для регистрации пользователя. :param user: Информация о пользователе, который нужно зарегистрировать.
:type user: RegisterUser :type user: RegisterUser
:return: Данные зарегистрированного пользователя. :returns: Данные о созданном пользователе.
:rtype: UserReturnData :rtype: UserReturnData
""" """
hashed_password = await self.handler.get_password_hash(user.password) hashed_password = await self.handler.get_password_hash(user.password)
new_user = CreateUser(email=user.email, hashed_password=hashed_password) new_user = CreateUser(email=user.email, hashed_password=hashed_password)
return await self.manager.create_user(user=new_user) user_data = await self.manager.create_user(user=new_user)
confirmation_token = self.serializer.dumps(user_data.email)
send_confirmation_email.delay(to_email=user_data.email, token=confirmation_token)
return user_data
async def confirm_user(self, token: str) -> None:
"""
Подтверждает пользователя по переданному токену.
:param token: Токен для подтверждения пользователя.
:type token: str
:raises HTTPException: Если токен неверный или просроченный.
"""
try:
email = self.serializer.loads(token, max_age=3600)
except BadSignature:
raise HTTPException(status_code=400, detail="Неверный или просроченный токен")
await self.manager.confirm_user(email=email)

75
lkeep/apps/auth/tasks.py Normal file
View File

@ -0,0 +1,75 @@
"""
Проект: Lkeep
Автор: Иван Ашихмин
Год: 2025
Специально для проекта "Код на салфетке"
https://pressanybutton.ru/category/servis-na-fastapi/
"""
import smtplib
from email.message import EmailMessage
from celery import shared_task
from starlette.templating import Jinja2Templates
from lkeep.core.settings import settings
@shared_task
def send_text_confirmation_email(to_email: str, token: str) -> None:
"""
Отправляет текстовое подтверждение регистрации по электронной почте.
:param to_email: Адрес электронной почты получателя подтверждения.
:type to_email: str
:param token: Токен для подтверждения регистрации.
:type token: str
"""
confirmation_url = f"{settings.frontend_url}/auth/register_confirm?token={token}"
text = f"""Спасибо за регистрацию!
Для подтверждения регистрации перейдите по ссылке: {confirmation_url}
"""
message = EmailMessage()
message.set_content(text)
message["From"] = settings.email_settings.email_username
message["To"] = to_email
message["Subject"] = "Подтверждение регистрации"
with smtplib.SMTP_SSL(host=settings.email_settings.email_host, port=settings.email_settings.email_port) as smtp:
smtp.login(
user=settings.email_settings.email_username,
password=settings.email_settings.email_password.get_secret_value(),
)
smtp.send_message(msg=message)
@shared_task
def send_confirmation_email(to_email: str, token: str) -> None:
"""
Отправляет подтверждение регистрации по электронной почте.
:param to_email: Адрес электронной почты получателя сообщения.
:type to_email: str
:param token: Токен для подтверждения регистрации, передаваемый в URL.
:type token: str
"""
confirmation_url = f"{settings.frontend_url}/auth/register_confirm?token={token}"
templates = Jinja2Templates(directory=settings.templates_dir)
template = templates.get_template(name="confirmation_email.html")
html_content = template.render(confirmation_url=confirmation_url)
message = EmailMessage()
message.add_alternative(html_content, subtype="html")
message["From"] = settings.email_settings.email_username
message["To"] = to_email
message["Subject"] = "Подтверждение регистрации"
with smtplib.SMTP_SSL(host=settings.email_settings.email_host, port=settings.email_settings.email_port) as smtp:
smtp.login(
user=settings.email_settings.email_username,
password=settings.email_settings.email_password.get_secret_value(),
)
smtp.send_message(msg=message)

View File

@ -5,3 +5,7 @@
Специально для проекта "Код на салфетке" Специально для проекта "Код на салфетке"
https://pressanybutton.ru/category/servis-na-fastapi/ https://pressanybutton.ru/category/servis-na-fastapi/
""" """
from .celery_config import celery_app
__all__ = ["celery_app"]

View File

@ -0,0 +1,15 @@
"""
Проект: Lkeep
Автор: Иван Ашихмин
Год: 2025
Специально для проекта "Код на салфетке"
https://pressanybutton.ru/category/servis-na-fastapi/
"""
from celery import Celery
from lkeep.core.settings import settings
celery_app = Celery(main="lkeep", broker=settings.redis_settings.redis_url, backend=settings.redis_settings.redis_url)
celery_app.autodiscover_tasks(packages=["lkeep.apps"])

View File

@ -48,18 +48,83 @@ class DBSettings(BaseSettings):
return f"postgresql+asyncpg://{self.db_user}:{self.db_password.get_secret_value()}@{self.db_host}:{self.db_port}/{self.db_name}" return f"postgresql+asyncpg://{self.db_user}:{self.db_password.get_secret_value()}@{self.db_host}:{self.db_port}/{self.db_name}"
class EmailSettings(BaseSettings):
"""
Настройки для электронной почты.
:ivar email_host: Адрес SMTP-сервера.
:type email_host: str
:ivar email_port: Порт, используемый для подключения к SMTP-серверу.
:type email_port: int
:ivar email_username: Имя пользователя для аутентификации на электронной почтовом сервере.
:type email_username: str
:ivar email_password: Пароль пользователя, скрытый через `SecretStr` для обеспечения безопасности.
:type email_password: SecretStr
:model_config: Конфигурация settings, которая указывает на файл окружения и его кодировку.
:type model_config: SettingsConfigDict
"""
email_host: str
email_port: int
email_username: str
email_password: SecretStr
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf8", extra="ignore")
class RedisSettings(BaseSettings):
"""
Класс для настройки соединения с Redis.
:ivar redis_host: Хост, на котором размещается Redis-сервер.
:type redis_host: str
:ivar redis_port: Порт, через который происходит соединение с Redis-сервером.
:type redis_port: int
:ivar redis_db: Номер базы данных для использования в Redis.
:type redis_db: int
"""
redis_host: str
redis_port: int
redis_db: int
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf8", extra="ignore")
@property
def redis_url(self):
"""
Получает URL для подключения к Redis.
:returns: Строка с URL для подключения к Redis в формате `redis://<хост>:<порт>/<база данных>`.
:rtype: str
"""
return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}"
class Settings(BaseSettings): class Settings(BaseSettings):
""" """
Класс Settings используется для хранения настроек приложения. Класс для хранения настроек приложения.
:ivar db_settings: Экземпляр класса DBSettings, содержащий настройки базы данных. :ivar db_settings: Настройки для работы с базой данных.
:type db_settings: DBSettings :type db_settings: DBSettings
:ivar secret_key: Секретный ключ для шифрования :ivar email_settings: Настройки для отправки электронной почты.
:type email_settings: EmailSettings
:ivar redis_settings: Настройки для работы с Redis.
:type redis_settings: RedisSettings
:ivar secret_key: Секретный ключ приложения.
:type secret_key: SecretStr :type secret_key: SecretStr
:ivar templates_dir: Путь к директории шаблонов.
:type templates_dir: str
:ivar frontend_url: Адрес фронтенд-приложения.
:type frontend_url: str
""" """
db_settings: DBSettings = DBSettings() db_settings: DBSettings = DBSettings()
email_settings: EmailSettings = EmailSettings()
redis_settings: RedisSettings = RedisSettings()
secret_key: SecretStr secret_key: SecretStr
templates_dir: str = "templates"
frontend_url: str
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf8", extra="ignore") model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf8", extra="ignore")

367
poetry.lock generated
View File

@ -20,6 +20,21 @@ typing-extensions = ">=4"
[package.extras] [package.extras]
tz = ["backports.zoneinfo"] tz = ["backports.zoneinfo"]
[[package]]
name = "amqp"
version = "5.3.1"
description = "Low-level AMQP client for Python (fork of amqplib)."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"},
{file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"},
]
[package.dependencies]
vine = ">=5.0.0,<6.0.0"
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
version = "0.7.0" version = "0.7.0"
@ -153,6 +168,120 @@ files = [
tests = ["pytest (>=3.2.1,!=3.3.0)"] tests = ["pytest (>=3.2.1,!=3.3.0)"]
typecheck = ["mypy"] typecheck = ["mypy"]
[[package]]
name = "billiard"
version = "4.2.1"
description = "Python multiprocessing fork with improvements and bugfixes"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"},
{file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"},
]
[[package]]
name = "black"
version = "25.1.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"},
{file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"},
{file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"},
{file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"},
{file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"},
{file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"},
{file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"},
{file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"},
{file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"},
{file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"},
{file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"},
{file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"},
{file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"},
{file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"},
{file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"},
{file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"},
{file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"},
{file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"},
{file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"},
{file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"},
{file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"},
{file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.10)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "celery"
version = "5.4.0"
description = "Distributed Task Queue."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"},
{file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"},
]
[package.dependencies]
billiard = ">=4.2.0,<5.0"
click = ">=8.1.2,<9.0"
click-didyoumean = ">=0.3.0"
click-plugins = ">=1.1.1"
click-repl = ">=0.2.0"
kombu = ">=5.3.4,<6.0"
python-dateutil = ">=2.8.2"
tzdata = ">=2022.7"
vine = ">=5.1.0,<6.0"
[package.extras]
arangodb = ["pyArango (>=2.0.2)"]
auth = ["cryptography (==42.0.5)"]
azureblockblob = ["azure-storage-blob (>=12.15.0)"]
brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"]
cassandra = ["cassandra-driver (>=3.25.0,<4)"]
consul = ["python-consul2 (==0.1.5)"]
cosmosdbsql = ["pydocumentdb (==2.3.5)"]
couchbase = ["couchbase (>=3.0.0)"]
couchdb = ["pycouchdb (==1.14.2)"]
django = ["Django (>=2.2.28)"]
dynamodb = ["boto3 (>=1.26.143)"]
elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"]
eventlet = ["eventlet (>=0.32.0)"]
gcs = ["google-cloud-storage (>=2.10.0)"]
gevent = ["gevent (>=1.5.0)"]
librabbitmq = ["librabbitmq (>=2.0.0)"]
memcache = ["pylibmc (==1.6.3)"]
mongodb = ["pymongo[srv] (>=4.0.2)"]
msgpack = ["msgpack (==1.0.8)"]
pymemcache = ["python-memcached (>=1.61)"]
pyro = ["pyro4 (==4.82)"]
pytest = ["pytest-celery[all] (>=1.0.0)"]
redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"]
s3 = ["boto3 (>=1.26.143)"]
slmq = ["softlayer-messaging (>=1.0.3)"]
solar = ["ephem (==4.1.5)"]
sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"]
sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"]
tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"]
yaml = ["PyYAML (>=3.10)"]
zookeeper = ["kazoo (>=1.3.1)"]
zstd = ["zstandard (==0.22.0)"]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2024.12.14" version = "2024.12.14"
@ -183,7 +312,7 @@ version = "8.1.8"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"] groups = ["main", "dev"]
files = [ files = [
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
@ -192,18 +321,70 @@ files = [
[package.dependencies] [package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""} colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "click-didyoumean"
version = "0.3.1"
description = "Enables git-like *did-you-mean* feature in click"
optional = false
python-versions = ">=3.6.2"
groups = ["main"]
files = [
{file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"},
{file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"},
]
[package.dependencies]
click = ">=7"
[[package]]
name = "click-plugins"
version = "1.1.1"
description = "An extension module for click to enable registering CLI commands via setuptools entry-points."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"},
{file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"},
]
[package.dependencies]
click = ">=4.0"
[package.extras]
dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"]
[[package]]
name = "click-repl"
version = "0.3.0"
description = "REPL plugin for Click"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"},
{file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"},
]
[package.dependencies]
click = ">=7.0"
prompt-toolkit = ">=3.0.36"
[package.extras]
testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"] groups = ["main", "dev"]
markers = "platform_system == \"Windows\" or sys_platform == \"win32\""
files = [ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "platform_system == \"Windows\""}
[[package]] [[package]]
name = "distlib" name = "distlib"
@ -551,6 +732,18 @@ files = [
[package.extras] [package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "itsdangerous"
version = "2.2.0"
description = "Safely pass data to untrusted environments and back."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.5" version = "3.1.5"
@ -569,6 +762,40 @@ MarkupSafe = ">=2.0"
[package.extras] [package.extras]
i18n = ["Babel (>=2.7)"] i18n = ["Babel (>=2.7)"]
[[package]]
name = "kombu"
version = "5.4.2"
description = "Messaging library for Python."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763"},
{file = "kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf"},
]
[package.dependencies]
amqp = ">=5.1.1,<6.0.0"
tzdata = {version = "*", markers = "python_version >= \"3.9\""}
vine = "5.1.0"
[package.extras]
azureservicebus = ["azure-servicebus (>=7.10.0)"]
azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"]
confluentkafka = ["confluent-kafka (>=2.2.0)"]
consul = ["python-consul2 (==0.1.5)"]
librabbitmq = ["librabbitmq (>=2.0.0)"]
mongodb = ["pymongo (>=4.1.1)"]
msgpack = ["msgpack (==1.1.0)"]
pyro = ["pyro4 (==4.82)"]
qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"]
redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"]
slmq = ["softlayer-messaging (>=1.0.3)"]
sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"]
sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"]
yaml = ["PyYAML (>=3.10)"]
zookeeper = ["kazoo (>=2.8.0)"]
[[package]] [[package]]
name = "mako" name = "mako"
version = "1.3.8" version = "1.3.8"
@ -697,6 +924,18 @@ files = [
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
] ]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
groups = ["dev"]
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.9.1" version = "1.9.1"
@ -709,6 +948,18 @@ files = [
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
] ]
[[package]]
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]] [[package]]
name = "passlib" name = "passlib"
version = "1.7.4" version = "1.7.4"
@ -727,13 +978,25 @@ bcrypt = ["bcrypt (>=3.1.0)"]
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
totp = ["cryptography"] totp = ["cryptography"]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.3.6" version = "4.3.6"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] groups = ["main", "dev"]
files = [ files = [
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
@ -763,6 +1026,21 @@ nodeenv = ">=0.11.1"
pyyaml = ">=5.1" pyyaml = ">=5.1"
virtualenv = ">=20.10.0" virtualenv = ">=20.10.0"
[[package]]
name = "prompt-toolkit"
version = "3.0.48"
description = "Library for building powerful interactive command lines in Python"
optional = false
python-versions = ">=3.7.0"
groups = ["main"]
files = [
{file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"},
{file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"},
]
[package.dependencies]
wcwidth = "*"
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.10.5" version = "2.10.5"
@ -934,6 +1212,21 @@ files = [
[package.extras] [package.extras]
windows-terminal = ["colorama (>=0.4.6)"] windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
groups = ["main"]
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
six = ">=1.5"
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.0.1" version = "1.0.1"
@ -1024,6 +1317,22 @@ files = [
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
] ]
[[package]]
name = "redis"
version = "5.2.1"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"},
{file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"},
]
[package.extras]
hiredis = ["hiredis (>=3.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"]
[[package]] [[package]]
name = "rich" name = "rich"
version = "13.9.4" version = "13.9.4"
@ -1100,6 +1409,18 @@ files = [
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
] ]
[[package]]
name = "six"
version = "1.17.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
groups = ["main"]
files = [
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
]
[[package]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.1" version = "1.3.1"
@ -1214,6 +1535,18 @@ files = [
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
] ]
[[package]]
name = "tzdata"
version = "2024.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
groups = ["main"]
files = [
{file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"},
{file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"},
]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.34.0" version = "0.34.0"
@ -1293,6 +1626,18 @@ dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"]
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
[[package]]
name = "vine"
version = "5.1.0"
description = "Python promises."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"},
{file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"},
]
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.28.1" version = "20.28.1"
@ -1398,6 +1743,18 @@ files = [
[package.dependencies] [package.dependencies]
anyio = ">=3.0.0" anyio = ">=3.0.0"
[[package]]
name = "wcwidth"
version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[[package]] [[package]]
name = "websockets" name = "websockets"
version = "14.1" version = "14.1"
@ -1480,4 +1837,4 @@ files = [
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.12" python-versions = ">=3.12"
content-hash = "3d4c2b89136e414e183f596de6faca17a5c9d78e0b2788ad4cb4fd598e486faa" content-hash = "881df1a02c4e0bc5ff26cb0029aef8488bb0391b229e6fd73ce31d45b2baa1fb"

View File

@ -19,6 +19,9 @@ dependencies = [
"pydantic[email] (>=2.10.5,<3.0.0)", "pydantic[email] (>=2.10.5,<3.0.0)",
"passlib (>=1.7.4,<2.0.0)", "passlib (>=1.7.4,<2.0.0)",
"bcrypt (==4.0.1)", "bcrypt (==4.0.1)",
"celery (>=5.4.0,<6.0.0)",
"redis (>=5.2.1,<6.0.0)",
"itsdangerous (>=2.2.0,<3.0.0)",
] ]
[build-system] [build-system]
@ -27,3 +30,6 @@ build-backend = "poetry.core.masonry.api"
[project.scripts] [project.scripts]
app = "lkeep.main:start" app = "lkeep.main:start"
[tool.poetry.group.dev.dependencies]
black = "^25.1.0"

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<title>Подтверждение регистрации</title>
</head>
<body>
<h1>Подтверждение регистрации</h1>
<p>Для подтверждения регистрации перейдите по ссылке:</p>
<a href="{{ confirmation_url }}">Подтвердить регистрацию</a>
</body>
</html>