diff --git a/fanfan/application/services/notification.py b/fanfan/application/services/notification.py index 5492c4f..624d50a 100644 --- a/fanfan/application/services/notification.py +++ b/fanfan/application/services/notification.py @@ -15,7 +15,10 @@ from fanfan.infrastructure.scheduler import ( redis_async_result, ) -from fanfan.infrastructure.scheduler.tasks import delete_message, send_notification +from fanfan.infrastructure.scheduler.tasks.notifications import ( + delete_message, + send_notification, +) logger = logging.getLogger(__name__) diff --git a/fanfan/application/services/quest.py b/fanfan/application/services/quest.py index e3c15f7..5ca3146 100644 --- a/fanfan/application/services/quest.py +++ b/fanfan/application/services/quest.py @@ -15,7 +15,7 @@ UserNotFound, ) from fanfan.application.services.base import BaseService -from fanfan.infrastructure.scheduler.tasks import send_notification +from fanfan.infrastructure.scheduler.tasks.notifications import send_notification logger = logging.getLogger(__name__) diff --git a/fanfan/application/services/ticket.py b/fanfan/application/services/ticket.py index 572aeee..bbdfede 100644 --- a/fanfan/application/services/ticket.py +++ b/fanfan/application/services/ticket.py @@ -12,7 +12,6 @@ from fanfan.application.exceptions.users import ( UserNotFound, ) -from fanfan.application.services.access import check_permission from fanfan.application.services.base import BaseService from fanfan.common.enums import UserRole from fanfan.infrastructure.db.models import Ticket @@ -21,7 +20,6 @@ class TicketService(BaseService): - @check_permission(allowed_roles=[UserRole.ORG]) async def create_ticket(self, ticket_id: str, role: UserRole) -> TicketDTO: """Create a new ticket""" async with self.uow: @@ -54,3 +52,13 @@ async def link_ticket(self, ticket_id: str, user_id: int) -> None: await self.uow.commit() logger.info(f"Ticket id={ticket.id} was linked to user id={user.id}") return + + async def delete_ticket(self, ticket_id: str) -> None: + ticket = await self.uow.tickets.get_ticket(ticket_id) + if not ticket: + raise TicketNotFound + async with self.uow: + await self.uow.session.delete(ticket) + await self.uow.commit() + logger.info(f"Ticket id={ticket_id} was deleted") + return diff --git a/fanfan/config.py b/fanfan/config.py index 5b8bc81..f0bba1d 100644 --- a/fanfan/config.py +++ b/fanfan/config.py @@ -154,6 +154,13 @@ def build_qr_scanner_url(self) -> str: return url.unicode_string() +class TimepadConfig(BaseSettings): + model_config = SettingsConfigDict(env_prefix="TIMEPAD_") + + client_id: Optional[SecretStr] = None + event_id: Optional[int] = None + + class DebugConfig(BaseSettings): model_config = SettingsConfigDict(env_prefix="DEBUG_") @@ -177,6 +184,7 @@ class Configuration: redis: RedisConfig = RedisConfig() bot: BotConfig = BotConfig() web: WebConfig = WebConfig() + timepad: TimepadConfig = TimepadConfig() debug: DebugConfig = DebugConfig() diff --git a/fanfan/infrastructure/di/__init__.py b/fanfan/infrastructure/di/__init__.py index fcb2814..2a5fa04 100644 --- a/fanfan/infrastructure/di/__init__.py +++ b/fanfan/infrastructure/di/__init__.py @@ -6,6 +6,7 @@ from fanfan.infrastructure.di.config import ConfigProvider from fanfan.infrastructure.di.db import DbProvider from fanfan.infrastructure.di.redis import RedisProvider +from fanfan.infrastructure.di.timepad import TimepadProvider from fanfan.infrastructure.di.user_bot import UserProvider @@ -16,6 +17,7 @@ def get_app_providers() -> List[Provider]: DpProvider(), BotProvider(), RedisProvider(), + TimepadProvider(), ] diff --git a/fanfan/infrastructure/di/config.py b/fanfan/infrastructure/di/config.py index 9c03cd7..287a1e5 100644 --- a/fanfan/infrastructure/di/config.py +++ b/fanfan/infrastructure/di/config.py @@ -1,6 +1,13 @@ from dishka import Provider, Scope, provide -from fanfan.config import BotConfig, DatabaseConfig, DebugConfig, RedisConfig, WebConfig +from fanfan.config import ( + BotConfig, + DatabaseConfig, + DebugConfig, + RedisConfig, + TimepadConfig, + WebConfig, +) class ConfigProvider(Provider): @@ -22,6 +29,10 @@ def get_bot_config(self) -> BotConfig: def get_web_config(self) -> WebConfig: return WebConfig() + @provide + def get_timepad_config(self) -> TimepadConfig: + return TimepadConfig() + @provide def get_debug_config(self) -> DebugConfig: return DebugConfig() diff --git a/fanfan/infrastructure/di/timepad.py b/fanfan/infrastructure/di/timepad.py new file mode 100644 index 0000000..896a70e --- /dev/null +++ b/fanfan/infrastructure/di/timepad.py @@ -0,0 +1,26 @@ +from typing import AsyncIterable, NewType + +from aiohttp import ClientSession +from dishka import Provider, Scope, provide + +from fanfan.config import TimepadConfig +from fanfan.infrastructure.timepad.client import TimepadClient + +TimepadSession = NewType("TimepadSession", ClientSession) + + +class TimepadProvider(Provider): + scope = Scope.APP + + @provide(scope=Scope.REQUEST) + async def get_timepad_session( + self, config: TimepadConfig + ) -> AsyncIterable[TimepadSession]: + async with ClientSession( + headers={"Authorization": f"Bearer {config.client_id.get_secret_value()}"} + ) as session: + yield session + + @provide(scope=Scope.REQUEST) + async def get_timepad_client(self, session: TimepadSession) -> TimepadClient: + return TimepadClient(session) diff --git a/fanfan/infrastructure/scheduler/__init__.py b/fanfan/infrastructure/scheduler/__init__.py index 3258dff..59825ec 100644 --- a/fanfan/infrastructure/scheduler/__init__.py +++ b/fanfan/infrastructure/scheduler/__init__.py @@ -1,6 +1,8 @@ import uuid from datetime import timedelta +from taskiq import SimpleRetryMiddleware, TaskiqScheduler +from taskiq.schedule_sources import LabelScheduleSource from taskiq.serializers import ORJSONSerializer from taskiq_redis import ListQueueBroker, RedisAsyncResultBackend @@ -17,5 +19,11 @@ ) .with_result_backend(redis_async_result) .with_serializer(ORJSONSerializer()) + .with_middlewares(SimpleRetryMiddleware(default_retry_count=0)) .with_id_generator(lambda: f"task:{uuid.uuid4().hex}") ) + +scheduler = TaskiqScheduler( + broker=broker, + sources=[LabelScheduleSource(broker)], +) diff --git a/fanfan/infrastructure/scheduler/tasks/__init__.py b/fanfan/infrastructure/scheduler/tasks/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fanfan/infrastructure/scheduler/tasks/__init__.py @@ -0,0 +1 @@ + diff --git a/fanfan/infrastructure/scheduler/tasks.py b/fanfan/infrastructure/scheduler/tasks/notifications.py similarity index 98% rename from fanfan/infrastructure/scheduler/tasks.py rename to fanfan/infrastructure/scheduler/tasks/notifications.py index 8613e11..8080119 100644 --- a/fanfan/infrastructure/scheduler/tasks.py +++ b/fanfan/infrastructure/scheduler/tasks/notifications.py @@ -16,7 +16,7 @@ logger = logging.getLogger("__name__") -@broker.task +@broker.task() @inject async def send_notification( notification: UserNotification, @@ -53,7 +53,7 @@ async def send_notification( logger.info(f"Failed to send message to {notification.user_id}, skip") -@broker.task +@broker.task() @inject async def delete_message( message: Message, diff --git a/fanfan/infrastructure/scheduler/tasks/tickets.py b/fanfan/infrastructure/scheduler/tasks/tickets.py new file mode 100644 index 0000000..e1d17d5 --- /dev/null +++ b/fanfan/infrastructure/scheduler/tasks/tickets.py @@ -0,0 +1,81 @@ +import asyncio +import logging +import math + +from dishka import FromDishka +from dishka.integrations.taskiq import inject + +from fanfan.application.exceptions.ticket import TicketAlreadyExist, TicketNotFound +from fanfan.application.services import TicketService +from fanfan.common.enums import UserRole +from fanfan.config import TimepadConfig +from fanfan.infrastructure.db import UnitOfWork +from fanfan.infrastructure.scheduler import broker +from fanfan.infrastructure.timepad.client import TimepadClient +from fanfan.infrastructure.timepad.models import OrderStatus + +ORDERS_PER_REQUEST = 100 +PARTICIPANT_NOMINATIONS = [ + "Участник сценической программы", + "Участник не сценических конкурсов", +] + +logger = logging.getLogger("__name__") + + +@broker.task(schedule=[{"cron": "0 * * * *"}], retry_on_error=True, max_retries=3) +@inject +async def update_tickets( + client: FromDishka[TimepadClient], + config: FromDishka[TimepadConfig], + uow: FromDishka[UnitOfWork], +) -> None: + if not (config.client_id or config.event_id): + logger.info( + "TimePad client id or event id was not provided, skipping importing" + ) + return None + added_tickets, deleted_tickets = 0, 0 + step = 0 + service = TicketService(uow) + init = await client.get_orders(config.event_id) + logger.info(f"Tickets import started, about to process {init.total} orders") + while step != math.ceil(init.total / ORDERS_PER_REQUEST): + orders = await client.get_orders( + config.event_id, limit=ORDERS_PER_REQUEST, skip=step * ORDERS_PER_REQUEST + ) + for order in orders.values: + if order.status.name in [ + OrderStatus.PAID, + OrderStatus.OK, + OrderStatus.PAID_OFFLINE, + OrderStatus.PAID_UR, + ]: + for ticket in order.tickets: + try: + await service.create_ticket( + ticket_id=ticket.number, + role=UserRole.PARTICIPANT + if ticket.ticket_type.name in PARTICIPANT_NOMINATIONS + else UserRole.VISITOR, + ) + added_tickets += 1 + except TicketAlreadyExist: + pass + else: + for ticket in order.tickets: + try: + await service.delete_ticket(ticket_id=ticket.number) + deleted_tickets += 1 + except TicketNotFound: + pass + logger.info( + f"Ongoing import: {added_tickets} tickets added, {deleted_tickets} " + f"tickets deleted" + ) + await asyncio.sleep(3) + step += 1 + logger.info( + f"Import done: {added_tickets} tickets added, {deleted_tickets} tickets deleted" + ) + return None diff --git a/fanfan/infrastructure/timepad/__init__.py b/fanfan/infrastructure/timepad/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fanfan/infrastructure/timepad/client.py b/fanfan/infrastructure/timepad/client.py new file mode 100644 index 0000000..abeea72 --- /dev/null +++ b/fanfan/infrastructure/timepad/client.py @@ -0,0 +1,21 @@ +from aiohttp import ClientSession +from dataclass_rest import get +from dataclass_rest.http.aiohttp import AiohttpClient + +from fanfan.infrastructure.timepad.models import RegistrationOrdersResponse + +TIMEPAD_API_BASE_URL = "https://api.timepad.ru/" + + +class TimepadClient(AiohttpClient): + def __init__(self, session: ClientSession): + super().__init__(base_url=TIMEPAD_API_BASE_URL, session=session) + + @get("v1/events/{event_id}/orders") + async def get_orders( + self, + event_id: int, + limit: int = 10, + skip: int = 0, + ) -> RegistrationOrdersResponse: + pass diff --git a/fanfan/infrastructure/timepad/models.py b/fanfan/infrastructure/timepad/models.py new file mode 100644 index 0000000..a667c84 --- /dev/null +++ b/fanfan/infrastructure/timepad/models.py @@ -0,0 +1,53 @@ +import enum +from dataclasses import dataclass +from typing import List + + +class OrderStatus(enum.StrEnum): + NOT_PAID = "notpaid" + OK = "ok" + PAID = "paid" + INACTIVE = "inactive" + DELETED = "deleted" + BLOCKED = "blocked" + RETURNED = "returned" + BOOKED = "booked" + PENDING = "pending" + REJECTED = "rejected" + BOOKED_OFFLINE = "booked_offline" + PAID_OFFLINE = "paid_offline" + PAID_UR = "paid_ur" + TRANSFER_PAYMENT = "transfer_payment" + RETURN_PAYMENT_REQUEST = "return_payment_request" + RETURN_PAYMENT_REJECT = "return_payment_reject" + RETURN_ORG = "return_org" + RETURN_TP = "return_tp" + + +@dataclass +class TicketTypeResponse: + name: str + + +@dataclass +class TicketResponse: + number: str + ticket_type: TicketTypeResponse + + +@dataclass +class OrderStatusResponse: + name: OrderStatus + title: str + + +@dataclass +class RegistrationOrderResponse: + status: OrderStatusResponse + tickets: List[TicketResponse] + + +@dataclass +class RegistrationOrdersResponse: + total: int + values: List[RegistrationOrderResponse] diff --git a/fanfan/presentation/__main__.py b/fanfan/presentation/__main__.py index 378401d..2682f1b 100644 --- a/fanfan/presentation/__main__.py +++ b/fanfan/presentation/__main__.py @@ -15,13 +15,13 @@ from sqlalchemy.ext.asyncio import async_sessionmaker from starlette.middleware.cors import CORSMiddleware from starlette.middleware.sessions import SessionMiddleware -from taskiq.api import run_receiver_task +from taskiq.api import run_receiver_task, run_scheduler_task from fanfan.application.services import SettingsService from fanfan.common.enums import BotMode from fanfan.config import get_config from fanfan.infrastructure.db import UnitOfWork -from fanfan.infrastructure.scheduler import broker +from fanfan.infrastructure.scheduler import broker, scheduler from fanfan.presentation.admin import setup_admin from fanfan.presentation.tgbot.web.webapp import webapp_router from fanfan.presentation.tgbot.web.webhook import webhook_router @@ -50,7 +50,8 @@ async def lifespan(app: FastAPI): # Run scheduler setup_dishka_taskiq(app_container, broker) - worker_task = asyncio.create_task(run_receiver_task(broker, run_startup=True)) + worker_task = asyncio.create_task(run_receiver_task(broker)) + scheduler_task = asyncio.create_task(run_scheduler_task(scheduler)) async with app_container() as request_container: uow = await request_container.get(UnitOfWork) @@ -78,7 +79,8 @@ async def lifespan(app: FastAPI): bot_task = asyncio.create_task(dp.start_polling(bot)) logger.info("Running in polling mode") yield - logger.info("Stopping scheduler worker...") + logger.info("Stopping scheduler...") + scheduler_task.cancel() worker_task.cancel() await broker.shutdown() logger.info("Stopping bot...") @@ -126,6 +128,7 @@ def create_app() -> FastAPI: host=config.web.host, port=config.web.port, app=create_app(), + log_level=logging.ERROR, ) except KeyboardInterrupt: pass diff --git a/poetry.lock b/poetry.lock index ec3e8cd..c8e8f68 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,24 @@ # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +[[package]] +name = "adaptix" +version = "3.0.0b5" +description = "An extremely flexible and configurable data model conversion library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "adaptix-3.0.0b5-py3-none-any.whl", hash = "sha256:3bf98b05a8a65f28f78515eac035c1c275ab689cf318db8ae5a06e518aa8af67"}, + {file = "adaptix-3.0.0b5.tar.gz", hash = "sha256:426fb1ced59f06b80e59265dabb70f68567c6dfd7fb322fa96cd99d1178bceaa"}, +] + +[package.extras] +attrs = ["attrs (>=21.3.0)"] +attrs-strict = ["attrs (>=21.3.0,<=23.2.0)"] +pydantic = ["pydantic (>=2.0.0)"] +pydantic-strict = ["pydantic (>=2.0.0,<=2.7.0)"] +sqlalchemy = ["sqlalchemy (>=2.0.0)"] +sqlalchemy-strict = ["sqlalchemy (>=2.0.0,<=2.0.29)"] + [[package]] name = "aiodns" version = "3.2.0" @@ -310,17 +329,17 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "boto3" -version = "1.34.91" +version = "1.34.92" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.91-py3-none-any.whl", hash = "sha256:97fac686c47647db4b44e4789317e4aeecd38511d71e84f8d20abe33eb630ff1"}, - {file = "boto3-1.34.91.tar.gz", hash = "sha256:5077917041adaaae15eeca340289547ef905ca7e11516e9bd22d394fb5057d2a"}, + {file = "boto3-1.34.92-py3-none-any.whl", hash = "sha256:db7bbb1c6059e99b74dcf634e497b04addcac4c527ae2b2696e47c39eccc6c50"}, + {file = "boto3-1.34.92.tar.gz", hash = "sha256:684cba753d64978a486e8ea9645d53de0d4e3b4a3ab1495b26bd04b9541cea2d"}, ] [package.dependencies] -botocore = ">=1.34.91,<1.35.0" +botocore = ">=1.34.92,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -329,13 +348,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.91" +version = "1.34.92" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.91-py3-none-any.whl", hash = "sha256:4d1b13f2b1c28ce1743b1e5895ae62bb7e67f892b51882164ea19c27a130852b"}, - {file = "botocore-1.34.91.tar.gz", hash = "sha256:93ef7071292a1b2b9fc26537f8ae3a8227da1177969241939ea3fbdb1a1a1d0c"}, + {file = "botocore-1.34.92-py3-none-any.whl", hash = "sha256:4211a22a1f6c6935e70cbb84c2cd93b29f9723eaf5036d59748dd104f389a681"}, + {file = "botocore-1.34.92.tar.gz", hash = "sha256:d1ca4886271f184445ec737cd2e752498648cca383887c5a37b2e01c8ab94039"}, ] [package.dependencies] @@ -457,6 +476,20 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "dataclass-rest" +version = "0.4" +description = "An utility for writing simple clients for REST like APIs" +optional = false +python-versions = ">=3.6" +files = [ + {file = "dataclass_rest-0.4-py3-none-any.whl", hash = "sha256:fbbcccc1b1db70b6836e4d11a1e093bfac042a8664b67905cd8c5db9ccabaa4e"}, + {file = "dataclass_rest-0.4.tar.gz", hash = "sha256:3a7a3d8a17465ef73a7a6d5d5479a3271599c47562c67adaec3e20dff68cd8c5"}, +] + +[package.dependencies] +adaptix = "*" + [[package]] name = "dishka" version = "1.1.0" @@ -1630,28 +1663,28 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "ruff" -version = "0.4.1" +version = "0.4.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2d9ef6231e3fbdc0b8c72404a1a0c46fd0dcea84efca83beb4681c318ea6a953"}, - {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9485f54a7189e6f7433e0058cf8581bee45c31a25cd69009d2a040d1bd4bfaef"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2921ac03ce1383e360e8a95442ffb0d757a6a7ddd9a5be68561a671e0e5807e"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eec8d185fe193ad053eda3a6be23069e0c8ba8c5d20bc5ace6e3b9e37d246d3f"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa27d9d72a94574d250f42b7640b3bd2edc4c58ac8ac2778a8c82374bb27984"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f1ee41580bff1a651339eb3337c20c12f4037f6110a36ae4a2d864c52e5ef954"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0926cefb57fc5fced629603fbd1a23d458b25418681d96823992ba975f050c2b"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6e37f2e3cd74496a74af9a4fa67b547ab3ca137688c484749189bf3a686ceb"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd703a5975ac1998c2cc5e9494e13b28f31e66c616b0a76e206de2562e0843c"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b92f03b4aa9fa23e1799b40f15f8b95cdc418782a567d6c43def65e1bbb7f1cf"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1c859f294f8633889e7d77de228b203eb0e9a03071b72b5989d89a0cf98ee262"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b34510141e393519a47f2d7b8216fec747ea1f2c81e85f076e9f2910588d4b64"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6e68d248ed688b9d69fd4d18737edcbb79c98b251bba5a2b031ce2470224bdf9"}, - {file = "ruff-0.4.1-py3-none-win32.whl", hash = "sha256:b90506f3d6d1f41f43f9b7b5ff845aeefabed6d2494307bc7b178360a8805252"}, - {file = "ruff-0.4.1-py3-none-win_amd64.whl", hash = "sha256:c7d391e5936af5c9e252743d767c564670dc3889aff460d35c518ee76e4b26d7"}, - {file = "ruff-0.4.1-py3-none-win_arm64.whl", hash = "sha256:a1eaf03d87e6a7cd5e661d36d8c6e874693cb9bc3049d110bc9a97b350680c43"}, - {file = "ruff-0.4.1.tar.gz", hash = "sha256:d592116cdbb65f8b1b7e2a2b48297eb865f6bdc20641879aa9d7b9c11d86db79"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, + {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, + {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, + {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, + {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, ] [[package]] @@ -1673,18 +1706,18 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "sentry-sdk" -version = "1.45.0" +version = "2.0.1" description = "Python client for Sentry (https://sentry.io)" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "sentry-sdk-1.45.0.tar.gz", hash = "sha256:509aa9678c0512344ca886281766c2e538682f8acfa50fd8d405f8c417ad0625"}, - {file = "sentry_sdk-1.45.0-py2.py3-none-any.whl", hash = "sha256:1ce29e30240cc289a027011103a8c83885b15ef2f316a60bcc7c5300afa144f1"}, + {file = "sentry_sdk-2.0.1-py2.py3-none-any.whl", hash = "sha256:b54c54a2160f509cf2757260d0cf3885b608c6192c2555a3857e3a4d0f84bdb3"}, + {file = "sentry_sdk-2.0.1.tar.gz", hash = "sha256:c278e0f523f6f0ee69dc43ad26dcdb1202dffe5ac326ae31472e012d941bee21"}, ] [package.dependencies] certifi = "*" -urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} +urllib3 = ">=1.26.11" [package.extras] aiohttp = ["aiohttp (>=3.5)"] @@ -2174,4 +2207,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "50922b27d61a939c7a7b7efbc5b0ea293e39d952e69b9381743f7964f17cdc73" +content-hash = "dcd058bfb11d761d82e913a2b87f535a263c9b259e48c7a840a16c2f31cd28de" diff --git a/pyproject.toml b/pyproject.toml index 0595b4b..05535c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ aiogram = {version = "^3.5.0", extras = ["fast"]} aiogram-dialog = "^2.1.0" alembic = "^1.13.1" asyncpg = "^0.29.0" +dataclass-rest = "^0.4" dishka = "^1.1.0" fastapi = "^0.110.1" fastapi-storages = "^0.3.0" @@ -25,7 +26,7 @@ pydantic-settings = "^2.2.1" pyjwt = "^2.8.0" qrcode = {version = "^7.4.2", extras = ["pil"]} redis = "^5.0.4" -sentry-sdk = "^1.45.0" +sentry-sdk = "^2.0.1" sqladmin = "^0.16.1" sqlalchemy = "^2.0.29" sulguk = "^0.7.0"