Skip to content

Commit 890ba58

Browse files
refactor lint
1 parent 5b742e2 commit 890ba58

File tree

12 files changed

+212
-72
lines changed

12 files changed

+212
-72
lines changed

app/infrastructure/containers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import asyncio
2+
from typing import Callable as TypeCallable
23

34
from dependency_injector import containers, providers
45
from dependency_injector.providers import Callable, Factory
56
from httpx import AsyncClient, Limits
67
from pybotx import Bot, HandlerCollector
78
from redis import asyncio as aioredis
9+
from sqlalchemy.ext.asyncio import AsyncSession
810

911
from app.application.use_cases.record_use_cases import SampleRecordUseCases
1012
from app.infrastructure.repositories.caching.callback_redis_repo import (
@@ -29,7 +31,9 @@
2931

3032

3133
class BotSampleRecordCommandContainer(containers.DeclarativeContainer):
32-
session_factory = providers.Dependency()
34+
session_factory: providers.Dependency[TypeCallable[[], AsyncSession]] = (
35+
providers.Dependency()
36+
)
3337

3438
ro_unit_of_work: Factory[ReadOnlySampleRecordUnitOfWork] = Factory(
3539
ReadOnlySampleRecordUnitOfWork, session_factory

app/infrastructure/repositories/sample_record.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@
2121
from app.decorators.mapper.exception_mapper import (
2222
ExceptionMapper,
2323
)
24-
from app.decorators.mapper.factories import EnrichedExceptionFactory
24+
from app.decorators.mapper.factories import ContextAwareError, EnrichedExceptionFactory
2525
from app.domain.entities.sample_record import SampleRecord
2626
from app.infrastructure.db.sample_record.models import SampleRecordModel
2727
from app.infrastructure.db.sqlalchemy import AsyncSession
2828

2929

3030
class IntegrityErrorFactory(EnrichedExceptionFactory):
31-
def make_exception(self, context: ExceptionContext) -> Exception:
31+
def make_exception(self, context: ExceptionContext) -> ContextAwareError:
3232
if not (orig := getattr(context.original_exception, "orig", None)):
3333
return self.generated_error(context.formatted_context)
3434

app/infrastructure/repositories/unit_of_work.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import asyncio
1+
from types import TracebackType
2+
from typing import Self
23

34
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
45

@@ -10,38 +11,56 @@
1011

1112

1213
class ReadOnlySampleRecordUnitOfWork(ISampleRecordUnitOfWork):
14+
def __init__(self, session_factory: async_sessionmaker):
15+
super().__init__()
16+
self.session_factory = session_factory
17+
self._session: AsyncSession | None = None
18+
1319
def get_sample_record_repository(self) -> ISampleRecordRepository:
1420
if not self._session:
1521
raise RuntimeError("Session is not initialized")
1622

1723
return SampleRecordRepository(self._session)
1824

19-
def __init__(self, session_factory: async_sessionmaker):
20-
super().__init__()
21-
self.session_factory = session_factory
22-
self._session: AsyncSession | None = None
23-
24-
async def __aenter__(self):
25+
async def __aenter__(self) -> Self:
2526
self._session = self.session_factory()
2627
return self
2728

28-
async def __aexit__(self, exc_type, exc_val, exc_tb):
29+
async def __aexit__(
30+
self,
31+
exc_type: type[BaseException] | None,
32+
exc_val: BaseException | None,
33+
exc_tb: TracebackType | None,
34+
) -> None:
35+
if not self._session:
36+
return
37+
2938
try:
30-
# Recommended for explicit resources cleanup
31-
await self._session.rollback()
39+
if exc_type is not None or self._session.in_transaction():
40+
await self._session.rollback()
3241
finally:
3342
await self._session.close()
43+
self._session = None
3444

3545

3646
class WriteSampleRecordUnitOfWork(ReadOnlySampleRecordUnitOfWork):
3747
"""Unit of Work for write operations with full transaction management."""
3848

39-
async def __aenter__(self):
49+
async def __aenter__(self) -> Self:
4050
self._session = self.session_factory()
41-
await asyncio.wait_for(self._session.begin(), timeout=5)
51+
52+
await self._session.begin()
4253
return self
4354

44-
async def __aexit__(self, exc_type, exc_val, exc_tb):
55+
async def __aexit__(
56+
self,
57+
exc_type: type[BaseException] | None,
58+
exc_val: BaseException | None,
59+
exc_tb: TracebackType | None,
60+
) -> None:
61+
if not self._session:
62+
return
63+
4564
try:
4665
if exc_type:
4766
await self._session.rollback()

app/presentation/bot/command_handlers/base_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def __init__(
1515
self,
1616
bot: Bot,
1717
message: IncomingMessage,
18-
exception_handler_executor: ExceptionHandlersChainExecutor | None,
18+
exception_handler_executor: ExceptionHandlersChainExecutor,
1919
):
2020
self._bot = bot
2121
self._message = message

app/presentation/bot/command_handlers/sample_record.py

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from app.presentation.bot.error_handlers.exceptions_chain_executor import (
2020
DEFAULT_HANDLERS_WITH_EXPLAIN,
2121
ExceptionHandlersChainExecutor,
22+
HandlerOrHandlerClass,
2223
)
2324
from app.presentation.bot.resources.strings import (
2425
SAMPLE_RECORD_CREATED_ANSWER,
@@ -39,15 +40,18 @@
3940
class CreateSampleRecordHandler(BaseCommandHandler):
4041
incoming_argument_parser = BotXJsonRequestParser(SampleRecordCreateRequestSchema)
4142

42-
_EXCEPTIONS_HANDLERS = DEFAULT_HANDLERS_WITH_EXPLAIN + [
43-
SendErrorExplainToUserHandler(
44-
exception_explain_mapping={
45-
RecordAlreadyExistsError: "Запись с такими параметрами уже существует",
46-
RecordCreateError: "Внутренняя ошибка создания записи",
47-
MessageValidationError: "Неправильный формат данных",
48-
}
49-
)
50-
]
43+
_EXCEPTIONS_HANDLERS: list[HandlerOrHandlerClass] = (
44+
DEFAULT_HANDLERS_WITH_EXPLAIN
45+
+ [
46+
SendErrorExplainToUserHandler(
47+
exception_explain_mapping={
48+
RecordAlreadyExistsError: "Запись с такими параметрами существует",
49+
RecordCreateError: "Внутренняя ошибка создания записи",
50+
MessageValidationError: "Неправильный формат данных",
51+
}
52+
)
53+
]
54+
)
5155

5256
def __init__(
5357
self,
@@ -87,14 +91,17 @@ class DeleteSampleRecordHandler(BaseCommandHandler):
8791
SampleRecordGetOrDeleteRequestSchema
8892
)
8993

90-
_EXCEPTIONS_HANDLERS = DEFAULT_HANDLERS_WITH_EXPLAIN + [
91-
SendErrorExplainToUserHandler(
92-
exception_explain_mapping={
93-
RecordDoesNotExistError: "Запиcь с указанным id не найдена",
94-
MessageValidationError: "Неправильный формат данных",
95-
}
96-
)
97-
]
94+
_EXCEPTIONS_HANDLERS: list[HandlerOrHandlerClass] = (
95+
DEFAULT_HANDLERS_WITH_EXPLAIN
96+
+ [
97+
SendErrorExplainToUserHandler(
98+
exception_explain_mapping={
99+
RecordDoesNotExistError: "Запиcь с указанным id не найдена",
100+
MessageValidationError: "Неправильный формат данных",
101+
}
102+
)
103+
]
104+
)
98105

99106
def __init__(
100107
self,
@@ -132,14 +139,17 @@ class GetSampleRecordHandler(BaseCommandHandler):
132139
SampleRecordGetOrDeleteRequestSchema
133140
)
134141

135-
_EXCEPTIONS_HANDLERS = DEFAULT_HANDLERS_WITH_EXPLAIN + [
136-
SendErrorExplainToUserHandler(
137-
exception_explain_mapping={
138-
RecordDoesNotExistError: "Запиcь с указанным id не найдена",
139-
MessageValidationError: "Неправильный формат данных",
140-
}
141-
)
142-
]
142+
_EXCEPTIONS_HANDLERS: list[HandlerOrHandlerClass] = (
143+
DEFAULT_HANDLERS_WITH_EXPLAIN
144+
+ [
145+
SendErrorExplainToUserHandler(
146+
exception_explain_mapping={
147+
RecordDoesNotExistError: "Запиcь с указанным id не найдена",
148+
MessageValidationError: "Неправильный формат данных",
149+
}
150+
)
151+
]
152+
)
143153
exception_handler_chain_executor = ExceptionHandlersChainExecutor(
144154
_EXCEPTIONS_HANDLERS
145155
)
@@ -162,7 +172,7 @@ def __init__(
162172

163173
async def handle_logic(
164174
self,
165-
request_parameter: SampleRecordGetOrDeleteRequestSchema,
175+
request_parameter: SampleRecordGetOrDeleteRequestSchema, # type: ignore
166176
) -> None:
167177
async with self.unit_of_work as uof:
168178
record = await self._use_cases(

app/presentation/bot/error_handlers/base_handlers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,11 @@ async def handle_exception(
6767
await self.next_handler.handle_exception(
6868
exc, bot, message, exception_id
6969
)
70-
except Exception as exc:
70+
except Exception as inner_exc:
7171
logger.error(
7272
f"Error handling exception {exception_id}",
7373
exc_info=True,
74+
exc=inner_exc,
7475
)
7576
if self._stop_on_failure:
7677
return

app/presentation/bot/error_handlers/exceptions_chain_executor.py

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from abc import ABCMeta
12
from uuid import uuid4
23

34
from pybotx import Bot, IncomingMessage
@@ -9,6 +10,10 @@
910
SendErrorExplainToUserHandler,
1011
)
1112

13+
HandlerOrHandlerClass = (
14+
AbstractExceptionHandler | type[AbstractExceptionHandler] | ABCMeta
15+
)
16+
1217

1318
class ExceptionHandlersChainExecutor:
1419
"""
@@ -24,7 +29,8 @@ class ExceptionHandlersChainExecutor:
2429
"""
2530

2631
def __init__(
27-
self, handlers: list[type[AbstractExceptionHandler] | AbstractExceptionHandler]
32+
self,
33+
handlers: list[HandlerOrHandlerClass] | None = None,
2834
):
2935
self._chain_head, self._chain_tail = self._create_chain(handlers)
3036

@@ -52,13 +58,26 @@ async def execute_chain(
5258
await self._chain_head.handle_exception(exc, bot, message, exception_id)
5359

5460
def _get_handler(
55-
self, handler: AbstractExceptionHandler | type[AbstractExceptionHandler]
61+
self,
62+
handler: HandlerOrHandlerClass,
5663
) -> AbstractExceptionHandler:
5764
return handler if isinstance(handler, AbstractExceptionHandler) else handler()
5865

5966
def _create_chain(
60-
self, handlers: list[type[AbstractExceptionHandler] | AbstractExceptionHandler]
67+
self,
68+
handlers: list[HandlerOrHandlerClass] | None = None,
6169
) -> tuple[AbstractExceptionHandler | None, AbstractExceptionHandler | None]:
70+
"""
71+
Create a linked list of exception handlers from the given list.
72+
73+
This method takes a sequence of exception handler classes or instances
74+
and chains them together into a linked list. The returned tuple contains
75+
the head and tail of the constructed chain.
76+
77+
warning:
78+
This method modifies the passed objects of
79+
class:`AbstractExceptionHandler` type in place.
80+
"""
6281
if not handlers:
6382
return None, None
6483

@@ -71,29 +90,39 @@ def _create_chain(
7190
tail_handler = new_tail_handler
7291
return head_handler, tail_handler
7392

74-
def extend(
75-
self, handlers: list[AbstractExceptionHandler | type[AbstractExceptionHandler]]
76-
):
93+
def extend(self, handlers: list[HandlerOrHandlerClass] | None) -> None:
7794
"""Append handlers to the chain"""
95+
if not handlers:
96+
return
97+
7898
new_head, new_tail = self._create_chain(handlers)
79-
if self._chain_head is None:
99+
if self._is_empty():
80100
self._chain_head = new_head
81101
else:
82-
self._chain_tail.next_handler = new_head
102+
# The tail and head cannot be None at the same time.
103+
self._chain_tail.next_handler = new_head # type: ignore
83104

84105
self._chain_tail = new_tail
85106

86-
def append(
87-
self, handler: AbstractExceptionHandler | type[AbstractExceptionHandler]
88-
):
89-
"""Append handler to the end of chain"""
107+
def append(self, handler: HandlerOrHandlerClass) -> None:
108+
"""Append handler to the end of a chain"""
90109
new_tail = self._get_handler(handler)
91-
self._chain_tail.next_handler = new_tail
92110

111+
if self._is_empty():
112+
self._chain_head = new_tail
113+
self._chain_tail = new_tail
114+
else:
115+
self._chain_tail.next_handler = new_tail # type:ignore
116+
117+
def _is_empty(self) -> bool:
118+
return self._chain_head is None and self._chain_tail is None
93119

94-
DEFAULT_HANDLERS = [
120+
121+
DEFAULT_HANDLERS: list[HandlerOrHandlerClass] = [
95122
LoggingExceptionHandler,
96123
DropFSMOnErrorHandler,
97124
]
98125

99-
DEFAULT_HANDLERS_WITH_EXPLAIN = DEFAULT_HANDLERS + [SendErrorExplainToUserHandler]
126+
DEFAULT_HANDLERS_WITH_EXPLAIN: list[HandlerOrHandlerClass] = DEFAULT_HANDLERS + [
127+
SendErrorExplainToUserHandler
128+
]

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ deepdiff = "^8.5.0"
6464

6565

6666

67+
[tool.poetry.group.dev.dependencies]
68+
types-psycopg2 = "^2.9.21.20250718"
69+
6770
[build-system]
6871
requires = ["poetry>=1.1.12"]
6972
build-backend = "poetry.masonry.api"

tests/integration/bot_commands/bot_factories.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,14 @@ class Meta:
7171
class IncomingMessageFactory(Factory):
7272
"""Factory for incoming messages."""
7373

74-
bot: BotAccount = factory.SubFactory(BotAccountFactory)
75-
sync_id: UUID = factory.Faker("uuid4")
74+
bot: BotAccount = factory.SubFactory(BotAccountFactory) # type: ignore
75+
sync_id: UUID = factory.Faker("uuid4") # type: ignore
7676
source_sync_id: Optional[UUID] = None
77-
body: str = factory.Faker("text", max_nb_chars=100)
77+
body: str = factory.Faker("text", max_nb_chars=100) # type: ignore
7878
data: dict[str, Any] = {}
7979
metadata: dict = {}
80-
sender: UserSender = factory.SubFactory(UserSenderFactory)
81-
chat: Chat = factory.SubFactory(ChatFactory)
80+
sender: UserSender = factory.SubFactory(UserSenderFactory) # type: ignore
81+
chat: Chat = factory.SubFactory(ChatFactory) # type: ignore
8282
raw_command: Optional[str] = None
8383

8484
class Meta:

0 commit comments

Comments
 (0)