Skip to content

Commit c754800

Browse files
committed
1.1.0
1 parent 286f689 commit c754800

File tree

12 files changed

+123
-44
lines changed

12 files changed

+123
-44
lines changed

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ test:
1010
uv:
1111
uv sync
1212
source .venv/bin/activate
13+
14+
build:
15+
uv build
16+
17+
publish:
18+
uv publish

context_async_sqlalchemy/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
)
1010
from .connect import (
1111
DBConnect,
12-
db_connect,
12+
master_connect,
13+
replica_connect,
1314
)
1415
from .session import (
1516
db_session,
@@ -19,6 +20,8 @@
1920
commit_db_session,
2021
rollback_db_session,
2122
close_db_session,
23+
new_non_ctx_atomic_session,
24+
new_non_ctx_session,
2225
)
2326
from .auto_commit import auto_commit_by_status_code
2427
from .fastapi_utils.middleware import fastapi_db_session_middleware
@@ -32,7 +35,8 @@
3235
"pop_db_session_from_context",
3336
"run_in_new_ctx",
3437
"DBConnect",
35-
"db_connect",
38+
"master_connect",
39+
"replica_connect",
3640
"db_session",
3741
"atomic_db_session",
3842
"run_with_new_db_session",
@@ -42,4 +46,6 @@
4246
"close_db_session",
4347
"auto_commit_by_status_code",
4448
"fastapi_db_session_middleware",
49+
"new_non_ctx_atomic_session",
50+
"new_non_ctx_session",
4551
]
Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,74 @@
11
import asyncio
2+
from typing import Any, Callable, Coroutine
3+
24
from sqlalchemy.ext.asyncio import (
35
async_sessionmaker,
46
AsyncEngine,
57
AsyncSession,
68
)
79

10+
EngineCreatorFunc = Callable[[str], AsyncEngine]
11+
SessionMakerCreatorFunc = Callable[
12+
[AsyncEngine], async_sessionmaker[AsyncSession]
13+
]
14+
AsyncFunc = Callable[["DBConnect"], Coroutine[Any, Any, None]]
15+
816

917
class DBConnect:
1018
"""stores the database connection parameters"""
1119

1220
def __init__(self) -> None:
13-
self.engine: AsyncEngine | None = None
14-
self.session_maker: async_sessionmaker[AsyncSession] | None = None
21+
self.host: str | None = None
22+
self._engine: AsyncEngine | None = None
23+
self._session_maker: async_sessionmaker[AsyncSession] | None = None
24+
25+
self.engine_creator: EngineCreatorFunc | None = None
26+
self.session_maker_creator: SessionMakerCreatorFunc | None = None
27+
self.before_create_session_handler: AsyncFunc | None = None
1528
self._lock = asyncio.Lock()
1629

17-
async def close(self) -> None:
18-
if self.engine:
19-
await self.engine.dispose()
20-
self.engine = None
30+
async def connect(self, host: str) -> None:
31+
"""initiates engine and session maker"""
32+
assert host
33+
async with self._lock:
34+
await self._connect(host)
2135

36+
async def change_host(self, host: str) -> None:
37+
"""Renews the connection if a host needs to be changed"""
38+
assert host
39+
async with self._lock:
40+
if host != self.host:
41+
await self._connect(host)
2242

23-
db_connect = DBConnect()
43+
async def create_session(self) -> AsyncSession:
44+
"""Creates a new session"""
45+
if self.before_create_session_handler:
46+
await self.before_create_session_handler(self)
47+
maker = await self.get_session_maker()
48+
return maker()
2449

50+
async def get_session_maker(self) -> async_sessionmaker[AsyncSession]:
51+
"""Gets the session maker"""
52+
if not self._session_maker:
53+
assert self.host
54+
await self.connect(self.host)
55+
56+
assert self._session_maker
57+
return self._session_maker
58+
59+
async def close(self) -> None:
60+
if self._engine:
61+
await self._engine.dispose()
62+
self._engine = None
2563

26-
def get_session_maker() -> async_sessionmaker[AsyncSession]:
27-
"""Gets the session maker"""
28-
assert db_connect.session_maker
29-
return db_connect.session_maker
64+
async def _connect(self, host: str) -> None:
65+
self.host = host
66+
await self.close()
67+
assert self.engine_creator
68+
self._engine = self.engine_creator(host)
69+
assert self.session_maker_creator
70+
self._session_maker = self.session_maker_creator(self._engine)
3071

3172

32-
def create_session() -> AsyncSession:
33-
"""Creates a new session"""
34-
assert db_connect.session_maker
35-
return db_connect.session_maker()
73+
master_connect = DBConnect()
74+
replica_connect = DBConnect()

context_async_sqlalchemy/context.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from sqlalchemy.ext.asyncio import AsyncSession
55

6-
from .connect import create_session
6+
from .connect import master_connect
77

88

99
def init_db_session_ctx() -> Token[dict[str, AsyncSession] | None]:
@@ -104,6 +104,7 @@ async def _new_ctx_wrapper(
104104
**kwargs: Any,
105105
) -> AsyncCallableResult:
106106
_init_db_session_ctx()
107-
async with create_session() as session:
107+
session_maker = await master_connect.get_session_maker()
108+
async with session_maker() as session:
108109
put_db_session_to_context(session)
109110
return await callable_func(*args, **kwargs)

context_async_sqlalchemy/session.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from sqlalchemy.ext.asyncio import AsyncSession
55

