sqlalchemy-foundation-kit 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlalchemy_foundation_kit/__init__.py +119 -0
- sqlalchemy_foundation_kit/__version__.py +1 -0
- sqlalchemy_foundation_kit/_typing.py +28 -0
- sqlalchemy_foundation_kit/base/__init__.py +46 -0
- sqlalchemy_foundation_kit/base/_optional.py +37 -0
- sqlalchemy_foundation_kit/base/engine.py +256 -0
- sqlalchemy_foundation_kit/base/metadata.py +57 -0
- sqlalchemy_foundation_kit/base/models.py +101 -0
- sqlalchemy_foundation_kit/base/serialization.py +98 -0
- sqlalchemy_foundation_kit/base/types.py +72 -0
- sqlalchemy_foundation_kit/config/__init__.py +17 -0
- sqlalchemy_foundation_kit/config/postgres.py +177 -0
- sqlalchemy_foundation_kit/contrib/__init__.py +5 -0
- sqlalchemy_foundation_kit/contrib/_metrics_utils.py +19 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/__init__.py +27 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/_base.py +27 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/_deps.py +37 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/database.py +196 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/metrics.py +85 -0
- sqlalchemy_foundation_kit/contrib/di/__init__.py +21 -0
- sqlalchemy_foundation_kit/contrib/di/_base.py +27 -0
- sqlalchemy_foundation_kit/contrib/di/_deps.py +32 -0
- sqlalchemy_foundation_kit/contrib/di/database.py +169 -0
- sqlalchemy_foundation_kit/contrib/di/metrics.py +71 -0
- sqlalchemy_foundation_kit/contrib/metrics/__init__.py +7 -0
- sqlalchemy_foundation_kit/contrib/metrics/postgres.py +149 -0
- sqlalchemy_foundation_kit/contrib/settings/__init__.py +19 -0
- sqlalchemy_foundation_kit/contrib/settings/postgres.py +183 -0
- sqlalchemy_foundation_kit/contrib/telemetry/__init__.py +17 -0
- sqlalchemy_foundation_kit/contrib/telemetry/instrumentations.py +101 -0
- sqlalchemy_foundation_kit/contrib/telemetry/uow.py +227 -0
- sqlalchemy_foundation_kit/protocols/__init__.py +21 -0
- sqlalchemy_foundation_kit/protocols/metrics.py +77 -0
- sqlalchemy_foundation_kit/py.typed +0 -0
- sqlalchemy_foundation_kit/session/__init__.py +27 -0
- sqlalchemy_foundation_kit/session/builder.py +293 -0
- sqlalchemy_foundation_kit/session/connection.py +33 -0
- sqlalchemy_foundation_kit/session/factories.py +104 -0
- sqlalchemy_foundation_kit/session/locks.py +68 -0
- sqlalchemy_foundation_kit/session/manager.py +252 -0
- sqlalchemy_foundation_kit/session/retry.py +82 -0
- sqlalchemy_foundation_kit/uow/__init__.py +21 -0
- sqlalchemy_foundation_kit/uow/enums.py +18 -0
- sqlalchemy_foundation_kit/uow/protocols.py +80 -0
- sqlalchemy_foundation_kit/uow/sqlalchemy.py +406 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/METADATA +624 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/RECORD +49 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/WHEEL +4 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Foundation layer for SQLAlchemy-based services with UoW, session management, and observability."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
# Base ORM
|
|
6
|
+
from .base import (
|
|
7
|
+
DB_NAMING_CONVENTION,
|
|
8
|
+
Base,
|
|
9
|
+
BaseTable,
|
|
10
|
+
DatetimeColumnsMixin,
|
|
11
|
+
GenericJSONDict,
|
|
12
|
+
PoolClassStr,
|
|
13
|
+
PoolRegistry,
|
|
14
|
+
PydanticJSONB,
|
|
15
|
+
UnConstrainedEnum,
|
|
16
|
+
build_engine_kwargs,
|
|
17
|
+
configure_orjson_serialization,
|
|
18
|
+
load_orm_metadata,
|
|
19
|
+
register_pool_class,
|
|
20
|
+
resolve_pool_class,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Config
|
|
24
|
+
from .config import (
|
|
25
|
+
ConnectionSettingsProtocol,
|
|
26
|
+
PoolSettingsProtocol,
|
|
27
|
+
PostgresSettingsProtocol,
|
|
28
|
+
QuerySettingsProtocol,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Protocols
|
|
32
|
+
from .protocols import (
|
|
33
|
+
CheckoutRecorder,
|
|
34
|
+
ErrorRecorder,
|
|
35
|
+
PoolStatsRecorder,
|
|
36
|
+
PostgresMetricsProtocol,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Session Management
|
|
40
|
+
from .session import (
|
|
41
|
+
DEFAULT_HEALTHCHECK_QUERY,
|
|
42
|
+
DEFAULT_RETRY_CONFIG,
|
|
43
|
+
AsyncCConnection,
|
|
44
|
+
AsyncSessionManager,
|
|
45
|
+
AsyncSessionManagerBuilder,
|
|
46
|
+
RetryConfig,
|
|
47
|
+
create_async_session_manager,
|
|
48
|
+
retry_async_connection,
|
|
49
|
+
try_advisory_xact_lock,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Unit of Work
|
|
53
|
+
from .uow import (
|
|
54
|
+
AsyncSQLAlchemyUnitOfWork,
|
|
55
|
+
AsyncSQLAlchemyUowTransaction,
|
|
56
|
+
AsyncUnitOfWork,
|
|
57
|
+
AsyncUowTransaction,
|
|
58
|
+
IsolationLevel,
|
|
59
|
+
PostgresAdvisoryLockMixin,
|
|
60
|
+
SupportsAdvisoryLock,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
__version__ = version("sqlalchemy-foundation-kit")
|
|
65
|
+
except PackageNotFoundError: # pragma: no cover
|
|
66
|
+
__version__ = "0.1.0"
|
|
67
|
+
|
|
68
|
+
__all__ = [ # noqa: RUF022
|
|
69
|
+
# Base ORM
|
|
70
|
+
"DB_NAMING_CONVENTION",
|
|
71
|
+
"Base",
|
|
72
|
+
"BaseTable",
|
|
73
|
+
"DatetimeColumnsMixin",
|
|
74
|
+
"GenericJSONDict",
|
|
75
|
+
"PoolClassStr",
|
|
76
|
+
"PoolRegistry",
|
|
77
|
+
"PydanticJSONB",
|
|
78
|
+
"UnConstrainedEnum",
|
|
79
|
+
"build_engine_kwargs",
|
|
80
|
+
"configure_orjson_serialization",
|
|
81
|
+
"load_orm_metadata",
|
|
82
|
+
"register_pool_class",
|
|
83
|
+
"resolve_pool_class",
|
|
84
|
+
# Config
|
|
85
|
+
"ConnectionSettingsProtocol",
|
|
86
|
+
"PoolSettingsProtocol",
|
|
87
|
+
"PostgresSettingsProtocol",
|
|
88
|
+
"QuerySettingsProtocol",
|
|
89
|
+
# Protocols
|
|
90
|
+
"CheckoutRecorder",
|
|
91
|
+
"ErrorRecorder",
|
|
92
|
+
"PoolStatsRecorder",
|
|
93
|
+
"PostgresMetricsProtocol",
|
|
94
|
+
# Session Management
|
|
95
|
+
"DEFAULT_HEALTHCHECK_QUERY",
|
|
96
|
+
"DEFAULT_RETRY_CONFIG",
|
|
97
|
+
"AsyncCConnection",
|
|
98
|
+
"AsyncSessionManager",
|
|
99
|
+
"AsyncSessionManagerBuilder",
|
|
100
|
+
"RetryConfig",
|
|
101
|
+
"create_async_session_manager",
|
|
102
|
+
"retry_async_connection",
|
|
103
|
+
"try_advisory_xact_lock",
|
|
104
|
+
# Unit of Work
|
|
105
|
+
"AsyncSQLAlchemyUnitOfWork",
|
|
106
|
+
"AsyncSQLAlchemyUowTransaction",
|
|
107
|
+
"AsyncUnitOfWork",
|
|
108
|
+
"AsyncUowTransaction",
|
|
109
|
+
"IsolationLevel",
|
|
110
|
+
"PostgresAdvisoryLockMixin",
|
|
111
|
+
"SupportsAdvisoryLock",
|
|
112
|
+
# Version
|
|
113
|
+
"__version__",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
# Note: contrib modules are available via:
|
|
117
|
+
# - from sqlalchemy_foundation_kit.contrib.settings import BasePostgresConfig
|
|
118
|
+
# - from sqlalchemy_foundation_kit.contrib.metrics import PostgresMetrics
|
|
119
|
+
# - from sqlalchemy_foundation_kit.contrib.di import AsyncDatabaseProvider, AsyncUnitOfWorkProvider
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Shared type variables for the library.
|
|
2
|
+
|
|
3
|
+
Centralized location for all TypeVars to avoid duplication and ensure consistency.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
from .uow.protocols import AsyncUowTransaction
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"SessionT",
|
|
17
|
+
"T",
|
|
18
|
+
"T_co",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# Covariant TypeVar for UoW protocols (used in Protocol definitions)
|
|
22
|
+
T_co = TypeVar("T_co", bound="AsyncUowTransaction", covariant=True)
|
|
23
|
+
|
|
24
|
+
# Invariant TypeVar for UoW implementations
|
|
25
|
+
T = TypeVar("T", bound="AsyncUowTransaction")
|
|
26
|
+
|
|
27
|
+
# Session TypeVar for AsyncSessionManager and AsyncSessionManagerBuilder
|
|
28
|
+
SessionT = TypeVar("SessionT", bound="AsyncSession")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Base ORM models and utilities.
|
|
2
|
+
|
|
3
|
+
Public API for base functionality. Import directly from submodules for clarity:
|
|
4
|
+
- base.engine - Engine configuration (build_engine_kwargs, resolve_pool_class)
|
|
5
|
+
- base.serialization - JSON serialization (configure_orjson_serialization)
|
|
6
|
+
- base.metadata - Metadata loading (load_orm_metadata)
|
|
7
|
+
- base.models - ORM base classes (Base, BaseTable, mixins)
|
|
8
|
+
- base.types - Custom SQLAlchemy types (PydanticJSONB)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from .engine import (
|
|
14
|
+
PoolClassStr,
|
|
15
|
+
PoolRegistry,
|
|
16
|
+
build_engine_kwargs,
|
|
17
|
+
register_pool_class,
|
|
18
|
+
resolve_pool_class,
|
|
19
|
+
)
|
|
20
|
+
from .metadata import load_orm_metadata
|
|
21
|
+
from .models import (
|
|
22
|
+
DB_NAMING_CONVENTION,
|
|
23
|
+
Base,
|
|
24
|
+
BaseTable,
|
|
25
|
+
DatetimeColumnsMixin,
|
|
26
|
+
UnConstrainedEnum,
|
|
27
|
+
)
|
|
28
|
+
from .serialization import configure_orjson_serialization
|
|
29
|
+
from .types import GenericJSONDict, PydanticJSONB
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"DB_NAMING_CONVENTION",
|
|
33
|
+
"Base",
|
|
34
|
+
"BaseTable",
|
|
35
|
+
"DatetimeColumnsMixin",
|
|
36
|
+
"GenericJSONDict",
|
|
37
|
+
"PoolClassStr",
|
|
38
|
+
"PoolRegistry",
|
|
39
|
+
"PydanticJSONB",
|
|
40
|
+
"UnConstrainedEnum",
|
|
41
|
+
"build_engine_kwargs",
|
|
42
|
+
"configure_orjson_serialization",
|
|
43
|
+
"load_orm_metadata",
|
|
44
|
+
"register_pool_class",
|
|
45
|
+
"resolve_pool_class",
|
|
46
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Utilities for handling optional dependencies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import types
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def require_optional(module_name: str, extra_name: str) -> types.ModuleType:
|
|
10
|
+
"""Import an optional dependency or raise a helpful error.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
module_name: Name of the module to import (e.g., "orjson", "opentelemetry").
|
|
14
|
+
extra_name: Name of the pip extra that provides this dependency (e.g., "json", "telemetry").
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
The imported module.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ImportError: If the module is not installed, with installation instructions.
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
>>> orjson = require_optional("orjson", "json")
|
|
24
|
+
>>> from opentelemetry import trace
|
|
25
|
+
# or
|
|
26
|
+
>>> otel = require_optional("opentelemetry", "telemetry")
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
return importlib.import_module(module_name)
|
|
30
|
+
except ImportError as e:
|
|
31
|
+
raise ImportError(
|
|
32
|
+
f"{module_name} is required for this functionality. "
|
|
33
|
+
f"Install it with: pip install 'sqlalchemy-foundation-kit[{extra_name}]'"
|
|
34
|
+
) from e
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = ["require_optional"]
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""SQLAlchemy engine configuration utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, Literal
|
|
6
|
+
|
|
7
|
+
from sqlalchemy.pool import (
|
|
8
|
+
AsyncAdaptedQueuePool,
|
|
9
|
+
FallbackAsyncAdaptedQueuePool,
|
|
10
|
+
NullPool,
|
|
11
|
+
QueuePool,
|
|
12
|
+
SingletonThreadPool,
|
|
13
|
+
StaticPool,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..config import PoolSettingsProtocol
|
|
18
|
+
|
|
19
|
+
PoolClassStr = Literal[
|
|
20
|
+
"null",
|
|
21
|
+
"queue",
|
|
22
|
+
"singleton_thread",
|
|
23
|
+
"async_adapted_queue",
|
|
24
|
+
"fallback_async_adapted_queue",
|
|
25
|
+
"static",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PoolRegistry:
|
|
30
|
+
"""Registry for SQLAlchemy pool classes.
|
|
31
|
+
|
|
32
|
+
Provides a centralized registry for pool classes that follows the Open/Closed principle:
|
|
33
|
+
- Open for extension: custom pools can be registered via :meth:`register`
|
|
34
|
+
- Closed for modification: built-in pools are immutable
|
|
35
|
+
|
|
36
|
+
This design allows library users to register custom pool implementations without
|
|
37
|
+
modifying library code.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
Register a custom pool class:
|
|
41
|
+
>>> class MyCustomPool(QueuePool):
|
|
42
|
+
... pass
|
|
43
|
+
>>> PoolRegistry.register("custom", MyCustomPool)
|
|
44
|
+
>>> pool = PoolRegistry.resolve("custom")
|
|
45
|
+
|
|
46
|
+
Override built-in pool (not recommended, but possible):
|
|
47
|
+
>>> PoolRegistry.register("queue", MyCustomQueuePool, override=True)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
_pools: ClassVar[dict[str, type]] = {
|
|
51
|
+
"null": NullPool,
|
|
52
|
+
"queue": QueuePool,
|
|
53
|
+
"singleton_thread": SingletonThreadPool,
|
|
54
|
+
"async_adapted_queue": AsyncAdaptedQueuePool,
|
|
55
|
+
"fallback_async_adapted_queue": FallbackAsyncAdaptedQueuePool,
|
|
56
|
+
"static": StaticPool,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def register(cls, name: str, pool_class: type, *, override: bool = False) -> None:
|
|
61
|
+
"""Register a custom pool class.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
name: Pool class identifier (lowercase recommended).
|
|
65
|
+
pool_class: Pool class type to register.
|
|
66
|
+
override: If True, allows overriding built-in pools (use with caution).
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If name already exists and override=False.
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
>>> PoolRegistry.register("my_pool", MyCustomPool)
|
|
73
|
+
"""
|
|
74
|
+
if name in cls._pools and not override:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Pool class '{name}' is already registered. "
|
|
77
|
+
f"Use override=True to replace it (not recommended for built-ins)."
|
|
78
|
+
)
|
|
79
|
+
cls._pools[name] = pool_class
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def resolve(cls, name: str) -> type:
|
|
83
|
+
"""Resolve pool class by name.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
name: Pool class identifier.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Pool class type.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If pool class name is not registered.
|
|
93
|
+
|
|
94
|
+
Examples:
|
|
95
|
+
>>> pool = PoolRegistry.resolve("queue")
|
|
96
|
+
>>> pool
|
|
97
|
+
<class 'sqlalchemy.pool.QueuePool'>
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
return cls._pools[name.lower()]
|
|
101
|
+
except KeyError as e:
|
|
102
|
+
available = ", ".join(sorted(cls._pools.keys()))
|
|
103
|
+
raise ValueError(f"Unknown pool class: {name}. Available: {available}") from e
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def list_available(cls) -> list[str]:
|
|
107
|
+
"""List all registered pool class names.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Sorted list of registered pool names.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
>>> PoolRegistry.list_available()
|
|
114
|
+
['async_adapted_queue', 'fallback_async_adapted_queue', 'null', 'queue', ...]
|
|
115
|
+
"""
|
|
116
|
+
return sorted(cls._pools.keys())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def resolve_pool_class(poolclass: PoolClassStr | str | type) -> type:
|
|
120
|
+
"""Resolve pool class from string name or return the class directly.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
poolclass: Pool class name (e.g., "null", "queue") or actual class type.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Pool class type.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ValueError: If pool class name is not recognized.
|
|
130
|
+
"""
|
|
131
|
+
if isinstance(poolclass, str):
|
|
132
|
+
return PoolRegistry.resolve(poolclass)
|
|
133
|
+
|
|
134
|
+
return poolclass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def register_pool_class(name: str, pool_class: type, *, override: bool = False) -> None:
|
|
138
|
+
"""Register a custom pool class.
|
|
139
|
+
|
|
140
|
+
Convenience wrapper around :meth:`PoolRegistry.register` for users who prefer
|
|
141
|
+
a functional API over the class-based one.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
name: Pool class identifier (lowercase recommended).
|
|
145
|
+
pool_class: Pool class type to register.
|
|
146
|
+
override: If True, allows overriding built-in pools (use with caution).
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ValueError: If name already exists and override=False.
|
|
150
|
+
"""
|
|
151
|
+
PoolRegistry.register(name, pool_class, override=override)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_engine_kwargs(
|
|
155
|
+
echo: bool,
|
|
156
|
+
poolclass: type,
|
|
157
|
+
isolation_level: str | None,
|
|
158
|
+
pool_settings: PoolSettingsProtocol | None,
|
|
159
|
+
connect_args: dict[str, object] | None,
|
|
160
|
+
extra_kwargs: dict[str, object],
|
|
161
|
+
use_orjson: bool = False,
|
|
162
|
+
) -> dict[str, object]:
|
|
163
|
+
"""Build SQLAlchemy engine keyword arguments.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
echo: If True, SQLAlchemy will log all SQL statements.
|
|
167
|
+
poolclass: SQLAlchemy pool class.
|
|
168
|
+
isolation_level: Default transaction isolation level.
|
|
169
|
+
pool_settings: Pool configuration settings (validated by caller, e.g., Pydantic).
|
|
170
|
+
connect_args: Arguments passed to the database driver.
|
|
171
|
+
extra_kwargs: Additional keyword arguments for create_async_engine.
|
|
172
|
+
use_orjson: If True, use orjson for JSON serialization.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Dictionary of engine keyword arguments ready for create_async_engine().
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
ImportError: If use_orjson is True but orjson is not installed.
|
|
179
|
+
|
|
180
|
+
Examples:
|
|
181
|
+
>>> kwargs = build_engine_kwargs(
|
|
182
|
+
... echo=False,
|
|
183
|
+
... poolclass=NullPool,
|
|
184
|
+
... isolation_level=None,
|
|
185
|
+
... pool_settings=None,
|
|
186
|
+
... connect_args=None,
|
|
187
|
+
... extra_kwargs={},
|
|
188
|
+
... use_orjson=False,
|
|
189
|
+
... )
|
|
190
|
+
>>> kwargs["echo"]
|
|
191
|
+
False
|
|
192
|
+
"""
|
|
193
|
+
engine_kwargs: dict[str, object] = {
|
|
194
|
+
"echo": echo,
|
|
195
|
+
"poolclass": poolclass,
|
|
196
|
+
"isolation_level": isolation_level,
|
|
197
|
+
"pool_pre_ping": pool_settings.pre_ping if pool_settings else True,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if use_orjson:
|
|
201
|
+
from .serialization import configure_orjson_serialization # noqa: PLC0415
|
|
202
|
+
|
|
203
|
+
engine_kwargs.update(configure_orjson_serialization())
|
|
204
|
+
|
|
205
|
+
if pool_settings:
|
|
206
|
+
_apply_pool_settings(engine_kwargs, poolclass, pool_settings)
|
|
207
|
+
|
|
208
|
+
if connect_args:
|
|
209
|
+
engine_kwargs["connect_args"] = {k: v for k, v in connect_args.items() if v is not None}
|
|
210
|
+
|
|
211
|
+
if extra_kwargs:
|
|
212
|
+
engine_kwargs.update(extra_kwargs)
|
|
213
|
+
|
|
214
|
+
return engine_kwargs
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _apply_pool_settings(
|
|
218
|
+
engine_kwargs: dict[str, object],
|
|
219
|
+
poolclass: type,
|
|
220
|
+
pool_settings: PoolSettingsProtocol,
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Apply pool settings to engine kwargs.
|
|
223
|
+
|
|
224
|
+
Maps pool settings to SQLAlchemy's expected ``pool_*`` keyword arguments.
|
|
225
|
+
Note: pool_pre_ping is already set in build_engine_kwargs, so not applied here.
|
|
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.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
engine_kwargs: Dictionary to update with pool configuration.
|
|
233
|
+
poolclass: Pool class being configured.
|
|
234
|
+
pool_settings: Pool configuration settings.
|
|
235
|
+
"""
|
|
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
|
+
}
|
|
243
|
+
|
|
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
|
+
engine_kwargs.update({k: v for k, v in params.items() if v is not None})
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
__all__ = [
|
|
251
|
+
"PoolClassStr",
|
|
252
|
+
"PoolRegistry",
|
|
253
|
+
"build_engine_kwargs",
|
|
254
|
+
"register_pool_class",
|
|
255
|
+
"resolve_pool_class",
|
|
256
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""ORM metadata loading utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from importlib import import_module
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import MetaData
|
|
9
|
+
|
|
10
|
+
from .models import Base
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_orm_metadata(models_modules: Iterable[str], metadata: MetaData | None = None) -> MetaData:
|
|
14
|
+
"""Load all ORM models metadata synchronously.
|
|
15
|
+
|
|
16
|
+
Imports specified modules to ensure that all SQLAlchemy models are
|
|
17
|
+
registered in the metadata. This is useful for migrations and schema
|
|
18
|
+
introspection with tools like Alembic.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
models_modules: Iterable of module paths to import (e.g., ["myapp.models", "myapp.core.models"]).
|
|
22
|
+
metadata: Optional specific MetaData object to use. If None, uses Base.metadata.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
MetaData object containing all registered models from the imported modules.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
Load models from multiple modules:
|
|
29
|
+
>>> from sqlalchemy_foundation_kit.base import load_orm_metadata
|
|
30
|
+
>>> metadata = load_orm_metadata([
|
|
31
|
+
... "myapp.users.models",
|
|
32
|
+
... "myapp.orders.models",
|
|
33
|
+
... "myapp.products.models",
|
|
34
|
+
... ])
|
|
35
|
+
>>> len(metadata.tables)
|
|
36
|
+
15
|
|
37
|
+
|
|
38
|
+
Use with custom metadata:
|
|
39
|
+
>>> from sqlalchemy import MetaData
|
|
40
|
+
>>> custom_meta = MetaData(schema="public")
|
|
41
|
+
>>> metadata = load_orm_metadata(["myapp.models"], metadata=custom_meta)
|
|
42
|
+
|
|
43
|
+
Typical usage in Alembic env.py:
|
|
44
|
+
>>> from sqlalchemy_foundation_kit.base import Base, load_orm_metadata
|
|
45
|
+
>>> target_metadata = Base.metadata
|
|
46
|
+
>>> load_orm_metadata(["myapp.models"]) # Register all models
|
|
47
|
+
>>> # Now target_metadata.tables contains all tables
|
|
48
|
+
"""
|
|
49
|
+
for module in models_modules:
|
|
50
|
+
import_module(module)
|
|
51
|
+
|
|
52
|
+
return metadata if metadata is not None else Base.metadata
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"load_orm_metadata",
|
|
57
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Database base models and mixins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import enum
|
|
7
|
+
import uuid
|
|
8
|
+
from functools import partial
|
|
9
|
+
from typing import Any, ClassVar
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import TIMESTAMP, Enum, MetaData, func
|
|
12
|
+
from sqlalchemy.dialects import postgresql
|
|
13
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column
|
|
14
|
+
from sqlalchemy.types import TypeEngine
|
|
15
|
+
|
|
16
|
+
DB_NAMING_CONVENTION: dict[str, str] = {
|
|
17
|
+
"ix": "%(column_0_label)s_idx",
|
|
18
|
+
"uq": "%(table_name)s_%(column_0_name)s_key",
|
|
19
|
+
"ck": "%(table_name)s_%(constraint_name)s_check",
|
|
20
|
+
"fk": "%(table_name)s_%(column_0_name)s_fkey",
|
|
21
|
+
"pk": "%(table_name)s_pkey",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Base(DeclarativeBase):
|
|
26
|
+
"""Base class for all ORM models."""
|
|
27
|
+
|
|
28
|
+
type_annotation_map: ClassVar[dict[type, TypeEngine[Any]]] = {
|
|
29
|
+
uuid.UUID: postgresql.UUID(),
|
|
30
|
+
datetime.datetime: TIMESTAMP(timezone=True),
|
|
31
|
+
}
|
|
32
|
+
metadata = MetaData(naming_convention=DB_NAMING_CONVENTION)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BaseTable(Base):
|
|
36
|
+
"""Base table class with __repr__."""
|
|
37
|
+
|
|
38
|
+
__abstract__ = True
|
|
39
|
+
|
|
40
|
+
def __repr__(self) -> str:
|
|
41
|
+
columns = {column.name: getattr(self, column.name) for column in self.__table__.columns}
|
|
42
|
+
return f"<{self.__tablename__}: {', '.join(f'{k}={v}' for k, v in columns.items())}>"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DatetimeColumnsMixin:
|
|
46
|
+
"""Mixin for tables that need created_at and updated_at timestamps.
|
|
47
|
+
|
|
48
|
+
Control indexing via __created_at_index__ and __updated_at_index__ class variables in the model.
|
|
49
|
+
Defaults to False.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
__created_at_index__: ClassVar[bool] = False
|
|
53
|
+
__updated_at_index__: ClassVar[bool] = False
|
|
54
|
+
|
|
55
|
+
@declared_attr
|
|
56
|
+
def created_at(self) -> Mapped[datetime.datetime]:
|
|
57
|
+
return mapped_column(
|
|
58
|
+
server_default=func.timezone("UTC", func.now()),
|
|
59
|
+
index=self.__created_at_index__,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@declared_attr
|
|
63
|
+
def updated_at(self) -> Mapped[datetime.datetime]:
|
|
64
|
+
return mapped_column(
|
|
65
|
+
server_default=func.timezone("UTC", func.now()),
|
|
66
|
+
onupdate=func.timezone("UTC", func.now()),
|
|
67
|
+
index=self.__updated_at_index__,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _extract_enum_values(enum_obj: type[enum.Enum] | list[object]) -> list[object]:
|
|
72
|
+
"""Extract values from Python enum or return the list as-is.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
enum_obj: Either a Python Enum class with __members__ or a list of values.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of enum values (extracting .value attribute if available) or the input list.
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
>>> from enum import Enum
|
|
82
|
+
>>> class Color(Enum):
|
|
83
|
+
... RED = "red"
|
|
84
|
+
... BLUE = "blue"
|
|
85
|
+
>>> _extract_enum_values(Color)
|
|
86
|
+
["red", "blue"]
|
|
87
|
+
>>> _extract_enum_values(["red", "blue"])
|
|
88
|
+
["red", "blue"]
|
|
89
|
+
"""
|
|
90
|
+
if hasattr(enum_obj, "__members__"):
|
|
91
|
+
return [getattr(item, "value", item) for item in enum_obj]
|
|
92
|
+
return list(enum_obj) # type: ignore[arg-type]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
UnConstrainedEnum = partial(
|
|
96
|
+
Enum,
|
|
97
|
+
native_enum=False,
|
|
98
|
+
create_constraint=False,
|
|
99
|
+
validate_strings=True,
|
|
100
|
+
values_callable=_extract_enum_values,
|
|
101
|
+
)
|