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.
Files changed (56) hide show
  1. sqlalchemy_foundation_kit-0.5.0/CHANGELOG.md +70 -0
  2. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/PKG-INFO +5 -3
  3. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/README.md +2 -2
  4. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/pyproject.toml +2 -0
  5. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/__init__.py +5 -1
  6. sqlalchemy_foundation_kit-0.5.0/sqlalchemy_foundation_kit/__version__.py +1 -0
  7. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/engine.py +12 -13
  8. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/config/postgres.py +1 -1
  9. sqlalchemy_foundation_kit-0.5.0/sqlalchemy_foundation_kit/contrib/metrics/postgres.py +226 -0
  10. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/settings/postgres.py +17 -5
  11. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/protocols/__init__.py +4 -0
  12. sqlalchemy_foundation_kit-0.5.0/sqlalchemy_foundation_kit/protocols/metrics.py +162 -0
  13. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/builder.py +7 -5
  14. sqlalchemy_foundation_kit-0.5.0/sqlalchemy_foundation_kit/session/connection.py +40 -0
  15. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/factories.py +5 -2
  16. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/manager.py +172 -12
  17. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/uow/protocols.py +2 -2
  18. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/uow/sqlalchemy.py +46 -23
  19. sqlalchemy_foundation_kit-0.3.0/CHANGELOG.md +0 -53
  20. sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/__version__.py +0 -1
  21. sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/contrib/metrics/postgres.py +0 -149
  22. sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/protocols/metrics.py +0 -77
  23. sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/session/connection.py +0 -33
  24. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/.gitignore +0 -0
  25. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/LICENSE +0 -0
  26. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/_typing.py +0 -0
  27. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/__init__.py +0 -0
  28. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/_optional.py +0 -0
  29. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/metadata.py +0 -0
  30. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/models.py +0 -0
  31. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/serialization.py +0 -0
  32. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/base/types.py +0 -0
  33. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/config/__init__.py +0 -0
  34. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/__init__.py +0 -0
  35. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/_metrics_utils.py +0 -0
  36. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/__init__.py +0 -0
  37. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/_base.py +0 -0
  38. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/_deps.py +0 -0
  39. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/database.py +0 -0
  40. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/metrics.py +0 -0
  41. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/di/__init__.py +0 -0
  42. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/di/_base.py +0 -0
  43. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/di/_deps.py +0 -0
  44. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/di/database.py +0 -0
  45. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/di/metrics.py +0 -0
  46. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/metrics/__init__.py +0 -0
  47. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/settings/__init__.py +0 -0
  48. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/telemetry/__init__.py +0 -0
  49. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/telemetry/instrumentations.py +0 -0
  50. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/contrib/telemetry/uow.py +0 -0
  51. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/py.typed +0 -0
  52. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/__init__.py +0 -0
  53. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/locks.py +0 -0
  54. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/session/retry.py +0 -0
  55. {sqlalchemy_foundation_kit-0.3.0 → sqlalchemy_foundation_kit-0.5.0}/sqlalchemy_foundation_kit/uow/__init__.py +0 -0
  56. {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.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 = True
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 = True
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.3.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
- Only applies pool size/overflow/recycle/timeout for pool classes that support them.
228
- Checks if the pool class accepts these parameters via hasattr to avoid passing
229
- unsupported kwargs to pools like NullPool or StaticPool.
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
- # Build pool parameters dict
237
- params = {
238
- "pool_size": pool_settings.size,
239
- "max_overflow": pool_settings.max_overflow,
240
- "pool_recycle": pool_settings.recycle,
241
- "pool_timeout": pool_settings.timeout,
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 = True
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.max_overflow > 0:
72
- raise ValueError(f"max_overflow must be 0 for static pool, got {self.max_overflow}")
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: Use orjson for JSON serialization (requires orjson).
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=True,
137
- description="Use orjson for JSON serialization (requires orjson installed)",
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
  ]