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.
Files changed (49) hide show
  1. sqlalchemy_foundation_kit/__init__.py +119 -0
  2. sqlalchemy_foundation_kit/__version__.py +1 -0
  3. sqlalchemy_foundation_kit/_typing.py +28 -0
  4. sqlalchemy_foundation_kit/base/__init__.py +46 -0
  5. sqlalchemy_foundation_kit/base/_optional.py +37 -0
  6. sqlalchemy_foundation_kit/base/engine.py +256 -0
  7. sqlalchemy_foundation_kit/base/metadata.py +57 -0
  8. sqlalchemy_foundation_kit/base/models.py +101 -0
  9. sqlalchemy_foundation_kit/base/serialization.py +98 -0
  10. sqlalchemy_foundation_kit/base/types.py +72 -0
  11. sqlalchemy_foundation_kit/config/__init__.py +17 -0
  12. sqlalchemy_foundation_kit/config/postgres.py +177 -0
  13. sqlalchemy_foundation_kit/contrib/__init__.py +5 -0
  14. sqlalchemy_foundation_kit/contrib/_metrics_utils.py +19 -0
  15. sqlalchemy_foundation_kit/contrib/dependency_injector/__init__.py +27 -0
  16. sqlalchemy_foundation_kit/contrib/dependency_injector/_base.py +27 -0
  17. sqlalchemy_foundation_kit/contrib/dependency_injector/_deps.py +37 -0
  18. sqlalchemy_foundation_kit/contrib/dependency_injector/database.py +196 -0
  19. sqlalchemy_foundation_kit/contrib/dependency_injector/metrics.py +85 -0
  20. sqlalchemy_foundation_kit/contrib/di/__init__.py +21 -0
  21. sqlalchemy_foundation_kit/contrib/di/_base.py +27 -0
  22. sqlalchemy_foundation_kit/contrib/di/_deps.py +32 -0
  23. sqlalchemy_foundation_kit/contrib/di/database.py +169 -0
  24. sqlalchemy_foundation_kit/contrib/di/metrics.py +71 -0
  25. sqlalchemy_foundation_kit/contrib/metrics/__init__.py +7 -0
  26. sqlalchemy_foundation_kit/contrib/metrics/postgres.py +149 -0
  27. sqlalchemy_foundation_kit/contrib/settings/__init__.py +19 -0
  28. sqlalchemy_foundation_kit/contrib/settings/postgres.py +183 -0
  29. sqlalchemy_foundation_kit/contrib/telemetry/__init__.py +17 -0
  30. sqlalchemy_foundation_kit/contrib/telemetry/instrumentations.py +101 -0
  31. sqlalchemy_foundation_kit/contrib/telemetry/uow.py +227 -0
  32. sqlalchemy_foundation_kit/protocols/__init__.py +21 -0
  33. sqlalchemy_foundation_kit/protocols/metrics.py +77 -0
  34. sqlalchemy_foundation_kit/py.typed +0 -0
  35. sqlalchemy_foundation_kit/session/__init__.py +27 -0
  36. sqlalchemy_foundation_kit/session/builder.py +293 -0
  37. sqlalchemy_foundation_kit/session/connection.py +33 -0
  38. sqlalchemy_foundation_kit/session/factories.py +104 -0
  39. sqlalchemy_foundation_kit/session/locks.py +68 -0
  40. sqlalchemy_foundation_kit/session/manager.py +252 -0
  41. sqlalchemy_foundation_kit/session/retry.py +82 -0
  42. sqlalchemy_foundation_kit/uow/__init__.py +21 -0
  43. sqlalchemy_foundation_kit/uow/enums.py +18 -0
  44. sqlalchemy_foundation_kit/uow/protocols.py +80 -0
  45. sqlalchemy_foundation_kit/uow/sqlalchemy.py +406 -0
  46. sqlalchemy_foundation_kit-0.0.0.dist-info/METADATA +624 -0
  47. sqlalchemy_foundation_kit-0.0.0.dist-info/RECORD +49 -0
  48. sqlalchemy_foundation_kit-0.0.0.dist-info/WHEEL +4 -0
  49. sqlalchemy_foundation_kit-0.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,85 @@
