-
-
Notifications
You must be signed in to change notification settings - Fork 74
Expand file tree
/
Copy pathasyncio.py
More file actions
198 lines (158 loc) · 8.12 KB
/
Copy pathasyncio.py
File metadata and controls
198 lines (158 loc) · 8.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Callable, Optional, Union, cast
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import sessionmaker as sync_sessionmaker
from advanced_alchemy._listeners import set_async_context
from advanced_alchemy.config.common import (
GenericAlembicConfig,
GenericSessionConfig,
GenericSQLAlchemyConfig,
)
from advanced_alchemy.exceptions import ImproperConfigurationError
from advanced_alchemy.utils.dataclass import Empty
if TYPE_CHECKING:
from typing import Callable
from sqlalchemy.orm import Session
from advanced_alchemy.config.routing import RoutingConfig
from advanced_alchemy.utils.dataclass import EmptyType
__all__ = (
"AlembicAsyncConfig",
"AsyncSessionConfig",
"SQLAlchemyAsyncConfig",
)
@dataclass
class AsyncSessionConfig(GenericSessionConfig[AsyncConnection, AsyncEngine, AsyncSession]):
"""SQLAlchemy async session config."""
sync_session_class: "Optional[Union[type[Session], EmptyType]]" = Empty
"""A :class:`Session <sqlalchemy.orm.Session>` subclass or other callable which will be used to construct the
:class:`Session <sqlalchemy.orm.Session>` which will be proxied. This parameter may be used to provide custom
:class:`Session <sqlalchemy.orm.Session>` subclasses. Defaults to the
:attr:`AsyncSession.sync_session_class <sqlalchemy.ext.asyncio.AsyncSession.sync_session_class>` class-level
attribute."""
@dataclass
class AlembicAsyncConfig(GenericAlembicConfig):
"""Configuration for an Async Alembic's Config class.
.. seealso::
https://alembic.sqlalchemy.org/en/latest/api/config.html
"""
@dataclass
class SQLAlchemyAsyncConfig(GenericSQLAlchemyConfig[AsyncEngine, AsyncSession, async_sessionmaker[AsyncSession]]):
"""Async SQLAlchemy Configuration.
Note:
The alembic configuration options are documented in the Alembic documentation.
Example:
Basic async configuration::
config = SQLAlchemyAsyncConfig(
connection_string="postgresql+asyncpg://user:pass@localhost/db",
)
Configuration with read/write routing::
from advanced_alchemy.config.routing import RoutingConfig
config = SQLAlchemyAsyncConfig(
routing_config=RoutingConfig(
primary_connection_string="postgresql+asyncpg://user:pass@primary/db",
read_replicas=[
"postgresql+asyncpg://user:pass@replica/db"
],
),
)
"""
create_engine_callable: "Callable[[str], AsyncEngine]" = create_async_engine
"""Callable that creates an :class:`AsyncEngine <sqlalchemy.ext.asyncio.AsyncEngine>` instance or instance of its
subclass.
"""
session_config: AsyncSessionConfig = field(default_factory=AsyncSessionConfig) # pyright: ignore[reportIncompatibleVariableOverride]
"""Configuration options for the :class:`async_sessionmaker<sqlalchemy.ext.asyncio.async_sessionmaker>`."""
session_maker_class: "type[async_sessionmaker[AsyncSession]]" = async_sessionmaker # pyright: ignore[reportIncompatibleVariableOverride]
"""Sessionmaker class to use."""
alembic_config: "AlembicAsyncConfig" = field(default_factory=AlembicAsyncConfig)
"""Configuration for the SQLAlchemy Alembic migrations.
The configuration options are documented in the Alembic documentation.
"""
routing_config: "Optional[RoutingConfig]" = None
"""Optional read/write routing configuration.
When provided, enables automatic routing of read operations to replicas
and write operations to the primary database.
.. note::
When using ``routing_config``, do not set ``connection_string``.
The primary connection is specified in the routing config.
"""
def __post_init__(self) -> None:
# Validate routing config vs connection_string
if self.routing_config is not None and self.connection_string is not None:
msg = "Provide either 'connection_string' or 'routing_config', not both"
raise ImproperConfigurationError(msg)
# If routing_config is set, use its primary as the connection_string for compatibility
if self.routing_config is not None:
self.connection_string = self.routing_config.primary_connection_string
if self.connection_string is None:
# Try to get from default group engines
configs = self.routing_config.get_engine_configs(self.routing_config.default_group)
if configs:
self.connection_string = configs[0].connection_string
super().__post_init__()
def __hash__(self) -> int:
return super().__hash__()
def __eq__(self, other: object) -> bool:
return super().__eq__(other)
def create_session_maker(self) -> "Callable[[], AsyncSession]":
"""Get a session maker.
If routing is configured, returns a routing-aware session maker.
Otherwise, returns a standard session maker.
Returns:
A callable that creates session instances.
"""
if self.session_maker:
return self.session_maker
from sqlalchemy import event
from advanced_alchemy._listeners import (
AsyncCacheListener,
AsyncFileObjectListener,
touch_updated_timestamp,
)
# Use routing session maker if routing is configured
if self.routing_config is not None:
from advanced_alchemy.routing import RoutingAsyncSessionMaker
routing_maker: Callable[[], AsyncSession] = RoutingAsyncSessionMaker(
routing_config=self.routing_config,
engine_config=self.engine_config_dict,
session_config=self.session_config_dict,
)
self.session_maker = routing_maker
else:
self.session_maker = cast("Callable[[], AsyncSession]", super().create_session_maker()) # type: ignore[redundant-cast]
if isinstance(self.session_maker, async_sessionmaker):
session_maker = cast(
"async_sessionmaker[AsyncSession]",
self.session_maker, # pyright: ignore[reportUnknownMemberType]
)
# async_sessionmaker does not support Session-level events directly.
# Create a sync sessionmaker, register events on it, and inject it
# as sync_session_class so events fire on the underlying sync Session.
sync_maker = sync_sessionmaker()
if self.enable_file_object_listener:
event.listen(sync_maker, "before_flush", AsyncFileObjectListener.before_flush)
event.listen(sync_maker, "after_commit", AsyncFileObjectListener.after_commit)
event.listen(sync_maker, "after_rollback", AsyncFileObjectListener.after_rollback)
if self.enable_touch_updated_timestamp_listener:
event.listen(sync_maker, "before_flush", touch_updated_timestamp)
event.listen(sync_maker, "after_commit", AsyncCacheListener.after_commit)
event.listen(sync_maker, "after_rollback", AsyncCacheListener.after_rollback)
session_maker.configure(sync_session_class=sync_maker)
if self.session_maker is None: # pyright: ignore
msg = "Session maker was not initialized." # type: ignore[unreachable]
raise ImproperConfigurationError(msg)
return cast("async_sessionmaker[AsyncSession]", self.session_maker) # pyright: ignore[reportUnknownMemberType]
@asynccontextmanager
async def get_session(
self,
) -> AsyncGenerator[AsyncSession, None]:
"""Get a session from the session maker.
Yields:
AsyncGenerator[AsyncSession, None]: An async context manager that yields an AsyncSession.
"""
session_maker = self.create_session_maker()
set_async_context(True)
async with session_maker() as session:
yield session