6-
from .connect import create_session
6+
from .connect import master_connect
77
from .context import (
88
AsyncCallable,
99
AsyncCallableResult,
@@ -24,7 +24,7 @@ async def db_session() -> AsyncSession:
2424
"""
2525
session = get_db_session_from_context()
2626
if not session:
27-
session = create_session()
27+
session = await master_connect.create_session()
2828
put_db_session_to_context(session)
2929
return session
3030

@@ -128,6 +128,22 @@ async def close_db_session() -> None:
128128
await session.close()
129129

130130

131+
@asynccontextmanager
132+
async def new_non_ctx_session() -> AsyncGenerator[AsyncSession, None]:
133+
"""Creating a new session without using a context"""
134+
session_maker = await master_connect.get_session_maker()
135+
async with session_maker() as session:
136+
yield session
137+
138+
139+
@asynccontextmanager
140+
async def new_non_ctx_atomic_session() -> AsyncGenerator[AsyncSession, None]:
141+
"""Creating a new session with transaction without using a context"""
142+
async with new_non_ctx_session() as session:
143+
async with session.begin():
144+
yield session
145+
146+
131147
async def _atomic_wrapper(
132148
callable_func: AsyncCallable[AsyncCallableResult],
133149
*args: Any,

exmaples/fastapi_example/database.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
)
1111

1212

13-
def create_engine() -> AsyncEngine:
13+
def create_engine(host: str) -> AsyncEngine:
1414
"""
1515
database connection parameters.
1616
In production code, you will probably take these parameters from
@@ -19,7 +19,6 @@ def create_engine() -> AsyncEngine:
1919
pg_user = "krylosov-aa"
2020
pg_password = ""
2121
pg_port = 6432
22-
host = "localhost"
2322
pg_db = "test"
2423
return create_async_engine(
2524
f"postgresql+asyncpg://"

exmaples/fastapi_example/setup_app.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
from fastapi import FastAPI
77
from starlette.middleware.base import BaseHTTPMiddleware
88

9-
from context_async_sqlalchemy import db_connect, fastapi_db_session_middleware
9+
from context_async_sqlalchemy import (
10+
master_connect,
11+
replica_connect,
12+
fastapi_db_session_middleware,
13+
)
1014

1115
from .database import create_engine, create_session_maker
1216
from .routes.atomic_usage import handler_with_db_session_and_atomic
@@ -32,19 +36,17 @@ def setup_app() -> FastAPI:
3236
)
3337
setup_middlewares(app)
3438
setup_routes(app)
35-
setup_database()
3639
return app
3740

3841

3942
@asynccontextmanager
4043
async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
41-
"""
42-
It is important to clean up resources at the end of an application's
43-
life.
44-
"""
44+
"""Database connection lifecycle management"""
45+
await setup_database()
4546
yield
4647
await asyncio.gather(
47-
db_connect.close(), # Close the engine if it was open
48+
master_connect.close(), # Close the engine if it was open
49+
replica_connect.close(), # Close the engine if it was open
4850
)
4951

5052

@@ -58,13 +60,14 @@ def setup_middlewares(app: FastAPI) -> None:
5860
)
5961

6062

61-
def setup_database() -> None:
63+
async def setup_database() -> None:
6264
"""
6365
Here you pass the database connection parameters to the library.
6466
More specifically, the engine and session maker.
6567
"""
66-
db_connect.engine = create_engine()
67-
db_connect.session_maker = create_session_maker(db_connect.engine)
68+
master_connect.engine_creator = create_engine
69+
master_connect.session_maker_creator = create_session_maker
70+
await master_connect.connect("127.0.0.1")
6871

6972

7073
def setup_routes(app: FastAPI) -> None:

exmaples/fastapi_example/tests/conftest.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,30 @@
22
Basic settings and fixtures for testing
33
"""
44

5-
from typing import AsyncGenerator, Generator
5+
from typing import AsyncGenerator
66

77
import pytest_asyncio
8-
import pytest
98
from fastapi import FastAPI
109
from httpx import AsyncClient, ASGITransport
1110
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
1211

12+
from context_async_sqlalchemy import master_connect, replica_connect
1313
from exmaples.fastapi_example.database import (
1414
create_engine,
1515
create_session_maker,
1616
)
17-
from exmaples.fastapi_example.setup_app import setup_app
17+
from exmaples.fastapi_example.setup_app import lifespan, setup_app
1818

1919

20-
@pytest.fixture
21-
def app() -> Generator[FastAPI]:
20+
@pytest_asyncio.fixture
21+
async def app() -> AsyncGenerator[FastAPI]:
2222
"""
2323
A new application for each test allows for complete isolation between
2424
tests.
2525
"""
26-
yield setup_app()
26+
app = setup_app()
27+
async with lifespan(app):
28+
yield app
2729

2830

2931
@pytest_asyncio.fixture
@@ -51,7 +53,14 @@ async def db_session_test(
5153
async def session_maker_test() -> AsyncGenerator[
5254
async_sessionmaker[AsyncSession]
5355
]:
54-
engine = create_engine()
56+
engine = create_engine("127.0.0.1")
5557
session_maker = create_session_maker(engine)
5658
yield session_maker
5759
await engine.dispose()
60+
61+
62+
@pytest_asyncio.fixture(autouse=True)
63+
async def close_connect() -> AsyncGenerator[None]:
64+
yield
65+
await master_connect.close()
66+
await replica_connect.close()

exmaples/fastapi_example/tests/non_transactional/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ async def cleanup_tables_before() -> None:
5252

5353

5454
async def _cleanup_tables() -> None:
55-
engine = create_engine()
55+
engine = create_engine("127.0.0.1")
5656
session_maker = create_session_maker(engine)
5757
async with session_maker() as session:
5858
await session.execute(

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "context-async-sqlalchemy"
3-
version = "1.0.1"
3+
version = "1.1.0"
44
description = "A convenient way to configure and interact with a async sqlalchemy session through context in asynchronous applications"
55
readme = "README.md"
66
authors = [

0 commit comments

Comments
 (0)