1
+ """Metrics containers for dependency-injector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+ from ...protocols import PostgresMetricsProtocol
8
+ from .._metrics_utils import _infra_metrics_prefix
9
+ from ._base import BaseDIContainer
10
+ from ._deps import providers
11
+
12
+
13
+ @runtime_checkable
14
+ class PrometheusMetricsSettingsProtocol(Protocol):
15
+ """Protocol for general prometheus metrics settings."""
16
+
17
+ @property
18
+ def prefix(self) -> str | None:
19
+ """Metric prefix."""
20
+ ...
21
+
22
+ @property
23
+ def enabled(self) -> bool:
24
+ """Whether metrics are enabled."""
25
+ ...
26
+
27
+
28
+ @runtime_checkable
29
+ class PostgresMetricsSettingsProtocol(Protocol):
30
+ """Protocol for Postgres settings that have metrics_enabled flag."""
31
+
32
+ @property
33
+ def metrics_enabled(self) -> bool:
34
+ """Whether infrastructure metrics are enabled."""
35
+ ...
36
+
37
+
38
+ try:
39
+ from ...contrib.metrics import PostgresMetrics
40
+
41
+ def _create_postgres_metrics(
42
+ metrics_settings: PrometheusMetricsSettingsProtocol,
43
+ default_prefix: str | None,
44
+ postgres_settings: PostgresMetricsSettingsProtocol | None,
45
+ ) -> PostgresMetricsProtocol | None:
46
+ """Create Postgres metrics if enabled."""
47
+ if postgres_settings is None or not postgres_settings.metrics_enabled:
48
+ return None
49
+ return PostgresMetrics(prefix=_infra_metrics_prefix(default_prefix))
50
+
51
+ class PrometheusMetricsContainer(BaseDIContainer):
52
+ """Container for Prometheus PostgreSQL metrics.
53
+
54
+ Provides:
55
+ - postgres_metrics: PostgreSQL metrics collector implementing PostgresMetricsProtocol.
56
+
57
+ Configuration:
58
+ - metrics_settings: General prometheus metrics settings (PrometheusMetricsSettingsProtocol).
59
+ - default_prefix: Default prefix for infrastructure metrics (str | None).
60
+ - postgres_settings: PostgreSQL settings with metrics_enabled flag (PostgresMetricsSettingsProtocol).
61
+ """
62
+
63
+ # Configuration
64
+ metrics_settings = providers.Dependency() # type: ignore[misc,var-annotated]
65
+ default_prefix = providers.Dependency() # type: ignore[misc,var-annotated]
66
+ postgres_settings = providers.Dependency(default=None) # type: ignore[misc,var-annotated]
67
+
68
+ # Postgres metrics
69
+ postgres_metrics = providers.Singleton( # type: ignore[misc,var-annotated]
70
+ _create_postgres_metrics,
71
+ metrics_settings=metrics_settings,
72
+ default_prefix=default_prefix,
73
+ postgres_settings=postgres_settings,
74
+ )
75
+
76
+ except ImportError: # pragma: no cover
77
+ PrometheusMetricsContainer = None # type: ignore[misc,assignment]
78
+ _create_postgres_metrics = None # type: ignore[misc,assignment]
79
+
80
+
81
+ __all__ = [
82
+ "PostgresMetricsSettingsProtocol",
83
+ "PrometheusMetricsContainer",
84
+ "PrometheusMetricsSettingsProtocol",
85
+ ]
@@ -0,0 +1,21 @@
1
+ """Dishka dependency injection providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._base import BaseDishkaProvider
6
+ from .database import (
7
+ AsyncDatabaseProvider,
8
+ AsyncUnitOfWorkProvider,
9
+ RetryConfig,
10
+ retry_async_connection,
11
+ )
12
+ from .metrics import PrometheusPostgresMetricsProvider
13
+
14
+ __all__ = [
15
+ "AsyncDatabaseProvider",
16
+ "AsyncUnitOfWorkProvider",
17
+ "BaseDishkaProvider",
18
+ "PrometheusPostgresMetricsProvider",
19
+ "RetryConfig",
20
+ "retry_async_connection",
21
+ ]
@@ -0,0 +1,27 @@
1
+ """Base Dishka provider with automatic dependency checking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._deps import Provider, check_dishka
6
+
7
+
8
+ class BaseDishkaProvider(Provider):
9
+ """Base provider that checks dishka availability on subclass creation.
10
+
11
+ All Dishka providers should inherit from this class instead of directly
12
+ from dishka.Provider. This ensures consistent error messages when dishka
13
+ is not installed.
14
+ """
15
+
16
+ def __init_subclass__(cls, **kwargs: object) -> None:
17
+ """Check dishka availability when creating a subclass."""
18
+ super().__init_subclass__(**kwargs)
19
+ check_dishka()
20
+
21
+ def __init__(self) -> None:
22
+ """Check dishka availability when instantiating."""
23
+ check_dishka()
24
+ super().__init__()
25
+
26
+
27
+ __all__ = ["BaseDishkaProvider"]
@@ -0,0 +1,32 @@
1
+ """Shared dishka dependency helpers.
2
+
3
+ Centralizes the dishka import boilerplate and availability check so each
4
+ provider module doesn't have to repeat it.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ try:
10
+ from dishka import Provider, Scope, provide
11
+
12
+ HAS_DISHKA = True
13
+ except ImportError:
14
+ HAS_DISHKA = False
15
+ Provider = object # type: ignore[misc,assignment]
16
+ Scope = None # type: ignore[misc,assignment]
17
+ provide = None # type: ignore[misc,assignment]
18
+
19
+
20
+ def check_dishka() -> None:
21
+ """Raise ImportError if dishka is not installed.
22
+
23
+ Raises:
24
+ ImportError: If dishka is not available.
25
+ """
26
+ if not HAS_DISHKA:
27
+ raise ImportError(
28
+ "dishka is required for providers. Install it with: pip install 'sqlalchemy-foundation-kit[dishka]'"
29
+ )
30
+
31
+
32
+ __all__ = ["HAS_DISHKA", "Provider", "Scope", "check_dishka", "provide"]
@@ -0,0 +1,169 @@
1
+ """Database providers for dishka."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import AsyncIterator
7
+
8
+ from sqlalchemy import text
9
+ from sqlalchemy.exc import SQLAlchemyError
10
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
11
+
12
+ from ...config import PostgresSettingsProtocol
13
+ from ...protocols import PostgresMetricsProtocol
14
+ from ...session import (
15
+ DEFAULT_HEALTHCHECK_QUERY,
16
+ DEFAULT_RETRY_CONFIG,
17
+ AsyncSessionManager,
18
+ RetryConfig,
19
+ create_async_session_manager,
20
+ retry_async_connection,
21
+ )
22
+ from ...uow import (
23
+ AsyncSQLAlchemyUnitOfWork,
24
+ AsyncSQLAlchemyUowTransaction,
25
+ AsyncUnitOfWork,
26
+ )
27
+ from ._base import BaseDishkaProvider
28
+ from ._deps import Scope, provide
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class AsyncDatabaseProvider(BaseDishkaProvider):
34
+ """Provider for database dependencies.
35
+
36
+ Provides:
37
+ - ``AsyncSessionManager``: Manages database connections and engine lifecycle.
38
+ - ``async_sessionmaker[AsyncSession]``: Factory for creating database sessions.
39
+
40
+ Note: ``PostgresMetricsProtocol`` must be provided by a separate provider
41
+ (e.g., :class:`PrometheusPostgresMetricsProvider`) or registered in the container.
42
+ If you don't want metrics, simply don't register such a provider.
43
+
44
+ Customization:
45
+ - Pass ``healthcheck_query=None`` to skip the startup connectivity check.
46
+ - Pass a custom :class:`RetryConfig` to tune startup retry behaviour.
47
+ - Subclass and override :meth:`create_session_manager` to fully customize
48
+ how the manager is constructed (e.g., to pass ``extra_server_settings``,
49
+ ``connection_class``, or ``on_engine_created``).
50
+ """
51
+
52
+ scope = Scope.APP
53
+
54
+ def __init__(
55
+ self,
56
+ healthcheck_query: str | None = DEFAULT_HEALTHCHECK_QUERY,
57
+ retry_config: RetryConfig = DEFAULT_RETRY_CONFIG,
58
+ ) -> None:
59
+ """Initialize provider.
60
+
61
+ Args:
62
+ healthcheck_query: SQL executed at startup to verify connectivity.
63
+ Pass ``None`` to skip the healthcheck entirely.
64
+ retry_config: Retry behavior for the startup healthcheck.
65
+ """
66
+ super().__init__()
67
+ self._healthcheck_query = healthcheck_query
68
+ self._retry_config = retry_config
69
+
70
+ def create_session_manager(
71
+ self,
72
+ postgres_config: PostgresSettingsProtocol,
73
+ metrics: PostgresMetricsProtocol | None,
74
+ ) -> AsyncSessionManager[AsyncSession]:
75
+ """Build an ``AsyncSessionManager`` for the given config.
76
+
77
+ Override this hook to customize session manager construction — for example,
78
+ to pass ``extra_server_settings``, a custom ``connection_class``, or an
79
+ ``on_engine_created`` callback for OpenTelemetry instrumentation.
80
+
81
+ Args:
82
+ postgres_config: PostgreSQL configuration.
83
+ metrics: Optional metrics collector.
84
+
85
+ Returns:
86
+ Configured ``AsyncSessionManager``.
87
+ """
88
+ return create_async_session_manager(postgres_config, metrics=metrics)
89
+
90
+ @provide
91
+ async def get_session_manager(
92
+ self,
93
+ postgres_config: PostgresSettingsProtocol,
94
+ metrics: PostgresMetricsProtocol | None = None,
95
+ ) -> AsyncIterator[AsyncSessionManager[AsyncSession]]:
96
+ """Provide database session manager."""
97
+ manager = self.create_session_manager(postgres_config, metrics)
98
+
99
+ if self._healthcheck_query is not None:
100
+ query = self._healthcheck_query
101
+
102
+ async def test_connection() -> None:
103
+ async with manager.session_maker() as session:
104
+ await session.execute(text(query))
105
+
106
+ await retry_async_connection(
107
+ connect_func=test_connection,
108
+ service_name="PostgreSQL",
109
+ config=self._retry_config,
110
+ )
111
+
112
+ try:
113
+ yield manager
114
+ finally:
115
+ try:
116
+ await manager.aclose()
117
+ logger.info("Database session manager closed successfully")
118
+ except SQLAlchemyError as e:
119
+ logger.warning("Error closing database session manager: %s", e)
120
+
121
+ @provide
122
+ def get_session_maker(self, session_manager: AsyncSessionManager[AsyncSession]) -> async_sessionmaker[AsyncSession]:
123
+ """Provide session maker."""
124
+ return session_manager.session_maker # type: ignore[no-any-return]
125
+
126
+
127
+ class AsyncUnitOfWorkProvider(BaseDishkaProvider):
128
+ """Provider for Unit of Work.
129
+
130
+ Provides:
131
+ - ``AsyncUnitOfWork``: Standardized interface for database transactions.
132
+
133
+ Note: UoW is APP-scoped because it's stateless — it only holds a reference to
134
+ ``session_maker`` (factory). Real database connections are created only when calling
135
+ ``uow.transaction()``, and are properly closed after the context manager exits.
136
+
137
+ Customization:
138
+ Override :meth:`create_uow` to use a custom transaction class
139
+ (e.g., one that exposes domain repositories as lazy properties).
140
+ """
141
+
142
+ scope = Scope.APP
143
+
144
+ def create_uow(
145
+ self,
146
+ session_maker: async_sessionmaker[AsyncSession],
147
+ ) -> AsyncUnitOfWork[AsyncSQLAlchemyUowTransaction]:
148
+ """Construct the UoW instance. Override to inject a custom transaction class."""
149
+ return AsyncSQLAlchemyUnitOfWork(session_maker, transaction_factory=AsyncSQLAlchemyUowTransaction)
150
+
151
+ @provide
152
+ def get_uow(
153
+ self,
154
+ session_maker: async_sessionmaker[AsyncSession],
155
+ ) -> AsyncUnitOfWork[AsyncSQLAlchemyUowTransaction]:
156
+ """Provide Unit of Work.
157
+
158
+ Returns a stateless UoW instance that can be safely reused.
159
+ Each call to ``uow.transaction()`` creates a new database session/connection.
160
+ """
161
+ return self.create_uow(session_maker)
162
+
163
+
164
+ __all__ = [
165
+ "AsyncDatabaseProvider",
166
+ "AsyncUnitOfWorkProvider",
167
+ "RetryConfig",
168
+ "retry_async_connection",
169
+ ]
@@ -0,0 +1,71 @@
1
+ """Metrics providers for dishka."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+ from ...protocols import PostgresMetricsProtocol
8
+ from .._metrics_utils import _infra_metrics_prefix
9
+ from ._base import BaseDishkaProvider
10
+ from ._deps import Scope, provide
11
+
12
+
13
+ @runtime_checkable
14
+ class PrometheusMetricsSettingsProtocol(Protocol):
15
+ """Protocol for general prometheus metrics settings."""
16
+
17
+ @property
18
+ def prefix(self) -> str | None:
19
+ """Metric prefix."""
20
+ ...
21
+
22
+ @property
23
+ def enabled(self) -> bool:
24
+ """Whether metrics are enabled."""
25
+ ...
26
+
27
+
28
+ @runtime_checkable
29
+ class PostgresMetricsSettingsProtocol(Protocol):
30
+ """Protocol for Postgres settings that have metrics_enabled flag."""
31
+
32
+ @property
33
+ def metrics_enabled(self) -> bool:
34
+ """Whether infrastructure metrics are enabled."""
35
+ ...
36
+
37
+
38
+ class BaseMetricsProvider(BaseDishkaProvider):
39
+ """Base provider for metrics with helper for optional infra name prefix."""
40
+
41
+ scope = Scope.APP
42
+
43
+
44
+ try:
45
+ from ...contrib.metrics import PostgresMetrics
46
+
47
+ class PrometheusPostgresMetricsProvider(BaseMetricsProvider):
48
+ """Provider for Prometheus PostgreSQL metrics."""
49
+
50
+ @provide
51
+ def get_metrics(
52
+ self,
53
+ metrics: PrometheusMetricsSettingsProtocol,
54
+ default_prefix: str | None,
55
+ postgres: PostgresMetricsSettingsProtocol | None = None,
56
+ ) -> PostgresMetricsProtocol | None:
57
+ """Provide Postgres metrics implementing PostgresMetricsProtocol."""
58
+ if postgres is None or not postgres.metrics_enabled:
59
+ return None
60
+ return PostgresMetrics(prefix=_infra_metrics_prefix(default_prefix))
61
+
62
+ except ImportError: # pragma: no cover
63
+ PrometheusPostgresMetricsProvider = None # type: ignore[misc,assignment]
64
+
65
+
66
+ __all__ = [
67
+ "BaseMetricsProvider",
68
+ "PostgresMetricsSettingsProtocol",
69
+ "PrometheusMetricsSettingsProtocol",
70
+ "PrometheusPostgresMetricsProvider",
71
+ ]
@@ -0,0 +1,7 @@
1
+ """Prometheus metrics integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .postgres import PostgresMetrics
6
+
7
+ __all__ = ["PostgresMetrics"]
@@ -0,0 +1,149 @@
1
+ """Postgres metrics using prometheus-client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ try:
8
+ from prometheus_client import Counter, Gauge, Histogram
9
+
10
+ HAS_PROMETHEUS = True
11
+ except ImportError:
12
+ HAS_PROMETHEUS = False
13
+
14
+ # Default buckets for connection checkout duration (seconds)
15
+ CONNECTION_CHECKOUT_BUCKETS: tuple[float, ...] = (
16
+ 0.001,
17
+ 0.005,
18
+ 0.01,
19
+ 0.025,
20
+ 0.05,
21
+ 0.1,
22
+ 0.25,
23
+ 0.5,
24
+ 1.0,
25
+ 2.5,
26
+ 5.0,
27
+ )
28
+
29
+
30
+ def _check_prometheus() -> None:
31
+ """Check if prometheus-client is installed."""
32
+ if not HAS_PROMETHEUS:
33
+ raise ImportError(
34
+ "prometheus-client is required for PostgresMetrics. "
35
+ "Install it with: pip install 'sqlalchemy-foundation-kit[metrics]'"
36
+ )
37
+
38
+
39
+ class PostgresMetrics:
40
+ """Postgres connection pool metrics.
41
+
42
+ Metrics:
43
+ - postgres_db_pool_size: Current database connection pool size.
44
+ - postgres_db_pool_checked_out: Number of connections currently checked out.
45
+ - postgres_db_pool_overflow: Number of connections over pool_size (within max_overflow).
46
+ - postgres_db_connection_checkout_duration_seconds: Time to acquire connection from pool.
47
+ - postgres_db_connection_timeouts_total: Number of connection checkout timeouts.
48
+ - postgres_db_connection_errors_total: Number of connection errors.
49
+
50
+ Labels:
51
+ - error_type: Type of connection error (for errors_total).
52
+ """
53
+
54
+ def __init__(self, prefix: str | None = None) -> None:
55
+ """Initialize postgres metrics.
56
+
57
+ Args:
58
+ prefix: Metric name prefix.
59
+
60
+ Raises:
61
+ ImportError: If prometheus-client is not installed.
62
+ """
63
+ _check_prometheus()
64
+
65
+ self.pool_size = Gauge(
66
+ _make_metric_name("postgres_db_pool_size", prefix),
67
+ "Current database connection pool size",
68
+ )
69
+ self.pool_checked_out = Gauge(
70
+ _make_metric_name("postgres_db_pool_checked_out", prefix),
71
+ "Number of database connections currently checked out",
72
+ )
73
+ self.pool_overflow = Gauge(
74
+ _make_metric_name("postgres_db_pool_overflow", prefix),
75
+ "Number of connections over pool_size (within max_overflow)",
76
+ )
77
+ self.connection_checkout_duration = Histogram(
78
+ _make_metric_name("postgres_db_connection_checkout_duration_seconds", prefix),
79
+ "Time to acquire connection from pool",
80
+ buckets=list(CONNECTION_CHECKOUT_BUCKETS),
81
+ )
82
+ self.connection_timeouts_total = Counter(
83
+ _make_metric_name("postgres_db_connection_timeouts_total", prefix),
84
+ "Number of connection checkout timeouts",
85
+ )
86
+ self.connection_errors_total = Counter(
87
+ _make_metric_name("postgres_db_connection_errors_total", prefix),
88
+ "Number of connection errors",
89
+ ["error_type"],
90
+ )
91
+
92
+ def record_pool_stats(
93
+ self,
94
+ pool_size: int,
95
+ pool_checked_out: int,
96
+ pool_overflow: int,
97
+ ) -> None:
98
+ """Record database connection pool statistics."""
99
+ self.pool_size.set(float(pool_size))
100
+ self.pool_checked_out.set(float(pool_checked_out))
101
+ self.pool_overflow.set(float(pool_overflow))
102
+
103
+ def record_checkout(
104
+ self,
105
+ duration: float,
106
+ ) -> None:
107
+ """Record a database connection checkout from the pool."""
108
+ self.connection_checkout_duration.observe(duration)
109
+
110
+ def record_error(
111
+ self,
112
+ error_type: str,
113
+ is_timeout: bool = False,
114
+ ) -> None:
115
+ """Record a database connection or execution error."""
116
+ self.connection_errors_total.labels(error_type=error_type).inc()
117
+ if is_timeout:
118
+ self.connection_timeouts_total.inc()
119
+
120
+
121
+ _PREFIX_PATTERN: re.Pattern[str] = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
122
+
123
+
124
+ def _make_metric_name(name: str, prefix: str | None = None) -> str:
125
+ """Make metric name with optional prefix.
126
+
127
+ Prefix must follow Prometheus naming conventions (^[a-zA-Z_][a-zA-Z0-9_]*$).
128
+
129
+ Args:
130
+ name: Base metric name.
131
+ prefix: Optional prefix for the metric.
132
+
133
+ Returns:
134
+ The combined metric name.
135
+
136
+ Raises:
137
+ ValueError: If the prefix is invalid.
138
+ """
139
+ if prefix:
140
+ if not _PREFIX_PATTERN.match(prefix):
141
+ raise ValueError(
142
+ f"Invalid metric prefix: '{prefix}'. "
143
+ "Prefixes must start with a letter or underscore and contain only letters, numbers, or underscores."
144
+ )
145
+ return f"{prefix}_{name}"
146
+ return name
147
+
148
+
149
+ __all__ = ["PostgresMetrics"]
@@ -0,0 +1,19 @@
1
+ """Pydantic Settings integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .postgres import (
6
+ BasePostgresConfig,
7
+ BasePostgresMigrationsConfig,
8
+ ConnectionSettings,
9
+ PoolSettings,
10
+ QuerySettings,
11
+ )
12
+
13
+ __all__ = [
14
+ "BasePostgresConfig",
15
+ "BasePostgresMigrationsConfig",
16
+ "ConnectionSettings",
17
+ "PoolSettings",
18
+ "QuerySettings",
19
+ ]