Skip to content

Commit 7b94131

Browse files
committed
[ISSUE-33] Add clients with cache and reduce
1 parent 006166c commit 7b94131

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+2025
-481
lines changed

.env.dev

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
APP_DB_DSN=
1+
APP_DB_DSN=
2+
APP_DB_POOL_SIZE=
3+
APP_DB_POOL_TIMEOUT=
4+
APP_DB_MAX_OVERFLOW=
5+
6+
APP_REDIS_DSN=

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,4 @@ cython_debug/
159159
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160160
.idea/
161161
.vscode/
162+
.DS_Store

docker-compose.dev.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
services:
2+
redis:
3+
image: redis:latest
4+
ports:
5+
- "6379:6379"
26
db:
37
image: postgres:16
48
ports:

docker-compose.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ services:
1616
volumes:
1717
- postgres_data:/var/lib/postgresql/data
1818

19+
redis:
20+
image: redis:latest
21+
ports:
22+
- "${REDIS_PORT}:6379"
23+
1924
app:
2025
image: andytakker/example-web-service:latest
2126
command: granian --interface asgi --host 0.0.0.0 --port 8080 --loop uvloop library.main:app --access-log --log-level info

library/adapters/database/alembic.ini

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
script_location = %(here)s/migrations
33
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s
44
prepend_sys_path = .
5-
version_path_separator = os
5+
path_separator = os
66

77
[post_write_hooks]
88

@@ -39,4 +39,3 @@ formatter = generic
3939
[formatter_generic]
4040
format = %(levelname)-5.5s [%(name)s] %(message)s
4141
datefmt = %H:%M:%S
42-

library/adapters/database/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ class DatabaseConfig:
1111
pool_timeout: int = field(
1212
default_factory=lambda: int(environ.get("APP_DB_POOL_TIMEOUT", 10))
1313
)
14+
max_overflow: int = field(
15+
default_factory=lambda: int(environ.get("APP_DB_MAX_OVERFLOW", 10))
16+
)

library/adapters/database/di.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,28 @@ def __init__(
1717
self,
1818
dsn: str,
1919
debug: bool,
20+
pool_size: int,
21+
pool_timeout: int,
22+
max_overflow: int,
2023
scope: BaseScope | None = None,
2124
component: Component | None = None,
2225
) -> None:
23-
self.dsn = dsn
24-
self.debug = debug
26+
self._dsn = dsn
27+
self._debug = debug
28+
self._pool_size = pool_size
29+
self._pool_timeout = pool_timeout
30+
self._max_overflow = max_overflow
2531
super().__init__(scope=scope, component=component)
2632

2733
@provide(scope=Scope.APP)
2834
async def engine(self) -> AsyncIterator[AsyncEngine]:
29-
async with create_engine(dsn=self.dsn, debug=self.debug) as engine:
35+
async with create_engine(
36+
dsn=self._dsn,
37+
debug=self._debug,
38+
pool_size=self._pool_size,
39+
pool_timeout=self._pool_timeout,
40+
max_overflow=self._max_overflow,
41+
) as engine:
3042
yield engine
3143

