Skip to content

Commit 278f4f1

Browse files
committed
feat: implement uow
1 parent 9bbf395 commit 278f4f1

13 files changed

Lines changed: 117 additions & 24 deletions

File tree

src/modules/todo/application/create_todo/handler.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ async def execute(self, command: CreateTodoCommand, user_id: UUID) -> Todo:
1515
todo = Todo.create(
1616
title=command.title, user_id=user_id, description=command.description
1717
)
18-
try:
18+
async with self._unit_of_work:
1919
saved_todo = await self.todo_repo.save(todo)
2020
await self._unit_of_work.commit()
2121
return saved_todo
22-
except Exception:
23-
await self._unit_of_work.rollback()
24-
raise

src/modules/todo/application/delete_todo/handler.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ async def execute(self, todo_id: UUID, user_id: UUID) -> None:
2222
"You do not have permission to delete this todo"
2323
)
2424

25-
try:
25+
async with self._unit_of_work:
2626
await self.todo_repo.delete(todo_id)
2727
await self._unit_of_work.commit()
28-
except Exception:
29-
await self._unit_of_work.rollback()
30-
raise

src/modules/todo/application/update_todo/handler.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,7 @@ async def execute(
4040
else:
4141
todo.is_completed = False
4242

43-
try:
43+
async with self._unit_of_work:
4444
saved_todo = await self.todo_repo.save(todo)
4545
await self._unit_of_work.commit()
4646
return saved_todo
47-
except Exception:
48-
await self._unit_of_work.rollback()
49-
raise

src/modules/user/application/login_user/handler.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,12 @@ async def execute(self, command: LoginUserCommand) -> dict[str, str]:
4444
minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES
4545
)
4646

47-
try:
47+
async with self._unit_of_work:
4848
new_rt = RefreshToken.create(
4949
user_id=user.id, token_hash=token_hash, expires_at=expires_at
5050
)
5151
await self._refresh_token_repository.save(new_rt)
5252
await self._unit_of_work.commit()
53-
except Exception:
54-
await self._unit_of_work.rollback()
55-
raise
5653

5754
return {
5855
"access_token": access_token,

src/modules/user/application/refresh_token/handler.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async def execute(self, command: RefreshTokenCommand) -> dict:
3535
if stored_token.expires_at < datetime.now(timezone.utc):
3636
raise InvalidRefreshTokenError("Refresh token has expired")
3737

38-
try:
38+
async with self._unit_of_work:
3939
stored_token.revoke()
4040
await self._refresh_token_repo.save(stored_token)
4141

@@ -61,9 +61,6 @@ async def execute(self, command: RefreshTokenCommand) -> dict:
6161

6262
await self._refresh_token_repo.save(new_refresh_token_entity)
6363
await self._unit_of_work.commit()
64-
except Exception:
65-
await self._unit_of_work.rollback()
66-
raise
6764

6865
return {
6966
"access_token": new_access_token,

src/modules/user/application/register_user/handler.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ async def execute(self, command: RegisterUserCommand) -> User:
2121
command.email,
2222
password=hashed_password,
2323
)
24-
try:
24+
async with self._unit_of_work:
2525
saved_user = await self._user_repository.save(user=user)
2626
await self._unit_of_work.commit()
2727
return saved_user
28-
except Exception:
29-
await self._unit_of_work.rollback()
30-
raise

src/shared/database/unit_of_work.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from types import TracebackType
2+
from typing import Self
3+
14
from sqlalchemy.ext.asyncio import AsyncSession
25

36
from src.shared.unit_of_work import UnitOfWork
@@ -6,9 +9,25 @@
69
class SQLAlchemyUnitOfWork(UnitOfWork):
710
def __init__(self, session: AsyncSession):
811
self._session = session
12+
self._committed = False
13+
14+
async def __aenter__(self) -> Self:
15+
self._committed = False
16+
return self
17+
18+
async def __aexit__(
19+
self,
20+
exc_type: type[BaseException] | None,
21+
exc: BaseException | None,
22+
traceback: TracebackType | None,
23+
) -> bool:
24+
if exc_type is not None or not self._committed:
25+
await self.rollback()
26+
return False
927

1028
async def commit(self) -> None:
1129
await self._session.commit()
30+
self._committed = True
1231

1332
async def rollback(self) -> None:
1433
await self._session.rollback()

src/shared/unit_of_work.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
from abc import ABC, abstractmethod
2+
from types import TracebackType
3+
from typing import Self
24

35

46
class UnitOfWork(ABC):
7+
@abstractmethod
8+
async def __aenter__(self) -> Self:
9+
pass
10+
11+
@abstractmethod
12+
async def __aexit__(
13+
self,
14+
exc_type: type[BaseException] | None,
15+
exc: BaseException | None,
16+
traceback: TracebackType | None,
17+
) -> bool:
18+
pass
19+
520
@abstractmethod
621
async def commit(self) -> None:
722
pass

tests/shared/test_unit_of_work.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,48 @@ async def run():
3737
assert session.rolled_back is True
3838

3939
asyncio.run(run())
40+
41+
42+
def test_sqlalchemy_unit_of_work_rolls_back_when_scope_exits_without_commit():
43+
async def run():
44+
session = FakeSession()
45+
unit_of_work = SQLAlchemyUnitOfWork(session)
46+
47+
async with unit_of_work:
48+
pass
49+
50+
assert session.rolled_back is True
51+
assert session.committed is False
52+
53+
asyncio.run(run())
54+
55+
56+
def test_sqlalchemy_unit_of_work_does_not_roll_back_after_commit():
57+
async def run():
58+
session = FakeSession()
59+
unit_of_work = SQLAlchemyUnitOfWork(session)
60+
61+
async with unit_of_work:
62+
await unit_of_work.commit()
63+
64+
assert session.committed is True
65+
assert session.rolled_back is False
66+
67+
asyncio.run(run())
68+
69+
70+
def test_sqlalchemy_unit_of_work_rolls_back_and_propagates_scope_exception():
71+
async def run():
72+
session = FakeSession()
73+
unit_of_work = SQLAlchemyUnitOfWork(session)
74+
75+
try:
76+
async with unit_of_work:
77+
raise RuntimeError("write failed")
78+
except RuntimeError as exc:
79+
assert str(exc) == "write failed"
80+
81+
assert session.rolled_back is True
82+
assert session.committed is False
83+
84+
asyncio.run(run())

tests/todo/test_todo_flow.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ def __init__(self):
3232
self.committed = False
3333
self.rolled_back = False
3434

35+
async def __aenter__(self):
36+
return self
37+
38+
async def __aexit__(self, exc_type, exc, traceback):
39+
if exc_type is not None or not self.committed:
40+
await self.rollback()
41+
return False
42+
3543
async def commit(self):
3644
self.committed = True
3745

0 commit comments

Comments
 (0)