sqlalchemy-foundation-kit 0.2.1__tar.gz → 0.3.0__tar.gz
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-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/CHANGELOG.md +7 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/PKG-INFO +3 -3
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/README.md +2 -2
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/__init__.py +1 -1
- sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/__version__.py +1 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/config/postgres.py +3 -3
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/settings/postgres.py +13 -4
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/builder.py +18 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/factories.py +12 -3
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/manager.py +40 -1
- sqlalchemy_foundation_kit-0.2.1/sqlalchemy_foundation_kit/__version__.py +0 -1
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/.gitignore +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/LICENSE +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/pyproject.toml +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/_typing.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/_optional.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/engine.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/metadata.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/models.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/serialization.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/types.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/config/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/_metrics_utils.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/_base.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/_deps.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/database.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/metrics.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/di/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/di/_base.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/di/_deps.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/di/database.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/di/metrics.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/metrics/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/metrics/postgres.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/settings/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/telemetry/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/telemetry/instrumentations.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/telemetry/uow.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/protocols/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/protocols/metrics.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/py.typed +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/connection.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/locks.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/retry.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/uow/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/uow/enums.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/uow/protocols.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/uow/sqlalchemy.py +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/bedrock-python/sqlalchemy-foundation-kit/compare/sqlalchemy-foundation-kit-v0.2.1...sqlalchemy-foundation-kit-v0.3.0) (2026-09-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* stop sending jit and search_path as startup parameters ([#24](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/24)) ([704c343](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/704c343b932b15f0c6ee434f2fc80ab4732b0f80))
|
|
9
|
+
|
|
3
10
|
## [0.2.1](https://github.com/bedrock-python/sqlalchemy-foundation-kit/compare/sqlalchemy-foundation-kit-v0.2.0...sqlalchemy-foundation-kit-v0.2.1) (2026-09-06)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlalchemy-foundation-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Foundation layer for SQLAlchemy-based services with UoW, session management, and observability
|
|
5
5
|
Project-URL: Repository, https://github.com/bedrock-python/sqlalchemy-foundation-kit
|
|
6
6
|
Project-URL: Documentation, https://bedrock-python.github.io/sqlalchemy-foundation-kit/
|
|
@@ -295,7 +295,7 @@ Only `sqlalchemy[asyncio]`, `pydantic` and `asyncpg` are required by default —
|
|
|
295
295
|
✅ **Single dependency** — All foundation pieces in one place
|
|
296
296
|
✅ **Unit of Work pattern** — Transactional consistency with automatic commit/rollback
|
|
297
297
|
✅ **Connection pool management** — `AsyncSessionManager` with metrics and health checks
|
|
298
|
-
✅ **
|
|
298
|
+
✅ **PgBouncer compatible** — A startup packet PgBouncer accepts, `search_path` per transaction, unique statement names
|
|
299
299
|
✅ **Observability built-in** — Prometheus metrics + OpenTelemetry tracing
|
|
300
300
|
✅ **Type-safe configuration** — Pydantic settings with validation
|
|
301
301
|
✅ **Base ORM models** — Pre-configured `Base` with naming conventions and mixins
|
|
@@ -398,7 +398,7 @@ class PostgresConfig:
|
|
|
398
398
|
application_name: str = "my-service"
|
|
399
399
|
db_schema: str | None = None
|
|
400
400
|
use_orjson_serialization: bool = True
|
|
401
|
-
jit: str | None =
|
|
401
|
+
jit: str | None = None
|
|
402
402
|
|
|
403
403
|
def to_dsn(self) -> str:
|
|
404
404
|
return f"postgresql+asyncpg://{self.connection.user}@{self.connection.host}:{self.connection.port}/{self.connection.database}"
|
|
@@ -44,7 +44,7 @@ Only `sqlalchemy[asyncio]`, `pydantic` and `asyncpg` are required by default —
|
|
|
44
44
|
✅ **Single dependency** — All foundation pieces in one place
|
|
45
45
|
✅ **Unit of Work pattern** — Transactional consistency with automatic commit/rollback
|
|
46
46
|
✅ **Connection pool management** — `AsyncSessionManager` with metrics and health checks
|
|
47
|
-
✅ **
|
|
47
|
+
✅ **PgBouncer compatible** — A startup packet PgBouncer accepts, `search_path` per transaction, unique statement names
|
|
48
48
|
✅ **Observability built-in** — Prometheus metrics + OpenTelemetry tracing
|
|
49
49
|
✅ **Type-safe configuration** — Pydantic settings with validation
|
|
50
50
|
✅ **Base ORM models** — Pre-configured `Base` with naming conventions and mixins
|
|
@@ -147,7 +147,7 @@ class PostgresConfig:
|
|
|
147
147
|
application_name: str = "my-service"
|
|
148
148
|
db_schema: str | None = None
|
|
149
149
|
use_orjson_serialization: bool = True
|
|
150
|
-
jit: str | None =
|
|
150
|
+
jit: str | None = None
|
|
151
151
|
|
|
152
152
|
def to_dsn(self) -> str:
|
|
153
153
|
return f"postgresql+asyncpg://{self.connection.user}@{self.connection.host}:{self.connection.port}/{self.connection.database}"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -131,9 +131,9 @@ class PostgresSettingsProtocol(Protocol):
|
|
|
131
131
|
pool: Connection pool settings.
|
|
132
132
|
query: Query execution and transaction settings.
|
|
133
133
|
application_name: Application identifier for connections.
|
|
134
|
-
db_schema: Optional PostgreSQL
|
|
134
|
+
db_schema: Optional PostgreSQL ``search_path``, applied to every transaction.
|
|
135
135
|
use_orjson_serialization: Enable orjson for JSON operations.
|
|
136
|
-
jit: JIT compilation setting
|
|
136
|
+
jit: JIT compilation setting, sent as a startup parameter only when not ``None``.
|
|
137
137
|
|
|
138
138
|
Examples:
|
|
139
139
|
Implementing the protocol:
|
|
@@ -144,7 +144,7 @@ class PostgresSettingsProtocol(Protocol):
|
|
|
144
144
|
... application_name: str = "my-app"
|
|
145
145
|
... db_schema: str | None = None
|
|
146
146
|
... use_orjson_serialization: bool = True
|
|
147
|
-
... jit: str | None =
|
|
147
|
+
... jit: str | None = None
|
|
148
148
|
...
|
|
149
149
|
... def to_dsn(self) -> str:
|
|
150
150
|
... return f"postgresql://{self.connection.user}@{self.connection.host}..."
|
|
@@ -98,9 +98,12 @@ class BasePostgresConfig(BaseSettings):
|
|
|
98
98
|
pool: Connection pool configuration (size, overflow, timeouts).
|
|
99
99
|
query: Query execution settings (echo, caching, isolation level).
|
|
100
100
|
application_name: Application name for connection identification.
|
|
101
|
-
db_schema: Optional PostgreSQL schema
|
|
101
|
+
db_schema: Optional PostgreSQL ``search_path`` — a schema, or a comma-separated
|
|
102
|
+
list — applied to every transaction with ``SET LOCAL`` semantics.
|
|
102
103
|
use_orjson_serialization: Use orjson for JSON serialization (requires orjson).
|
|
103
|
-
jit: JIT compilation setting (off/on)
|
|
104
|
+
jit: JIT compilation setting (off/on), sent as a startup parameter only when set.
|
|
105
|
+
``None`` (the default) sends nothing and leaves the server's own setting; a
|
|
106
|
+
transaction-mode PgBouncer rejects the parameter unless it tracks it.
|
|
104
107
|
metrics_enabled: Enable connection pool metrics collection.
|
|
105
108
|
|
|
106
109
|
Examples:
|
|
@@ -125,12 +128,18 @@ class BasePostgresConfig(BaseSettings):
|
|
|
125
128
|
|
|
126
129
|
# Top-level settings
|
|
127
130
|
application_name: str = Field(description="Application name for PostgreSQL")
|
|
128
|
-
db_schema: str | None = Field(
|
|
131
|
+
db_schema: str | None = Field(
|
|
132
|
+
default=None,
|
|
133
|
+
description="PostgreSQL search_path (a schema or a comma-separated list), applied to every transaction",
|
|
134
|
+
)
|
|
129
135
|
use_orjson_serialization: bool = Field(
|
|
130
136
|
default=True,
|
|
131
137
|
description="Use orjson for JSON serialization (requires orjson installed)",
|
|
132
138
|
)
|
|
133
|
-
jit: PostgresJit | None = Field(
|
|
139
|
+
jit: PostgresJit | None = Field(
|
|
140
|
+
default=None,
|
|
141
|
+
description="JIT setting (off/on), sent as a startup parameter only when set",
|
|
142
|
+
)
|
|
134
143
|
metrics_enabled: bool = Field(default=False, description="Enable PostgreSQL metrics")
|
|
135
144
|
|
|
136
145
|
def __repr__(self) -> str:
|
|
@@ -77,6 +77,7 @@ class AsyncSessionManagerBuilder(Generic[SessionT]):
|
|
|
77
77
|
self._metrics: PostgresMetricsProtocol | None = None
|
|
78
78
|
self._on_engine_created: Callable[[AsyncEngine], None] | None = None
|
|
79
79
|
self._dispose_timeout: float | None = None
|
|
80
|
+
self._search_path: str | None = None
|
|
80
81
|
self._extra_kwargs: dict[str, object] = {}
|
|
81
82
|
|
|
82
83
|
def with_echo(self, echo: bool = True) -> AsyncSessionManagerBuilder[SessionT]:
|
|
@@ -258,6 +259,22 @@ class AsyncSessionManagerBuilder(Generic[SessionT]):
|
|
|
258
259
|
self._dispose_timeout = timeout
|
|
259
260
|
return self
|
|
260
261
|
|
|
262
|
+
def with_search_path(self, search_path: str) -> AsyncSessionManagerBuilder[SessionT]:
|
|
263
|
+
"""Apply a PostgreSQL ``search_path`` to every transaction.
|
|
264
|
+
|
|
265
|
+
Issued with ``SET LOCAL`` semantics at the start of each transaction — the one
|
|
266
|
+
scope a transaction-mode pooler such as PgBouncer honours. See
|
|
267
|
+
:func:`~sqlalchemy_foundation_kit.session.manager.attach_search_path`.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
search_path: A schema, or a comma-separated list such as ``"tenant_7, public"``.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Self for method chaining.
|
|
274
|
+
"""
|
|
275
|
+
self._search_path = search_path
|
|
276
|
+
return self
|
|
277
|
+
|
|
261
278
|
def build(self) -> AsyncSessionManager[SessionT]:
|
|
262
279
|
"""Build AsyncSessionManager instance with configured parameters.
|
|
263
280
|
|
|
@@ -284,6 +301,7 @@ class AsyncSessionManagerBuilder(Generic[SessionT]):
|
|
|
284
301
|
"use_orjson": self._use_orjson,
|
|
285
302
|
"metrics": self._metrics,
|
|
286
303
|
"on_engine_created": self._on_engine_created,
|
|
304
|
+
"search_path": self._search_path,
|
|
287
305
|
}
|
|
288
306
|
if self._dispose_timeout is not None:
|
|
289
307
|
kwargs["dispose_timeout"] = self._dispose_timeout
|
|
@@ -31,6 +31,13 @@ def create_async_session_manager(
|
|
|
31
31
|
) -> AsyncSessionManager[AsyncSession]:
|
|
32
32
|
"""Create async session manager with PostgreSQL-specific configuration.
|
|
33
33
|
|
|
34
|
+
The startup packet carries ``application_name`` and, only when the config sets it,
|
|
35
|
+
``jit``; nothing else, because a transaction-mode pooler such as PgBouncer rejects
|
|
36
|
+
startup parameters it does not track (``unsupported startup parameter: jit``) or,
|
|
37
|
+
with ``ignore_startup_parameters``, silently drops them. ``db_schema`` therefore
|
|
38
|
+
travels as the manager's ``search_path`` and is applied with ``SET LOCAL`` semantics
|
|
39
|
+
at the start of every transaction, which is the one scope such a pooler honours.
|
|
40
|
+
|
|
34
41
|
Args:
|
|
35
42
|
postgres_config: PostgreSQL configuration implementing PostgresSettingsProtocol.
|
|
36
43
|
application_name: Optional custom application name. If None, uses postgres_config.application_name.
|
|
@@ -41,7 +48,8 @@ def create_async_session_manager(
|
|
|
41
48
|
which provides pgbouncer transaction-mode compatibility.
|
|
42
49
|
extra_server_settings: Additional PostgreSQL ``server_settings`` to merge with defaults
|
|
43
50
|
(e.g., ``{"statement_timeout": "30000", "timezone": "UTC"}``). User-provided keys
|
|
44
|
-
override library defaults.
|
|
51
|
+
override library defaults. These are startup parameters: through PgBouncer only
|
|
52
|
+
the ones it tracks arrive (``track_extra_parameters``).
|
|
45
53
|
extra_connect_args: Additional asyncpg ``connect_args`` to merge with defaults
|
|
46
54
|
(e.g., ``{"command_timeout": 60}``). User-provided keys override library defaults.
|
|
47
55
|
**kwargs: Additional keyword arguments passed to AsyncSessionManager.
|
|
@@ -73,11 +81,11 @@ def create_async_session_manager(
|
|
|
73
81
|
"""
|
|
74
82
|
app_name = application_name or postgres_config.application_name
|
|
75
83
|
|
|
76
|
-
# Build server settings with optional overrides
|
|
84
|
+
# Build server settings with optional overrides. These are startup parameters, so
|
|
85
|
+
# search_path is deliberately not among them -- see the docstring.
|
|
77
86
|
server_settings: dict[str, str] = {
|
|
78
87
|
"application_name": app_name,
|
|
79
88
|
**({"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
89
|
**(extra_server_settings or {}),
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -100,5 +108,6 @@ def create_async_session_manager(
|
|
|
100
108
|
use_orjson=postgres_config.use_orjson_serialization,
|
|
101
109
|
metrics=metrics,
|
|
102
110
|
on_engine_created=on_engine_created,
|
|
111
|
+
search_path=postgres_config.db_schema,
|
|
103
112
|
**kwargs,
|
|
104
113
|
)
|
|
@@ -10,7 +10,7 @@ from contextlib import asynccontextmanager
|
|
|
10
10
|
from types import TracebackType
|
|
11
11
|
from typing import TYPE_CHECKING, Any, Generic, cast
|
|
12
12
|
|
|
13
|
-
from sqlalchemy import event
|
|
13
|
+
from sqlalchemy import event, text
|
|
14
14
|
from sqlalchemy.exc import TimeoutError as SATimeoutError
|
|
15
15
|
from sqlalchemy.ext.asyncio import (
|
|
16
16
|
AsyncEngine,
|
|
@@ -23,6 +23,8 @@ from .._typing import SessionT
|
|
|
23
23
|
from ..base import build_engine_kwargs, resolve_pool_class
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
+
from sqlalchemy.engine import Connection
|
|
27
|
+
|
|
26
28
|
from ..config import PoolSettingsProtocol
|
|
27
29
|
from ..protocols import PostgresMetricsProtocol
|
|
28
30
|
|
|
@@ -100,6 +102,35 @@ def attach_metrics(engine: AsyncEngine, metrics: PostgresMetricsProtocol) -> Non
|
|
|
100
102
|
event.listen(engine.sync_engine, "handle_error", on_error)
|
|
101
103
|
|
|
102
104
|
|
|
105
|
+
def attach_search_path(engine: AsyncEngine, search_path: str) -> None:
|
|
106
|
+
"""Apply ``search_path`` to every transaction the engine begins.
|
|
107
|
+
|
|
108
|
+
Registers a ``begin`` listener that issues ``set_config('search_path', …, true)`` —
|
|
109
|
+
the function form of ``SET LOCAL``, with the value as a bind parameter — as the first
|
|
110
|
+
statement of each transaction, so the setting lives exactly as long as the transaction
|
|
111
|
+
does. That is the one scope a transaction-mode pooler such as PgBouncer honours: a
|
|
112
|
+
startup parameter is rejected or dropped before it reaches PostgreSQL, and a plain
|
|
113
|
+
``SET`` on a server connection leaks to whichever client is handed it next.
|
|
114
|
+
|
|
115
|
+
Under the asyncpg adapter ``BEGIN`` is sent lazily with the first statement, so the
|
|
116
|
+
``set_config`` call lands inside the transaction rather than ahead of it. Every
|
|
117
|
+
transaction is covered — a session's autobegin, ``session.begin()``, a raw
|
|
118
|
+
``engine.connect()``, the one that follows a ``commit()`` — but a statement run with
|
|
119
|
+
``isolation_level="AUTOCOMMIT"`` begins none and runs with the server's default.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
engine: SQLAlchemy ``AsyncEngine`` to attach the listener to.
|
|
123
|
+
search_path: Value for ``search_path`` — a schema, or a comma-separated list such
|
|
124
|
+
as ``"tenant_7, public"``.
|
|
125
|
+
"""
|
|
126
|
+
statement = text("SELECT set_config('search_path', :search_path, true)")
|
|
127
|
+
|
|
128
|
+
def on_begin(conn: Connection) -> None:
|
|
129
|
+
conn.execute(statement, {"search_path": search_path})
|
|
130
|
+
|
|
131
|
+
event.listen(engine.sync_engine, "begin", on_begin)
|
|
132
|
+
|
|
133
|
+
|
|
103
134
|
class AsyncSessionManager(Generic[SessionT]):
|
|
104
135
|
"""Manages async database sessions with configurable connection pooling.
|
|
105
136
|
|
|
@@ -124,6 +155,7 @@ class AsyncSessionManager(Generic[SessionT]):
|
|
|
124
155
|
metrics: PostgresMetricsProtocol | None = None,
|
|
125
156
|
on_engine_created: Callable[[AsyncEngine], None] | None = None,
|
|
126
157
|
dispose_timeout: float = DEFAULT_DISPOSE_TIMEOUT_SECONDS,
|
|
158
|
+
search_path: str | None = None,
|
|
127
159
|
**kwargs: object,
|
|
128
160
|
) -> None:
|
|
129
161
|
"""Initialize session manager with direct configuration.
|
|
@@ -145,6 +177,10 @@ class AsyncSessionManager(Generic[SessionT]):
|
|
|
145
177
|
dispose_timeout: Maximum seconds to wait for engine disposal in :meth:`aclose`
|
|
146
178
|
(default: 30.0). Lower this in tests or short-lived environments; raise it
|
|
147
179
|
if you have long-running transactions that need more time to settle.
|
|
180
|
+
search_path: PostgreSQL ``search_path`` applied to every transaction with
|
|
181
|
+
``SET LOCAL`` semantics, which is what survives a transaction-mode pooler
|
|
182
|
+
(default: None — nothing is sent, the server default applies). See
|
|
183
|
+
:func:`attach_search_path`.
|
|
148
184
|
**kwargs: Additional keyword arguments for ``create_async_engine``.
|
|
149
185
|
"""
|
|
150
186
|
self._closed = False
|
|
@@ -174,6 +210,9 @@ class AsyncSessionManager(Generic[SessionT]):
|
|
|
174
210
|
if metrics:
|
|
175
211
|
attach_metrics(self._engine, metrics)
|
|
176
212
|
|
|
213
|
+
if search_path is not None:
|
|
214
|
+
attach_search_path(self._engine, search_path)
|
|
215
|
+
|
|
177
216
|
if on_engine_created is not None:
|
|
178
217
|
on_engine_created(self._engine)
|
|
179
218
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|