3244
@provide(scope=Scope.APP)
@@ -37,12 +49,12 @@ def session_factory(self, engine: AsyncEngine) -> async_sessionmaker[AsyncSessio
3749
def uow(
3850
self, session_factory: async_sessionmaker[AsyncSession]
3951
) -> AnyOf[SqlalchemyUow, AbstractUow]:
40-
return SqlalchemyUow(session=session_factory())
52+
return SqlalchemyUow(session_factory=session_factory)
4153

4254
@provide(scope=Scope.REQUEST)
4355
def book_storage(self, uow: SqlalchemyUow) -> IBookStorage:
44-
return BookStorage(session=uow.session)
56+
return BookStorage(uow=uow)
4557

4658
@provide(scope=Scope.REQUEST)
4759
def user_storage(self, uow: SqlalchemyUow) -> IUserStorage:
48-
return UserStorage(session=uow.session)
60+
return UserStorage(uow=uow)

library/adapters/database/storages/book.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from sqlalchemy.ext.asyncio import AsyncSession
88

99
from library.adapters.database.tables import BookTable
10+
from library.adapters.database.uow import SqlalchemyUow
1011
from library.application.exceptions import (
1112
EntityAlreadyExistsException,
1213
EntityNotFoundException,
@@ -19,19 +20,22 @@
1920
CreateBook,
2021
UpdateBook,
2122
)
22-
from library.domains.interfaces.storages.book import IBookStorage
2323

2424

25-
class BookStorage(IBookStorage):
26-
def __init__(self, session: AsyncSession) -> None:
27-
self.session = session
25+
class BookStorage:
26+
def __init__(self, *, uow: SqlalchemyUow) -> None:
27+
self._uow = uow
28+
29+
@property
30+
def _session(self) -> AsyncSession:
31+
return self._uow.session
2832

2933
async def fetch_book_by_id(self, *, book_id: BookId) -> Book | None:
3034
query = select(BookTable).where(
3135
BookTable.id == book_id,
3236
BookTable.deleted_at.is_(None),
3337
)
34-
book = (await self.session.scalars(query)).first()
38+
book = (await self._session.scalars(query)).first()
3539

3640
if book is None:
3741
return None
@@ -48,15 +52,15 @@ async def exists_book_by_id(self, *, book_id: BookId) -> bool:
4852
stmt = select(
4953
exists().where(BookTable.id == book_id, BookTable.deleted_at.is_(None))
5054
)
51-
return bool((await self.session.execute(stmt)).scalar())
55+
return bool((await self._session.execute(stmt)).scalar())
5256

5357
async def count_books(self, *, params: BookPaginationParams) -> int:
5458
query = (
5559
select(func.count())
5660
.select_from(BookTable)
5761
.where(BookTable.deleted_at.is_(None))
5862
)
59-
result = (await self.session.execute(query)).scalar()
63+
result = (await self._session.execute(query)).scalar()
6064
return result or 0
6165

6266
async def fetch_book_list(self, *, params: BookPaginationParams) -> Sequence[Book]:
@@ -74,7 +78,7 @@ async def fetch_book_list(self, *, params: BookPaginationParams) -> Sequence[Boo
7478
.offset(params.offset)
7579
.order_by(BookTable.id)
7680
)
77-
result = (await self.session.execute(query)).mappings().all()
81+
result = (await self._session.execute(query)).mappings().all()
7882
return [
7983
Book(
8084
id=book["id"],
@@ -105,7 +109,7 @@ async def create_book(self, *, book: CreateBook) -> Book:
105109
)
106110
)
107111
try:
108-
result = (await self.session.execute(stmt)).mappings().one()
112+
result = (await self._session.execute(stmt)).mappings().one()
109113
except IntegrityError as e:
110114
self._raise_error(e)
111115
return Book(
@@ -123,7 +127,7 @@ async def delete_book_by_id(self, *, book_id: BookId) -> None:
123127
.where(BookTable.id == book_id)
124128
.values(deleted_at=datetime.now(tz=UTC))
125129
)
126-
await self.session.execute(stmt)
130+
await self._session.execute(stmt)
127131

128132
async def update_book_by_id(self, *, update_book: UpdateBook) -> Book:
129133
stmt = (
@@ -140,7 +144,7 @@ async def update_book_by_id(self, *, update_book: UpdateBook) -> Book:
140144
)
141145
)
142146
try:
143-
result = (await self.session.execute(stmt)).mappings().one()
147+
result = (await self._session.execute(stmt)).mappings().one()
144148
except NoResultFound as e:
145149
raise EntityNotFoundException(entity=Book, entity_id=update_book.id) from e
146150
except IntegrityError as e:

library/adapters/database/storages/user.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from sqlalchemy.ext.asyncio import AsyncSession
88

99
from library.adapters.database.tables import UserTable
10+
from library.adapters.database.uow import SqlalchemyUow
1011
from library.application.exceptions import (
1112
EntityAlreadyExistsException,
1213
EntityNotFoundException,
@@ -19,18 +20,21 @@
1920
UserId,
2021
UserPaginationParams,
2122
)
22-
from library.domains.interfaces.storages.user import IUserStorage
2323

2424

25-
class UserStorage(IUserStorage):
26-
def __init__(self, session: AsyncSession) -> None:
27-
self.session = session
25+
class UserStorage:
26+
def __init__(self, *, uow: SqlalchemyUow) -> None:
27+
self._uow = uow
28+
29+
@property
30+
def _session(self) -> AsyncSession:
31+
return self._uow.session
2832

