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,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
+ )