sqlalchemy-foundation-kit 0.3.0__tar.gz → 0.5.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.5.0/CHANGELOG.md +70 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/PKG-INFO +5 -3
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/README.md +2 -2
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/pyproject.toml +2 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/__init__.py +5 -1
- sqlalchemy_foundation_kit-0.5.0/sqlalchemy_foundation_kit/__version__.py +1 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/engine.py +12 -13
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/config/postgres.py +1 -1
- sqlalchemy_foundation_kit-0.5.0/sqlalchemy_foundation_kit/contrib/metrics/postgres.py +226 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/settings/postgres.py +17 -5
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/protocols/__init__.py +4 -0
- sqlalchemy_foundation_kit-0.5.0/sqlalchemy_foundation_kit/protocols/metrics.py +162 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/builder.py +7 -5
- sqlalchemy_foundation_kit-0.5.0/sqlalchemy_foundation_kit/session/connection.py +40 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/factories.py +5 -2
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/manager.py +172 -12
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/uow/protocols.py +2 -2
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/uow/sqlalchemy.py +46 -23
- sqlalchemy_foundation_kit-0.3.0/CHANGELOG.md +0 -53
- sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/__version__.py +0 -1
- sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/contrib/metrics/postgres.py +0 -149
- sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/protocols/metrics.py +0 -77
- sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/session/connection.py +0 -33
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/.gitignore +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/LICENSE +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/_typing.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/_optional.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/metadata.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/models.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/serialization.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/types.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/config/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/_metrics_utils.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/_base.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/_deps.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/database.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/metrics.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/di/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/di/_base.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/di/_deps.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/di/database.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/di/metrics.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/metrics/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/settings/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/telemetry/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/telemetry/instrumentations.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/telemetry/uow.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/py.typed +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/locks.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/retry.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/uow/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/uow/enums.py +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.5.0](https://github.com/bedrock-python/sqlalchemy-foundation-kit/compare/sqlalchemy-foundation-kit-v0.4.0...sqlalchemy-foundation-kit-v0.5.0) (2026-10-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* BasePostgresConfig.use_orjson_serialization now defaults to False, matching AsyncSessionManager and the builder. A service that has orjson installed, kept the default, and writes values only orjson serializes (datetime, UUID, Decimal, non-str keys) into JSON columns now gets StatementError (TypeError: Object of type datetime is not JSON serializable). Set use_orjson_serialization=True to keep the old behaviour.
|
|
9
|
+
* uow.query() now opens its transaction READ ONLY. A write inside it (INSERT, UPDATE, DELETE, DDL, nextval()) fails with SQLSTATE 25006 instead of being rolled back silently, or persisted when followed by an explicit session.commit(). Move writes to uow.transaction().
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* build null and static pools from PoolSettings, and stop recommending the sync queue pool ([#61](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/61)) ([608d327](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/608d32747361321895f6fc43f5e77da6937375ff))
|
|
14
|
+
* count connect timeouts, not statement timeouts, in connection_timeouts_total ([#63](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/63)) ([961e0e8](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/961e0e862cb540525cda99b5ecc79ee12cbab2e4))
|
|
15
|
+
* default use_orjson_serialization to False, so the [settings] quick start runs ([#66](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/66)) ([3addfdd](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/3addfdd50c98657966867b680121facd596b6810))
|
|
16
|
+
* read the pool gauges when they are scraped, and never export a negative overflow ([#62](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/62)) ([e85ac96](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/e85ac96bd875ee47729f029c8046ea85be48f712))
|
|
17
|
+
* run query() in a READ ONLY transaction, as documented ([#64](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/64)) ([cb77404](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/cb7740468cc86d1d7e9f7bda80d56b27b0300739))
|
|
18
|
+
* stop logging a failed flush's parameters and row values ([#65](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/65)) ([6dd88ba](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/6dd88ba34de6f6fa3306f7e9002788ab57f7797c))
|
|
19
|
+
|
|
20
|
+
## [0.4.0](https://github.com/bedrock-python/sqlalchemy-foundation-kit/compare/sqlalchemy-foundation-kit-v0.3.0...sqlalchemy-foundation-kit-v0.4.0) (2026-09-07)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* measure the wait for a pool connection, and count pool checkout timeouts ([#27](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/27)) ([78c9d7e](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/78c9d7e2c05052090276bce08219047849cd6dbb))
|
|
26
|
+
|
|
27
|
+
## [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)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
* 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))
|
|
33
|
+
|
|
34
|
+
## [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)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Bug Fixes
|
|
38
|
+
|
|
39
|
+
* the defects the agents page turned up ([#21](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/21)) ([9548c72](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/9548c72670696dd68e0eeec1f0ae4ad896fa62d6))
|
|
40
|
+
|
|
41
|
+
## [0.2.0](https://github.com/bedrock-python/sqlalchemy-foundation-kit/compare/sqlalchemy-foundation-kit-v0.1.0...sqlalchemy-foundation-kit-v0.2.0) (2026-08-28)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Features
|
|
45
|
+
|
|
46
|
+
* **uow:** add savepoint() to AsyncSQLAlchemyUowTransaction ([#6](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/6)) ([c46d881](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/c46d881052b7476211c31f4adc6058d4f5500b6a)), closes [#5](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/5)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### Bug Fixes
|
|
50
|
+
|
|
51
|
+
* **ci:** add workflow_dispatch and created trigger to publish workflow ([142aef5](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/142aef5040777b31509e8d7b26f01774e8f22432))
|
|
52
|
+
|
|
53
|
+
## 0.1.0 (2026-05-19)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
### Features
|
|
57
|
+
|
|
58
|
+
* add managed_session method for manual transaction control and tracing ([776407b](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/776407b5dbe78d314e7525bd68ae8c9c772f78a7))
|
|
59
|
+
* **contrib:** add dependency-injector support, rename di to dishka, add test suite ([3bbf3e4](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/3bbf3e46b8ecd0419652dc53d1cee7d1fdbf031b))
|
|
60
|
+
* initial sqlalchemy-foundation-kit library ([738b4f1](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/738b4f126e03d0e7dc8852d30b0cf76b67bbbc57))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Bug Fixes
|
|
64
|
+
|
|
65
|
+
* comprehensive code review improvements ([487c283](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/487c2835e77c93cd64f9f00c73b7ae7a84673548))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
### Documentation
|
|
69
|
+
|
|
70
|
+
* **foundation-kit:** add comprehensive documentation for all features ([8d10f84](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/8d10f84179273a8cc40dc1fe326bddab84379d32))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlalchemy-foundation-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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/
|
|
@@ -216,6 +216,8 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
216
216
|
Classifier: Programming Language :: Python :: 3
|
|
217
217
|
Classifier: Programming Language :: Python :: 3.11
|
|
218
218
|
Classifier: Programming Language :: Python :: 3.12
|
|
219
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
220
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
219
221
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
220
222
|
Classifier: Typing :: Typed
|
|
221
223
|
Requires-Python: >=3.11
|
|
@@ -397,7 +399,7 @@ class PostgresConfig:
|
|
|
397
399
|
query: QuerySettingsProtocol = MyQuerySettings()
|
|
398
400
|
application_name: str = "my-service"
|
|
399
401
|
db_schema: str | None = None
|
|
400
|
-
use_orjson_serialization: bool =
|
|
402
|
+
use_orjson_serialization: bool = False
|
|
401
403
|
jit: str | None = None
|
|
402
404
|
|
|
403
405
|
def to_dsn(self) -> str:
|
|
@@ -494,7 +496,7 @@ from sqlalchemy_foundation_kit.contrib.settings import (
|
|
|
494
496
|
from sqlalchemy_foundation_kit.contrib.metrics import PostgresMetrics
|
|
495
497
|
```
|
|
496
498
|
- `PostgresMetrics` — Prometheus metrics for connection pool
|
|
497
|
-
- Tracks: pool size, checked out connections, checkout duration, errors
|
|
499
|
+
- Tracks: pool size, checked out connections, checkout wait, held duration, timeouts, errors
|
|
498
500
|
|
|
499
501
|
#### `contrib.di` (requires `[dishka]`)
|
|
500
502
|
```python
|
|
@@ -146,7 +146,7 @@ class PostgresConfig:
|
|
|
146
146
|
query: QuerySettingsProtocol = MyQuerySettings()
|
|
147
147
|
application_name: str = "my-service"
|
|
148
148
|
db_schema: str | None = None
|
|
149
|
-
use_orjson_serialization: bool =
|
|
149
|
+
use_orjson_serialization: bool = False
|
|
150
150
|
jit: str | None = None
|
|
151
151
|
|
|
152
152
|
def to_dsn(self) -> str:
|
|
@@ -243,7 +243,7 @@ from sqlalchemy_foundation_kit.contrib.settings import (
|
|
|
243
243
|
from sqlalchemy_foundation_kit.contrib.metrics import PostgresMetrics
|
|
244
244
|
```
|
|
245
245
|
- `PostgresMetrics` — Prometheus metrics for connection pool
|
|
246
|
-
- Tracks: pool size, checked out connections, checkout duration, errors
|
|
246
|
+
- Tracks: pool size, checked out connections, checkout wait, held duration, timeouts, errors
|
|
247
247
|
|
|
248
248
|
#### `contrib.di` (requires `[dishka]`)
|
|
249
249
|
```python
|
|
@@ -25,6 +25,8 @@ classifiers = [
|
|
|
25
25
|
"Programming Language :: Python :: 3",
|
|
26
26
|
"Programming Language :: Python :: 3.11",
|
|
27
27
|
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
28
30
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
31
|
"Typing :: Typed",
|
|
30
32
|
]
|
|
@@ -31,7 +31,9 @@ from .config import (
|
|
|
31
31
|
# Protocols
|
|
32
32
|
from .protocols import (
|
|
33
33
|
CheckoutRecorder,
|
|
34
|
+
CheckoutWaitRecorder,
|
|
34
35
|
ErrorRecorder,
|
|
36
|
+
PoolStatsFunctionRecorder,
|
|
35
37
|
PoolStatsRecorder,
|
|
36
38
|
PostgresMetricsProtocol,
|
|
37
39
|
)
|
|
@@ -64,7 +66,7 @@ from .uow import (
|
|
|
64
66
|
try:
|
|
65
67
|
__version__ = version("sqlalchemy-foundation-kit")
|
|
66
68
|
except PackageNotFoundError: # pragma: no cover
|
|
67
|
-
__version__ = "0.
|
|
69
|
+
__version__ = "0.5.0"
|
|
68
70
|
|
|
69
71
|
__all__ = [ # noqa: RUF022
|
|
70
72
|
# Base ORM
|
|
@@ -89,7 +91,9 @@ __all__ = [ # noqa: RUF022
|
|
|
89
91
|
"QuerySettingsProtocol",
|
|
90
92
|
# Protocols
|
|
91
93
|
"CheckoutRecorder",
|
|
94
|
+
"CheckoutWaitRecorder",
|
|
92
95
|
"ErrorRecorder",
|
|
96
|
+
"PoolStatsFunctionRecorder",
|
|
93
97
|
"PoolStatsRecorder",
|
|
94
98
|
"PostgresMetricsProtocol",
|
|
95
99
|
# Session Management
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -224,26 +224,25 @@ def _apply_pool_settings(
|
|
|
224
224
|
Maps pool settings to SQLAlchemy's expected ``pool_*`` keyword arguments.
|
|
225
225
|
Note: pool_pre_ping is already set in build_engine_kwargs, so not applied here.
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
``pool_recycle`` applies to every pool class. ``pool_size``, ``max_overflow`` and
|
|
228
|
+
``pool_timeout`` describe a queue of connections and exist only on ``QueuePool``
|
|
229
|
+
and its subclasses -- the async queue pools, a registered subclass, the class
|
|
230
|
+
``instrument_pool_class`` builds. ``create_async_engine`` rejects them for any other
|
|
231
|
+
pool (``NullPool``, ``StaticPool``), so they are left out there.
|
|
230
232
|
|
|
231
233
|
Args:
|
|
232
234
|
engine_kwargs: Dictionary to update with pool configuration.
|
|
233
235
|
poolclass: Pool class being configured.
|
|
234
236
|
pool_settings: Pool configuration settings.
|
|
235
237
|
"""
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
params: dict[str, object] = {"pool_recycle": pool_settings.recycle}
|
|
239
|
+
if issubclass(poolclass, QueuePool):
|
|
240
|
+
params.update(
|
|
241
|
+
pool_size=pool_settings.size,
|
|
242
|
+
max_overflow=pool_settings.max_overflow,
|
|
243
|
+
pool_timeout=pool_settings.timeout,
|
|
244
|
+
)
|
|
243
245
|
|
|
244
|
-
# Apply only non-None parameters
|
|
245
|
-
# SQLAlchemy pool classes that don't support these params will ignore them
|
|
246
|
-
# or raise TypeError if passed, so we rely on the pool class itself to validate
|
|
247
246
|
engine_kwargs.update({k: v for k, v in params.items() if v is not None})
|
|
248
247
|
|
|
249
248
|
|
|
@@ -143,7 +143,7 @@ class PostgresSettingsProtocol(Protocol):
|
|
|
143
143
|
... query: QuerySettingsProtocol
|
|
144
144
|
... application_name: str = "my-app"
|
|
145
145
|
... db_schema: str | None = None
|
|
146
|
-
... use_orjson_serialization: bool =
|
|
146
|
+
... use_orjson_serialization: bool = False
|
|
147
147
|
... jit: str | None = None
|
|
148
148
|
...
|
|
149
149
|
... def to_dsn(self) -> str:
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Postgres metrics using prometheus-client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from prometheus_client import Counter, Gauge, Histogram
|
|
10
|
+
|
|
11
|
+
HAS_PROMETHEUS = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
HAS_PROMETHEUS = False
|
|
14
|
+
|
|
15
|
+
# Default buckets for how long a connection was held (seconds)
|
|
16
|
+
CONNECTION_CHECKOUT_BUCKETS: tuple[float, ...] = (
|
|
17
|
+
0.001,
|
|
18
|
+
0.005,
|
|
19
|
+
0.01,
|
|
20
|
+
0.025,
|
|
21
|
+
0.05,
|
|
22
|
+
0.1,
|
|
23
|
+
0.25,
|
|
24
|
+
0.5,
|
|
25
|
+
1.0,
|
|
26
|
+
2.5,
|
|
27
|
+
5.0,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Default buckets for the wait for a connection from the pool (seconds). The range runs to
|
|
31
|
+
# 30 s, the default ``pool_timeout``, so a saturated pool fills the top buckets instead of
|
|
32
|
+
# collapsing into +Inf just when the histogram is worth reading.
|
|
33
|
+
CONNECTION_WAIT_BUCKETS: tuple[float, ...] = (
|
|
34
|
+
0.001,
|
|
35
|
+
0.005,
|
|
36
|
+
0.01,
|
|
37
|
+
0.025,
|
|
38
|
+
0.05,
|
|
39
|
+
0.1,
|
|
40
|
+
0.25,
|
|
41
|
+
0.5,
|
|
42
|
+
1.0,
|
|
43
|
+
2.5,
|
|
44
|
+
5.0,
|
|
45
|
+
10.0,
|
|
46
|
+
30.0,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _check_prometheus() -> None:
|
|
51
|
+
"""Check if prometheus-client is installed."""
|
|
52
|
+
if not HAS_PROMETHEUS:
|
|
53
|
+
raise ImportError(
|
|
54
|
+
"prometheus-client is required for PostgresMetrics. "
|
|
55
|
+
"Install it with: pip install 'sqlalchemy-foundation-kit[metrics]'"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PostgresMetrics:
|
|
60
|
+
"""Postgres connection pool metrics.
|
|
61
|
+
|
|
62
|
+
Metrics:
|
|
63
|
+
- postgres_db_pool_size: Configured pool_size of the connection pool.
|
|
64
|
+
- postgres_db_pool_checked_out: Number of connections currently checked out.
|
|
65
|
+
- postgres_db_pool_overflow: Number of open connections over pool_size (within max_overflow).
|
|
66
|
+
- postgres_db_connection_checkout_wait_seconds: Time a caller waited for a connection.
|
|
67
|
+
- postgres_db_connection_held_duration_seconds: Time a connection was held by its caller.
|
|
68
|
+
- postgres_db_connection_checkout_duration_seconds: Deprecated alias of the held duration,
|
|
69
|
+
published unchanged for one more minor release.
|
|
70
|
+
- postgres_db_connection_timeouts_total: Number of times acquiring a connection timed out:
|
|
71
|
+
pool checkout timeouts and connect timeouts. Statement timeouts are not counted here.
|
|
72
|
+
- postgres_db_connection_errors_total: Number of connection errors.
|
|
73
|
+
|
|
74
|
+
Labels:
|
|
75
|
+
- error_type: Type of connection error (for errors_total).
|
|
76
|
+
|
|
77
|
+
The three pool gauges are read from the pool when the registry is collected, not
|
|
78
|
+
sampled from pool events, so an idle pool reports what it holds. One instance reports
|
|
79
|
+
one engine: attached to a second engine, the gauges follow the second.
|
|
80
|
+
|
|
81
|
+
The wait histogram and the timeout counter are fed by the instrumented pool class the
|
|
82
|
+
session manager builds, so they only move on an engine created by
|
|
83
|
+
``AsyncSessionManager`` (or one whose pool went through ``instrument_pool_class``).
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, prefix: str | None = None) -> None:
|
|
87
|
+
"""Initialize postgres metrics.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
prefix: Metric name prefix.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ImportError: If prometheus-client is not installed.
|
|
94
|
+
"""
|
|
95
|
+
_check_prometheus()
|
|
96
|
+
|
|
97
|
+
self.pool_size = Gauge(
|
|
98
|
+
_make_metric_name("postgres_db_pool_size", prefix),
|
|
99
|
+
"Configured pool_size of the database connection pool",
|
|
100
|
+
)
|
|
101
|
+
self.pool_checked_out = Gauge(
|
|
102
|
+
_make_metric_name("postgres_db_pool_checked_out", prefix),
|
|
103
|
+
"Number of database connections currently checked out",
|
|
104
|
+
)
|
|
105
|
+
self.pool_overflow = Gauge(
|
|
106
|
+
_make_metric_name("postgres_db_pool_overflow", prefix),
|
|
107
|
+
"Number of open connections over pool_size (within max_overflow)",
|
|
108
|
+
)
|
|
109
|
+
self.connection_checkout_wait = Histogram(
|
|
110
|
+
_make_metric_name("postgres_db_connection_checkout_wait_seconds", prefix),
|
|
111
|
+
"Time a caller waited to acquire a connection from the pool",
|
|
112
|
+
buckets=list(CONNECTION_WAIT_BUCKETS),
|
|
113
|
+
)
|
|
114
|
+
self.connection_held_duration = Histogram(
|
|
115
|
+
_make_metric_name("postgres_db_connection_held_duration_seconds", prefix),
|
|
116
|
+
"Time a connection was held by its caller, from checkout to checkin",
|
|
117
|
+
buckets=list(CONNECTION_CHECKOUT_BUCKETS),
|
|
118
|
+
)
|
|
119
|
+
self.connection_checkout_duration = Histogram(
|
|
120
|
+
_make_metric_name("postgres_db_connection_checkout_duration_seconds", prefix),
|
|
121
|
+
"Deprecated: held duration, published under its old name. Use postgres_db_connection_held_duration_seconds",
|
|
122
|
+
buckets=list(CONNECTION_CHECKOUT_BUCKETS),
|
|
123
|
+
)
|
|
124
|
+
self.connection_timeouts_total = Counter(
|
|
125
|
+
_make_metric_name("postgres_db_connection_timeouts_total", prefix),
|
|
126
|
+
"Number of times acquiring a connection timed out: pool checkout timeouts and connect timeouts",
|
|
127
|
+
)
|
|
128
|
+
self.connection_errors_total = Counter(
|
|
129
|
+
_make_metric_name("postgres_db_connection_errors_total", prefix),
|
|
130
|
+
"Number of connection errors",
|
|
131
|
+
["error_type"],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def record_pool_stats(
|
|
135
|
+
self,
|
|
136
|
+
pool_size: int,
|
|
137
|
+
pool_checked_out: int,
|
|
138
|
+
pool_overflow: int,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Record database connection pool statistics."""
|
|
141
|
+
self.pool_size.set(float(pool_size))
|
|
142
|
+
self.pool_checked_out.set(float(pool_checked_out))
|
|
143
|
+
self.pool_overflow.set(float(pool_overflow))
|
|
144
|
+
|
|
145
|
+
def set_pool_stats_functions(
|
|
146
|
+
self,
|
|
147
|
+
pool_size: Callable[[], float],
|
|
148
|
+
pool_checked_out: Callable[[], float],
|
|
149
|
+
pool_overflow: Callable[[], float],
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Read the pool gauges from these functions whenever the registry is collected."""
|
|
152
|
+
self.pool_size.set_function(pool_size)
|
|
153
|
+
self.pool_checked_out.set_function(pool_checked_out)
|
|
154
|
+
self.pool_overflow.set_function(pool_overflow)
|
|
155
|
+
|
|
156
|
+
def record_checkout(
|
|
157
|
+
self,
|
|
158
|
+
duration: float,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Record how long a connection was held, from checkout to checkin.
|
|
161
|
+
|
|
162
|
+
Observed into ``postgres_db_connection_held_duration_seconds`` and, until the
|
|
163
|
+
deprecated name is dropped, into ``postgres_db_connection_checkout_duration_seconds``
|
|
164
|
+
as well.
|
|
165
|
+
"""
|
|
166
|
+
self.connection_held_duration.observe(duration)
|
|
167
|
+
self.connection_checkout_duration.observe(duration)
|
|
168
|
+
|
|
169
|
+
def record_checkout_wait(
|
|
170
|
+
self,
|
|
171
|
+
duration: float,
|
|
172
|
+
timed_out: bool = False,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Record the wait for a connection from the pool, and count a timeout acquiring it.
|
|
175
|
+
|
|
176
|
+
A pool checkout timeout and a connect timeout never reach the engine's
|
|
177
|
+
``handle_error`` listener; this is the only place either is counted.
|
|
178
|
+
"""
|
|
179
|
+
self.connection_checkout_wait.observe(duration)
|
|
180
|
+
if timed_out:
|
|
181
|
+
self.connection_timeouts_total.inc()
|
|
182
|
+
|
|
183
|
+
def record_error(
|
|
184
|
+
self,
|
|
185
|
+
error_type: str,
|
|
186
|
+
is_timeout: bool = False,
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Record a database connection or execution error.
|
|
189
|
+
|
|
190
|
+
``is_timeout`` is kept for compatibility and still counts a timeout when a custom
|
|
191
|
+
caller sets it; the library itself no longer does.
|
|
192
|
+
"""
|
|
193
|
+
self.connection_errors_total.labels(error_type=error_type).inc()
|
|
194
|
+
if is_timeout:
|
|
195
|
+
self.connection_timeouts_total.inc()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
_PREFIX_PATTERN: re.Pattern[str] = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _make_metric_name(name: str, prefix: str | None = None) -> str:
|
|
202
|
+
"""Make metric name with optional prefix.
|
|
203
|
+
|
|
204
|
+
Prefix must follow Prometheus naming conventions (^[a-zA-Z_][a-zA-Z0-9_]*$).
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
name: Base metric name.
|
|
208
|
+
prefix: Optional prefix for the metric.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
The combined metric name.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ValueError: If the prefix is invalid.
|
|
215
|
+
"""
|
|
216
|
+
if prefix:
|
|
217
|
+
if not _PREFIX_PATTERN.match(prefix):
|
|
218
|
+
raise ValueError(
|
|
219
|
+
f"Invalid metric prefix: '{prefix}'. "
|
|
220
|
+
"Prefixes must start with a letter or underscore and contain only letters, numbers, or underscores."
|
|
221
|
+
)
|
|
222
|
+
return f"{prefix}_{name}"
|
|
223
|
+
return name
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
__all__ = ["PostgresMetrics"]
|
|
@@ -47,8 +47,15 @@ class PoolSettings(BaseModel):
|
|
|
47
47
|
|
|
48
48
|
Groups all connection pool-related parameters for SQLAlchemy engine.
|
|
49
49
|
|
|
50
|
+
``size``, ``max_overflow`` and ``timeout`` apply to the queue pools
|
|
51
|
+
(``async_adapted_queue``, ``fallback_async_adapted_queue``) and are not passed to any
|
|
52
|
+
other pool. ``static`` is SQLAlchemy's ``StaticPool``: one connection shared by every
|
|
53
|
+
session, which concurrent sessions cannot use safely -- for tests, never as a
|
|
54
|
+
fixed-size pool. A fixed-size pool is ``async_adapted_queue`` with ``max_overflow=0``.
|
|
55
|
+
|
|
50
56
|
Examples:
|
|
51
57
|
>>> pool = PoolSettings(size=10, max_overflow=20)
|
|
58
|
+
>>> fixed = PoolSettings(size=10, max_overflow=0)
|
|
52
59
|
"""
|
|
53
60
|
|
|
54
61
|
kind: PoolClassStr = Field(
|
|
@@ -68,8 +75,11 @@ class PoolSettings(BaseModel):
|
|
|
68
75
|
@model_validator(mode="after")
|
|
69
76
|
def _validate_pool_settings(self) -> PoolSettings:
|
|
70
77
|
"""Validate pool configuration constraints."""
|
|
71
|
-
if self.kind == "static" and self.
|
|
72
|
-
raise ValueError(
|
|
78
|
+
if self.kind == "static" and self.size > 1:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"size must be 1 for a static pool, got {self.size}: a static pool is one connection "
|
|
81
|
+
"shared by every session. For a fixed-size pool use kind='async_adapted_queue' with max_overflow=0"
|
|
82
|
+
)
|
|
73
83
|
return self
|
|
74
84
|
|
|
75
85
|
|
|
@@ -100,7 +110,9 @@ class BasePostgresConfig(BaseSettings):
|
|
|
100
110
|
application_name: Application name for connection identification.
|
|
101
111
|
db_schema: Optional PostgreSQL ``search_path`` — a schema, or a comma-separated
|
|
102
112
|
list — applied to every transaction with ``SET LOCAL`` semantics.
|
|
103
|
-
use_orjson_serialization:
|
|
113
|
+
use_orjson_serialization: Serialize JSON / JSONB columns with orjson. Off by default:
|
|
114
|
+
it needs the ``orjson`` extra, and orjson serializes some values (``datetime``,
|
|
115
|
+
``UUID``, ``Decimal``, non-``str`` keys) that ``json.dumps`` rejects.
|
|
104
116
|
jit: JIT compilation setting (off/on), sent as a startup parameter only when set.
|
|
105
117
|
``None`` (the default) sends nothing and leaves the server's own setting; a
|
|
106
118
|
transaction-mode PgBouncer rejects the parameter unless it tracks it.
|
|
@@ -133,8 +145,8 @@ class BasePostgresConfig(BaseSettings):
|
|
|
133
145
|
description="PostgreSQL search_path (a schema or a comma-separated list), applied to every transaction",
|
|
134
146
|
)
|
|
135
147
|
use_orjson_serialization: bool = Field(
|
|
136
|
-
default=
|
|
137
|
-
description="Use orjson for JSON serialization (requires orjson
|
|
148
|
+
default=False,
|
|
149
|
+
description="Use orjson for JSON serialization (requires the orjson extra)",
|
|
138
150
|
)
|
|
139
151
|
jit: PostgresJit | None = Field(
|
|
140
152
|
default=None,
|
|
@@ -8,14 +8,18 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from .metrics import (
|
|
10
10
|
CheckoutRecorder,
|
|
11
|
+
CheckoutWaitRecorder,
|
|
11
12
|
ErrorRecorder,
|
|
13
|
+
PoolStatsFunctionRecorder,
|
|
12
14
|
PoolStatsRecorder,
|
|
13
15
|
PostgresMetricsProtocol,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
__all__ = [
|
|
17
19
|
"CheckoutRecorder",
|
|
20
|
+
"CheckoutWaitRecorder",
|
|
18
21
|
"ErrorRecorder",
|
|
22
|
+
"PoolStatsFunctionRecorder",
|
|
19
23
|
"PoolStatsRecorder",
|
|
20
24
|
"PostgresMetricsProtocol",
|
|
21
25
|
]
|