2933
async def fetch_user_by_id(self, *, user_id: UserId) -> User | None:
3034
stmt = select(UserTable).where(
3135
UserTable.id == user_id, UserTable.deleted_at.is_(None)
3236
)
33-
user = (await self.session.scalars(stmt)).first()
37+
user = (await self._session.scalars(stmt)).first()
3438
if user is None:
3539
return None
3640
return User(
@@ -45,15 +49,15 @@ async def exists_user_by_id(self, *, user_id: UserId) -> bool:
4549
stmt = select(
4650
exists().where(UserTable.id == user_id, UserTable.deleted_at.is_(None))
4751
)
48-
return bool((await self.session.execute(stmt)).scalar())
52+
return bool((await self._session.execute(stmt)).scalar())
4953

5054
async def count_users(self, *, params: UserPaginationParams) -> int:
5155
query = (
5256
select(func.count())
5357
.select_from(UserTable)
5458
.where(UserTable.deleted_at.is_(None))
5559
)
56-
result = (await self.session.execute(query)).scalar()
60+
result = (await self._session.execute(query)).scalar()
5761
return result or 0
5862

5963
async def fetch_user_list(self, *, params: UserPaginationParams) -> Sequence[User]:
@@ -69,7 +73,7 @@ async def fetch_user_list(self, *, params: UserPaginationParams) -> Sequence[Use
6973
.limit(params.limit)
7074
.offset(params.offset)
7175
)
72-
result = (await self.session.execute(query)).mappings().all()
76+
result = (await self._session.execute(query)).mappings().all()
7377
return [
7478
User(
7579
id=UserId(result["id"]),
@@ -97,7 +101,7 @@ async def create_user(self, *, user: CreateUser) -> User:
97101
)
98102
)
99103
try:
100-
result = (await self.session.execute(stmt)).mappings().one()
104+
result = (await self._session.execute(stmt)).mappings().one()
101105
except IntegrityError as e:
102106
self._raise_error(e)
103107
return User(
@@ -114,7 +118,7 @@ async def delete_user_by_id(self, *, user_id: UserId) -> None:
114118
.where(UserTable.id == user_id)
115119
.values(deleted_at=datetime.now(tz=UTC))
116120
)
117-
await self.session.execute(stmt)
121+
await self._session.execute(stmt)
118122

119123
async def update_user_by_id(self, *, update_user: UpdateUser) -> User:
120124
stmt = (
@@ -130,7 +134,7 @@ async def update_user_by_id(self, *, update_user: UpdateUser) -> User:
130134
)
131135
)
132136
try:
133-
result = (await self.session.execute(stmt)).mappings().one()
137+
result = (await self._session.execute(stmt)).mappings().one()
134138
except NoResultFound as e:
135139
raise EntityNotFoundException(entity=User, entity_id=update_user.id) from e
136140
return User(

library/adapters/database/uow.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,54 @@
11
import asyncio
2+
import logging
23
from typing import Any
34

4-
from sqlalchemy.ext.asyncio import AsyncSession, AsyncSessionTransaction
5+
from sqlalchemy.ext.asyncio import (
6+
AsyncSession,
7+
AsyncSessionTransaction,
8+
async_sessionmaker,
9+
)
510

611
from library.domains.uow import AbstractUow
712

13+
logger = logging.getLogger(__name__)
14+
815

916
class SqlalchemyUow(AbstractUow):
10-
transaction: AsyncSessionTransaction | None
17+
_session: AsyncSession | None
18+
_transaction: AsyncSessionTransaction | None
19+
_session_factory: async_sessionmaker[AsyncSession]
20+
21+
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
22+
self._session_factory = session_factory
23+
self._session = None
24+
self._transaction = None
1125

12-
def __init__(self, session: AsyncSession) -> None:
13-
self.session = session
14-
self.transaction = None
26+
@property
27+
def session(self) -> AsyncSession:
28+
if self._session is None:
29+
raise Exception("Session is not created")
30+
return self._session
1531

1632
async def commit(self) -> None:
1733
await self.session.commit()
1834

1935
async def rollback(self) -> None:
2036
await self.session.rollback()
21-
self.transaction = None
37+
self._transaction = None
2238

2339
async def create_transaction(self) -> None:
24-
self.transaction = await self.session.begin()
40+
if self._session is not None:
41+
logger.warning("Attempt to create already existing session")
42+
if self._transaction is not None:
43+
raise Exception("Session is already in transaction")
44+
else:
45+
self._transaction = await self.session.begin()
46+
else:
47+
self._session = self._session_factory()
48+
self._transaction = await self.session.begin()
2549

2650
async def close_transaction(self, *exc: Any) -> None:
2751
task = asyncio.create_task(self.session.close())
2852
await asyncio.shield(task)
53+
self._transaction = None
54+
self._session = None

0 commit comments

Comments
 (0)