sqlalchemy-foundation-kit 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlalchemy_foundation_kit/__init__.py +119 -0
- sqlalchemy_foundation_kit/__version__.py +1 -0
- sqlalchemy_foundation_kit/_typing.py +28 -0
- sqlalchemy_foundation_kit/base/__init__.py +46 -0
- sqlalchemy_foundation_kit/base/_optional.py +37 -0
- sqlalchemy_foundation_kit/base/engine.py +256 -0
- sqlalchemy_foundation_kit/base/metadata.py +57 -0
- sqlalchemy_foundation_kit/base/models.py +101 -0
- sqlalchemy_foundation_kit/base/serialization.py +98 -0
- sqlalchemy_foundation_kit/base/types.py +72 -0
- sqlalchemy_foundation_kit/config/__init__.py +17 -0
- sqlalchemy_foundation_kit/config/postgres.py +177 -0
- sqlalchemy_foundation_kit/contrib/__init__.py +5 -0
- sqlalchemy_foundation_kit/contrib/_metrics_utils.py +19 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/__init__.py +27 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/_base.py +27 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/_deps.py +37 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/database.py +196 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/metrics.py +85 -0
- sqlalchemy_foundation_kit/contrib/di/__init__.py +21 -0
- sqlalchemy_foundation_kit/contrib/di/_base.py +27 -0
- sqlalchemy_foundation_kit/contrib/di/_deps.py +32 -0
- sqlalchemy_foundation_kit/contrib/di/database.py +169 -0
- sqlalchemy_foundation_kit/contrib/di/metrics.py +71 -0
- sqlalchemy_foundation_kit/contrib/metrics/__init__.py +7 -0
- sqlalchemy_foundation_kit/contrib/metrics/postgres.py +149 -0
- sqlalchemy_foundation_kit/contrib/settings/__init__.py +19 -0
- sqlalchemy_foundation_kit/contrib/settings/postgres.py +183 -0
- sqlalchemy_foundation_kit/contrib/telemetry/__init__.py +17 -0
- sqlalchemy_foundation_kit/contrib/telemetry/instrumentations.py +101 -0
- sqlalchemy_foundation_kit/contrib/telemetry/uow.py +227 -0
- sqlalchemy_foundation_kit/protocols/__init__.py +21 -0
- sqlalchemy_foundation_kit/protocols/metrics.py +77 -0
- sqlalchemy_foundation_kit/py.typed +0 -0
- sqlalchemy_foundation_kit/session/__init__.py +27 -0
- sqlalchemy_foundation_kit/session/builder.py +293 -0
- sqlalchemy_foundation_kit/session/connection.py +33 -0
- sqlalchemy_foundation_kit/session/factories.py +104 -0
- sqlalchemy_foundation_kit/session/locks.py +68 -0
- sqlalchemy_foundation_kit/session/manager.py +252 -0
- sqlalchemy_foundation_kit/session/retry.py +82 -0
- sqlalchemy_foundation_kit/uow/__init__.py +21 -0
- sqlalchemy_foundation_kit/uow/enums.py +18 -0
- sqlalchemy_foundation_kit/uow/protocols.py +80 -0
- sqlalchemy_foundation_kit/uow/sqlalchemy.py +406 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/METADATA +624 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/RECORD +49 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/WHEEL +4 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Builder pattern for AsyncSessionManager configuration.
|
|
2
|
+
|
|
3
|
+
Provides a fluent interface for constructing AsyncSessionManager instances
|
|
4
|
+
with many optional parameters, following the Builder pattern to simplify
|
|
5
|
+
complex object creation and improve code readability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Generic
|
|
11
|
+
|
|
12
|
+
from .._typing import SessionT
|
|
13
|
+
from .manager import AsyncSessionManager
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
|
|
18
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
19
|
+
|
|
20
|
+
from ..config import PoolSettingsProtocol
|
|
21
|
+
from ..protocols import PostgresMetricsProtocol
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AsyncSessionManagerBuilder(Generic[SessionT]):
|
|
25
|
+
"""Builder for AsyncSessionManager construction.
|
|
26
|
+
|
|
27
|
+
Provides a fluent API for configuring AsyncSessionManager with many optional
|
|
28
|
+
parameters. This pattern improves code readability and follows KISS principle
|
|
29
|
+
by avoiding constructors with 10+ parameters.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
Basic usage:
|
|
33
|
+
>>> manager = (
|
|
34
|
+
... AsyncSessionManagerBuilder("postgresql+asyncpg://...")
|
|
35
|
+
... .with_pool("queue")
|
|
36
|
+
... .with_echo(True)
|
|
37
|
+
... .build()
|
|
38
|
+
... )
|
|
39
|
+
|
|
40
|
+
Advanced configuration:
|
|
41
|
+
>>> manager = (
|
|
42
|
+
... AsyncSessionManagerBuilder[CustomSession]("postgresql+asyncpg://...")
|
|
43
|
+
... .with_session_class(CustomSession)
|
|
44
|
+
... .with_pool("queue", pool_settings=settings.pool)
|
|
45
|
+
... .with_metrics(metrics)
|
|
46
|
+
... .with_callbacks(on_engine_created=instrument_engine)
|
|
47
|
+
... .with_json_serialization(orjson=True)
|
|
48
|
+
... .with_isolation_level("READ COMMITTED")
|
|
49
|
+
... .build()
|
|
50
|
+
... )
|
|
51
|
+
|
|
52
|
+
Reusable configuration:
|
|
53
|
+
>>> builder = (
|
|
54
|
+
... AsyncSessionManagerBuilder("postgresql+asyncpg://...")
|
|
55
|
+
... .with_pool("queue")
|
|
56
|
+
... .with_metrics(metrics)
|
|
57
|
+
... )
|
|
58
|
+
>>> manager1 = builder.with_echo(True).build()
|
|
59
|
+
>>> manager2 = builder.with_echo(False).build()
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, url: str) -> None:
|
|
63
|
+
"""Initialize builder with database URL (required).
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
url: Database connection URL (required parameter).
|
|
67
|
+
"""
|
|
68
|
+
self._url = url
|
|
69
|
+
self._echo: bool = False
|
|
70
|
+
self._poolclass: str | type = "null"
|
|
71
|
+
self._session_class: type[SessionT] | None = None
|
|
72
|
+
self._expire_on_commit: bool = False
|
|
73
|
+
self._connect_args: dict[str, object] | None = None
|
|
74
|
+
self._isolation_level: str | None = None
|
|
75
|
+
self._pool_settings: PoolSettingsProtocol | None = None
|
|
76
|
+
self._use_orjson: bool = False
|
|
77
|
+
self._metrics: PostgresMetricsProtocol | None = None
|
|
78
|
+
self._on_engine_created: Callable[[AsyncEngine], None] | None = None
|
|
79
|
+
self._dispose_timeout: float | None = None
|
|
80
|
+
self._extra_kwargs: dict[str, object] = {}
|
|
81
|
+
|
|
82
|
+
def with_echo(self, echo: bool = True) -> AsyncSessionManagerBuilder[SessionT]:
|
|
83
|
+
"""Enable SQL statement logging.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
echo: If True, SQLAlchemy logs all SQL statements.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Self for method chaining.
|
|
90
|
+
"""
|
|
91
|
+
self._echo = echo
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def with_pool(
|
|
95
|
+
self,
|
|
96
|
+
poolclass: str | type,
|
|
97
|
+
pool_settings: PoolSettingsProtocol | None = None,
|
|
98
|
+
) -> AsyncSessionManagerBuilder[SessionT]:
|
|
99
|
+
"""Configure connection pool.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
poolclass: Pool class name or type (e.g., "queue", "null").
|
|
103
|
+
pool_settings: Optional pool configuration settings.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Self for method chaining.
|
|
107
|
+
"""
|
|
108
|
+
self._poolclass = poolclass
|
|
109
|
+
self._pool_settings = pool_settings
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
def with_session_class(
|
|
113
|
+
self,
|
|
114
|
+
session_class: type[SessionT],
|
|
115
|
+
) -> AsyncSessionManagerBuilder[SessionT]:
|
|
116
|
+
"""Use custom session class.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
session_class: Custom AsyncSession subclass.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Self for method chaining.
|
|
123
|
+
"""
|
|
124
|
+
self._session_class = session_class
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def with_expire_on_commit(
|
|
128
|
+
self,
|
|
129
|
+
expire: bool = True,
|
|
130
|
+
) -> AsyncSessionManagerBuilder[SessionT]:
|
|
131
|
+
"""Configure object expiration behavior after commit.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
expire: If True, all objects expire after commit.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Self for method chaining.
|
|
138
|
+
"""
|
|
139
|
+
self._expire_on_commit = expire
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def with_connect_args(
|
|
143
|
+
self,
|
|
144
|
+
**connect_args: object,
|
|
145
|
+
) -> AsyncSessionManagerBuilder[SessionT]:
|
|
146
|
+
"""Add database driver connection arguments.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
**connect_args: Arguments passed to the database driver.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Self for method chaining.
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
>>> builder.with_connect_args(
|
|
156
|
+
... server_settings={"application_name": "myapp"},
|
|
157
|
+
... command_timeout=60,
|
|
158
|
+
... )
|
|
159
|
+
"""
|
|
160
|
+
if self._connect_args is None:
|
|
161
|
+
self._connect_args = {}
|
|
162
|
+
self._connect_args.update(connect_args)
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def with_isolation_level(
|
|
166
|
+
self,
|
|
167
|
+
isolation_level: str,
|
|
168
|
+
) -> AsyncSessionManagerBuilder[SessionT]:
|
|
169
|
+
"""Set default transaction isolation level.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
isolation_level: Default isolation level (e.g., "READ COMMITTED").
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Self for method chaining.
|
|
176
|
+
"""
|
|
177
|
+
self._isolation_level = isolation_level
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def with_metrics(
|
|
181
|
+
self,
|
|
182
|
+
metrics: PostgresMetricsProtocol,
|
|
183
|
+
) -> AsyncSessionManagerBuilder[SessionT]:
|
|
184
|
+
"""Enable connection pool metrics collection.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
metrics: Metrics collector implementing PostgresMetricsProtocol.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Self for method chaining.
|
|
191
|
+
"""
|
|
192
|
+
self._metrics = metrics
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def with_callbacks(
|
|
196
|
+
self,
|
|
197
|
+
on_engine_created: Callable[[AsyncEngine], None],
|
|
198
|
+
) -> AsyncSessionManagerBuilder[SessionT]:
|
|
199
|
+
"""Register engine creation callback.
|
|
200
|
+
|
|
201
|
+
Useful for attaching instrumentation (OpenTelemetry), custom event
|
|
202
|
+
listeners, or debug hooks right after engine creation.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
on_engine_created: Callback invoked with AsyncEngine after creation.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Self for method chaining.
|
|
209
|
+
|
|
210
|
+
Examples:
|
|
211
|
+
>>> from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
|
|
212
|
+
>>> def instrument(engine):
|
|
213
|
+
... SQLAlchemyInstrumentor().instrument(engine=engine.sync_engine)
|
|
214
|
+
>>> builder.with_callbacks(on_engine_created=instrument)
|
|
215
|
+
"""
|
|
216
|
+
self._on_engine_created = on_engine_created
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def with_json_serialization(
|
|
220
|
+
self,
|
|
221
|
+
orjson: bool = True,
|
|
222
|
+
) -> AsyncSessionManagerBuilder[SessionT]:
|
|
223
|
+
"""Configure JSON serialization backend.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
orjson: If True, use orjson for faster JSON serialization.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Self for method chaining.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ImportError: If orjson=True but orjson is not installed.
|
|
233
|
+
"""
|
|
234
|
+
self._use_orjson = orjson
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
def with_extra_kwargs(self, **kwargs: object) -> AsyncSessionManagerBuilder[SessionT]:
|
|
238
|
+
"""Add additional keyword arguments for create_async_engine.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
**kwargs: Additional engine configuration.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Self for method chaining.
|
|
245
|
+
"""
|
|
246
|
+
self._extra_kwargs.update(kwargs)
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
def with_dispose_timeout(self, timeout: float) -> AsyncSessionManagerBuilder[SessionT]:
|
|
250
|
+
"""Configure how long :meth:`AsyncSessionManager.aclose` waits for engine disposal.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
timeout: Maximum seconds to wait for the engine to dispose.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Self for method chaining.
|
|
257
|
+
"""
|
|
258
|
+
self._dispose_timeout = timeout
|
|
259
|
+
return self
|
|
260
|
+
|
|
261
|
+
def build(self) -> AsyncSessionManager[SessionT]:
|
|
262
|
+
"""Build AsyncSessionManager instance with configured parameters.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Configured AsyncSessionManager instance.
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
ImportError: If use_orjson=True but orjson is not installed.
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
>>> manager = builder.build()
|
|
272
|
+
>>> async with manager.get_session() as session:
|
|
273
|
+
... await session.execute(...)
|
|
274
|
+
"""
|
|
275
|
+
kwargs: dict[str, object] = {
|
|
276
|
+
"url": self._url,
|
|
277
|
+
"echo": self._echo,
|
|
278
|
+
"poolclass": self._poolclass,
|
|
279
|
+
"session_class": self._session_class,
|
|
280
|
+
"expire_on_commit": self._expire_on_commit,
|
|
281
|
+
"connect_args": self._connect_args,
|
|
282
|
+
"isolation_level": self._isolation_level,
|
|
283
|
+
"pool_settings": self._pool_settings,
|
|
284
|
+
"use_orjson": self._use_orjson,
|
|
285
|
+
"metrics": self._metrics,
|
|
286
|
+
"on_engine_created": self._on_engine_created,
|
|
287
|
+
}
|
|
288
|
+
if self._dispose_timeout is not None:
|
|
289
|
+
kwargs["dispose_timeout"] = self._dispose_timeout
|
|
290
|
+
return AsyncSessionManager(**kwargs, **self._extra_kwargs) # type: ignore[arg-type]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
__all__ = ["AsyncSessionManagerBuilder"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Custom connection class for pgbouncer compatibility (async)."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import asyncpg
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AsyncCConnection(asyncpg.Connection):
|
|
9
|
+
"""Custom async connection class for pgbouncer magic.
|
|
10
|
+
|
|
11
|
+
This subclass overrides only the private method _get_unique_id so that
|
|
12
|
+
prepared statement identifiers are unique per connection. That is required
|
|
13
|
+
when using pgbouncer in transaction mode, where the same server connection
|
|
14
|
+
may be reused for different logical connections.
|
|
15
|
+
|
|
16
|
+
See: https://github.com/sqlalchemy/sqlalchemy/issues/6467
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def _get_unique_id(self, prefix: str) -> str:
|
|
20
|
+
"""Generate unique ID for prepared statements.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
prefix: Prefix for the unique ID.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A unique string ID including the prefix and a UUID.
|
|
27
|
+
"""
|
|
28
|
+
return f"__asyncpg_{prefix}_{uuid.uuid4()}__"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AsyncCConnection",
|
|
33
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Async database session manager factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
|
|
9
|
+
from ..config import PostgresSettingsProtocol
|
|
10
|
+
from .connection import AsyncCConnection
|
|
11
|
+
from .manager import AsyncSessionManager
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
|
|
16
|
+
import asyncpg
|
|
17
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
18
|
+
|
|
19
|
+
from ..protocols import PostgresMetricsProtocol
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_async_session_manager(
|
|
23
|
+
postgres_config: PostgresSettingsProtocol,
|
|
24
|
+
application_name: str | None = None,
|
|
25
|
+
metrics: PostgresMetricsProtocol | None = None,
|
|
26
|
+
on_engine_created: Callable[[AsyncEngine], None] | None = None,
|
|
27
|
+
connection_class: type[asyncpg.Connection] | None = None,
|
|
28
|
+
extra_server_settings: dict[str, str] | None = None,
|
|
29
|
+
extra_connect_args: dict[str, object] | None = None,
|
|
30
|
+
**kwargs: Any,
|
|
31
|
+
) -> AsyncSessionManager[AsyncSession]:
|
|
32
|
+
"""Create async session manager with PostgreSQL-specific configuration.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
postgres_config: PostgreSQL configuration implementing PostgresSettingsProtocol.
|
|
36
|
+
application_name: Optional custom application name. If None, uses postgres_config.application_name.
|
|
37
|
+
metrics: Optional metrics collector for connection pool monitoring.
|
|
38
|
+
on_engine_created: Optional callback invoked with the AsyncEngine right after creation.
|
|
39
|
+
Use it to attach OpenTelemetry instrumentation, custom listeners, etc.
|
|
40
|
+
connection_class: Custom asyncpg Connection subclass. Defaults to ``AsyncCConnection``
|
|
41
|
+
which provides pgbouncer transaction-mode compatibility.
|
|
42
|
+
extra_server_settings: Additional PostgreSQL ``server_settings`` to merge with defaults
|
|
43
|
+
(e.g., ``{"statement_timeout": "30000", "timezone": "UTC"}``). User-provided keys
|
|
44
|
+
override library defaults.
|
|
45
|
+
extra_connect_args: Additional asyncpg ``connect_args`` to merge with defaults
|
|
46
|
+
(e.g., ``{"command_timeout": 60}``). User-provided keys override library defaults.
|
|
47
|
+
**kwargs: Additional keyword arguments passed to AsyncSessionManager.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Configured AsyncSessionManager instance.
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
Basic usage:
|
|
54
|
+
>>> manager = create_async_session_manager(postgres_config)
|
|
55
|
+
|
|
56
|
+
With metrics:
|
|
57
|
+
>>> from sqlalchemy_foundation_kit.contrib.metrics import PostgresMetrics
|
|
58
|
+
>>> manager = create_async_session_manager(postgres_config, metrics=PostgresMetrics())
|
|
59
|
+
|
|
60
|
+
With custom server settings and command timeout:
|
|
61
|
+
>>> manager = create_async_session_manager(
|
|
62
|
+
... postgres_config,
|
|
63
|
+
... extra_server_settings={"statement_timeout": "30000", "timezone": "UTC"},
|
|
64
|
+
... extra_connect_args={"command_timeout": 60},
|
|
65
|
+
... )
|
|
66
|
+
|
|
67
|
+
With OpenTelemetry tracing bound to this engine:
|
|
68
|
+
>>> from sqlalchemy_foundation_kit.contrib.telemetry import instrument_engine
|
|
69
|
+
>>> manager = create_async_session_manager(
|
|
70
|
+
... postgres_config,
|
|
71
|
+
... on_engine_created=instrument_engine,
|
|
72
|
+
... )
|
|
73
|
+
"""
|
|
74
|
+
app_name = application_name or postgres_config.application_name
|
|
75
|
+
|
|
76
|
+
# Build server settings with optional overrides
|
|
77
|
+
server_settings: dict[str, str] = {
|
|
78
|
+
"application_name": app_name,
|
|
79
|
+
**({"jit": postgres_config.jit} if postgres_config.jit is not None else {}),
|
|
80
|
+
**({"search_path": postgres_config.db_schema} if postgres_config.db_schema is not None else {}),
|
|
81
|
+
**(extra_server_settings or {}),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Build connect args with optional overrides
|
|
85
|
+
connect_args: dict[str, object] = {
|
|
86
|
+
"server_settings": server_settings,
|
|
87
|
+
"statement_cache_size": postgres_config.query.statement_cache_size,
|
|
88
|
+
"prepared_statement_cache_size": postgres_config.query.prepared_statement_cache_size,
|
|
89
|
+
"connection_class": connection_class or AsyncCConnection,
|
|
90
|
+
**(extra_connect_args or {}),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return AsyncSessionManager(
|
|
94
|
+
url=postgres_config.to_dsn(),
|
|
95
|
+
echo=postgres_config.query.echo,
|
|
96
|
+
poolclass=postgres_config.pool.kind,
|
|
97
|
+
connect_args=connect_args,
|
|
98
|
+
isolation_level=postgres_config.query.isolation_level,
|
|
99
|
+
pool_settings=postgres_config.pool,
|
|
100
|
+
use_orjson=postgres_config.use_orjson_serialization,
|
|
101
|
+
metrics=metrics,
|
|
102
|
+
on_engine_created=on_engine_created,
|
|
103
|
+
**kwargs,
|
|
104
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""PostgreSQL advisory locks (async)."""
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import text
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
5
|
+
|
|
6
|
+
# PostgreSQL bigint (signed 64-bit) range constants
|
|
7
|
+
_INT64_OFFSET: int = 1 << 63 # 2^63 = 9223372036854775808
|
|
8
|
+
_INT64_MASK: int = (1 << 64) - 1 # 2^64 - 1 = 18446744073709551615
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def try_advisory_xact_lock(session: AsyncSession, key: str | int) -> bool:
|
|
12
|
+
"""Acquire a Postgres transaction-scoped advisory lock.
|
|
13
|
+
|
|
14
|
+
Uses ``pg_try_advisory_xact_lock``: non-blocking, released automatically
|
|
15
|
+
at transaction end. String keys are hashed to integers. The key is then
|
|
16
|
+
truncated to signed 64-bit as Postgres expects.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
session: SQLAlchemy AsyncSession within an active transaction.
|
|
20
|
+
key: Lock identifier (string or integer). Strings are hashed to integers.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if lock was acquired, False if already held by another session.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
>>> async with session_maker() as session:
|
|
27
|
+
... async with session.begin():
|
|
28
|
+
... if await try_advisory_xact_lock(session, "my_operation"):
|
|
29
|
+
... # Perform protected operation
|
|
30
|
+
... await session.execute(...)
|
|
31
|
+
... await session.commit()
|
|
32
|
+
"""
|
|
33
|
+
# Convert string keys to integers via hashing
|
|
34
|
+
int_key = hash(key) if isinstance(key, str) else key
|
|
35
|
+
|
|
36
|
+
result = await session.execute(
|
|
37
|
+
text("SELECT pg_try_advisory_xact_lock(:k)"),
|
|
38
|
+
{"k": _to_signed64(int_key)},
|
|
39
|
+
)
|
|
40
|
+
return bool(result.scalar())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _to_signed64(key: int) -> int:
|
|
44
|
+
"""Wrap integer to PostgreSQL signed 64-bit bigint range.
|
|
45
|
+
|
|
46
|
+
PostgreSQL advisory locks use bigint (signed 64-bit integers).
|
|
47
|
+
This function wraps arbitrary Python ints into the range [-2^63, 2^63-1].
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
key: Integer of any size.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Equivalent value in range [-9223372036854775808, 9223372036854775807].
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
>>> _to_signed64(12345)
|
|
57
|
+
12345
|
|
58
|
+
>>> _to_signed64(2**64 + 100)
|
|
59
|
+
100
|
|
60
|
+
>>> _to_signed64(-1)
|
|
61
|
+
-1
|
|
62
|
+
"""
|
|
63
|
+
return ((key + _INT64_OFFSET) & _INT64_MASK) - _INT64_OFFSET
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"try_advisory_xact_lock",
|
|
68
|
+
]
|