spakky-sqlalchemy 5.0.1__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 (20) hide show
  1. spakky_sqlalchemy-5.0.1/PKG-INFO +255 -0
  2. spakky_sqlalchemy-5.0.1/README.md +243 -0
  3. spakky_sqlalchemy-5.0.1/pyproject.toml +80 -0
  4. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/__init__.py +4 -0
  5. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/common/__init__.py +0 -0
  6. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/common/config.py +71 -0
  7. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/common/constants.py +1 -0
  8. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/error.py +7 -0
  9. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/main.py +41 -0
  10. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/orm/__init__.py +0 -0
  11. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/orm/error.py +7 -0
  12. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/orm/schema_registry.py +78 -0
  13. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/orm/table.py +83 -0
  14. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/persistency/__init__.py +0 -0
  15. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/persistency/connection_manager.py +76 -0
  16. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/persistency/error.py +7 -0
  17. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/persistency/repository.py +405 -0
  18. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/persistency/session_manager.py +103 -0
  19. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/persistency/transaction.py +68 -0
  20. spakky_sqlalchemy-5.0.1/src/spakky/plugins/sqlalchemy/py.typed +0 -0
@@ -0,0 +1,255 @@
1
+ Metadata-Version: 2.3
2
+ Name: spakky-sqlalchemy
3
+ Version: 5.0.1
4
+ Summary: SQLAlchemy plugin for Spakky framework
5
+ Author: Spakky
6
+ Author-email: Spakky <sejong418@icloud.com>
7
+ License: MIT
8
+ Requires-Dist: spakky-data>=5.0.1
9
+ Requires-Dist: sqlalchemy>=2.0.45
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Spakky SQLAlchemy
14
+
15
+ SQLAlchemy integration plugin for [Spakky Framework](https://github.com/E5presso/spakky-framework).
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install spakky-sqlalchemy
21
+ ```
22
+
23
+ Or install via Spakky extras:
24
+
25
+ ```bash
26
+ pip install spakky[sqlalchemy]
27
+ ```
28
+
29
+ ## Configuration
30
+
31
+ Set environment variables with the `SPAKKY_SQLALCHEMY__` prefix:
32
+
33
+ ```bash
34
+ # Required
35
+ export SPAKKY_SQLALCHEMY__CONNECTION_STRING="postgresql+psycopg://user:pass@localhost/db"
36
+
37
+ # Engine options (optional)
38
+ export SPAKKY_SQLALCHEMY__ECHO="false"
39
+ export SPAKKY_SQLALCHEMY__ECHO_POOL="false"
40
+
41
+ # Connection pool options (optional)
42
+ export SPAKKY_SQLALCHEMY__POOL_SIZE="5"
43
+ export SPAKKY_SQLALCHEMY__POOL_MAX_OVERFLOW="10"
44
+ export SPAKKY_SQLALCHEMY__POOL_TIMEOUT="30.0"
45
+ export SPAKKY_SQLALCHEMY__POOL_RECYCLE="-1"
46
+ export SPAKKY_SQLALCHEMY__POOL_PRE_PING="false"
47
+
48
+ # Session options (optional)
49
+ export SPAKKY_SQLALCHEMY__SESSION_AUTOFLUSH="true"
50
+ export SPAKKY_SQLALCHEMY__SESSION_EXPIRE_ON_COMMIT="true"
51
+
52
+ # Transaction options (optional)
53
+ export SPAKKY_SQLALCHEMY__AUTOCOMMIT="true"
54
+
55
+ # Async support (optional)
56
+ export SPAKKY_SQLALCHEMY__SUPPORT_ASYNC_MODE="true"
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ### Defining Tables with Domain Mapping
62
+
63
+ Use `@Table` decorator and inherit from `AbstractTable` to define ORM tables with domain model mapping:
64
+
65
+ ```python
66
+ from uuid import UUID
67
+ from sqlalchemy import String
68
+ from sqlalchemy.orm import Mapped, mapped_column
69
+ from spakky.plugins.sqlalchemy.orm.table import AbstractTable, Table
70
+ from spakky.domain.models.aggregate_root import AbstractAggregateRoot
71
+
72
+ # Domain model
73
+ class User(AbstractAggregateRoot[UUID]):
74
+ id: UUID
75
+ name: str
76
+ email: str
77
+
78
+ # Table definition with domain mapping
79
+ @Table()
80
+ class UserTable(AbstractMappableTable[User]):
81
+ __tablename__ = "users"
82
+
83
+ id: Mapped[UUID] = mapped_column(primary_key=True)
84
+ name: Mapped[str] = mapped_column(String(255))
85
+ email: Mapped[str] = mapped_column(String(255))
86
+
87
+ @classmethod
88
+ def from_domain(cls, domain: User) -> "UserTable":
89
+ return cls(id=domain.id, name=domain.name, email=domain.email)
90
+
91
+ def to_domain(self) -> User:
92
+ return User(id=self.id, name=self.name, email=self.email)
93
+ ```
94
+
95
+ ### Repository Implementation
96
+
97
+ Extend `AbstractGenericRepository` or `AbstractAsyncGenericRepository`:
98
+
99
+ ```python
100
+ from uuid import UUID
101
+ from spakky.data.stereotype.repository import Repository
102
+ from spakky.plugins.sqlalchemy.persistency.repository import (
103
+ AbstractGenericRepository,
104
+ AbstractAsyncGenericRepository,
105
+ )
106
+
107
+ # Synchronous repository
108
+ @Repository()
109
+ class UserRepository(AbstractGenericRepository[User, UUID]):
110
+ pass # CRUD methods inherited
111
+
112
+ # Asynchronous repository
113
+ @Repository()
114
+ class AsyncUserRepository(AbstractAsyncGenericRepository[User, UUID]):
115
+ pass # Async CRUD methods inherited
116
+ ```
117
+
118
+ ### Using Transactions
119
+
120
+ Use the `@Transactional` decorator from `spakky-data`:
121
+
122
+ ```python
123
+ from spakky.core.stereotype.usecase import UseCase
124
+ from spakky.data.aspects.transactional import Transactional
125
+
126
+ @UseCase()
127
+ class CreateUserUseCase:
128
+ def __init__(self, user_repo: UserRepository) -> None:
129
+ self._user_repo = user_repo
130
+
131
+ @Transactional()
132
+ def execute(self, name: str, email: str) -> User:
133
+ user = User.create(name, email)
134
+ return self._user_repo.save(user)
135
+ ```
136
+
137
+ ### Async Transactions
138
+
139
+ The same `@Transactional()` decorator works for both sync and async methods.
140
+ The framework automatically selects the correct aspect based on whether the method is a coroutine.
141
+
142
+ ```python
143
+ from spakky.data.aspects.transactional import Transactional
144
+
145
+ @UseCase()
146
+ class AsyncCreateUserUseCase:
147
+ def __init__(self, user_repo: AsyncUserRepository) -> None:
148
+ self._user_repo = user_repo
149
+
150
+ @Transactional()
151
+ async def execute(self, name: str, email: str) -> User:
152
+ user = User.create(name, email)
153
+ return await self._user_repo.save(user)
154
+ ```
155
+
156
+ ### Accessing Session Directly
157
+
158
+ For complex queries, access the SQLAlchemy session directly in **QueryUseCase**.
159
+ Following CQRS principles, queries should be implemented directly rather than
160
+ adding query methods to repositories.
161
+
162
+ ```python
163
+ from spakky.core.common.mutability import immutable
164
+ from spakky.core.stereotype.usecase import UseCase
165
+ from spakky.domain.application.query import AbstractQuery, IAsyncQueryUseCase
166
+ from spakky.plugins.sqlalchemy.persistency.session_manager import AsyncSessionManager
167
+
168
+
169
+ @immutable
170
+ class FindUserByEmailQuery(AbstractQuery):
171
+ email: str
172
+
173
+
174
+ @immutable
175
+ class UserDTO:
176
+ id: UUID
177
+ name: str
178
+ email: str
179
+
180
+
181
+ @UseCase()
182
+ class FindUserByEmailUseCase(IAsyncQueryUseCase[FindUserByEmailQuery, UserDTO | None]):
183
+ def __init__(self, session_manager: AsyncSessionManager) -> None:
184
+ self._session_manager = session_manager
185
+
186
+ async def run(self, query: FindUserByEmailQuery) -> UserDTO | None:
187
+ result = await self._session_manager.session.execute(
188
+ select(UserTable).where(UserTable.email == query.email)
189
+ )
190
+ table = result.scalar_one_or_none()
191
+ if table is None:
192
+ return None
193
+ return UserDTO(id=table.id, name=table.name, email=table.email)
194
+ ```
195
+
196
+ ### Schema Registry
197
+
198
+ Access `SchemaRegistry` to get table metadata for migrations:
199
+
200
+ ```python
201
+ from spakky.plugins.sqlalchemy.orm.schema_registry import SchemaRegistry
202
+
203
+ @Pod()
204
+ class MigrationService:
205
+ def __init__(self, schema_registry: SchemaRegistry) -> None:
206
+ self._schema_registry = schema_registry
207
+
208
+ def get_metadata(self) -> MetaData:
209
+ return self._schema_registry.metadata
210
+ ```
211
+
212
+ ## Features
213
+
214
+ - **Domain-Table mapping**: Bidirectional conversion between domain models and ORM tables
215
+ - **Generic repositories**: Pre-built CRUD operations with composite PK support
216
+ - **Sync and Async support**: Full support for both synchronous and asynchronous operations
217
+ - **Scoped sessions**: Thread/context-safe session management
218
+ - **Optimistic locking**: Built-in `VersionConflictError` for concurrent updates
219
+ - **Schema registry**: Centralized table metadata management
220
+
221
+ ## Components
222
+
223
+ | Component | Description |
224
+ |-----------|-------------|
225
+ | `@Table` | Decorator for registering ORM tables with domain mapping |
226
+ | `AbstractTable` | Base class for ORM tables with `from_domain`/`to_domain` |
227
+ | `AbstractGenericRepository` | Sync repository with CRUD operations |
228
+ | `AbstractAsyncGenericRepository` | Async repository with CRUD operations |
229
+ | `SchemaRegistry` | Central registry for table-domain mappings |
230
+ | `SessionManager` | Sync scoped session management |
231
+ | `AsyncSessionManager` | Async scoped session management |
232
+ | `Transaction` | Sync transaction implementation |
233
+ | `AsyncTransaction` | Async transaction implementation |
234
+ | `ConnectionManager` | Sync SQLAlchemy engine lifecycle |
235
+ | `AsyncConnectionManager` | Async SQLAlchemy engine lifecycle |
236
+ | `SQLAlchemyConnectionConfig` | Configuration via environment variables |
237
+
238
+ ## Repository Methods
239
+
240
+ Both sync and async repositories provide:
241
+
242
+ | Method | Description |
243
+ |--------|-------------|
244
+ | `get(id)` | Get aggregate by ID, raises `EntityNotFoundError` if not found |
245
+ | `get_or_none(id)` | Get aggregate by ID, returns `None` if not found |
246
+ | `contains(id)` | Check if aggregate exists |
247
+ | `range(ids)` | Get multiple aggregates by ID list |
248
+ | `save(aggregate)` | Save (insert or update) an aggregate |
249
+ | `save_all(aggregates)` | Save multiple aggregates |
250
+ | `delete(aggregate)` | Delete an aggregate |
251
+ | `delete_all(aggregates)` | Delete multiple aggregates |
252
+
253
+ ## License
254
+
255
+ MIT License
@@ -0,0 +1,243 @@
1
+ # Spakky SQLAlchemy
2
+
3
+ SQLAlchemy integration plugin for [Spakky Framework](https://github.com/E5presso/spakky-framework).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install spakky-sqlalchemy
9
+ ```
10
+
11
+ Or install via Spakky extras:
12
+
13
+ ```bash
14
+ pip install spakky[sqlalchemy]
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ Set environment variables with the `SPAKKY_SQLALCHEMY__` prefix:
20
+
21
+ ```bash
22
+ # Required
23
+ export SPAKKY_SQLALCHEMY__CONNECTION_STRING="postgresql+psycopg://user:pass@localhost/db"
24
+
25
+ # Engine options (optional)
26
+ export SPAKKY_SQLALCHEMY__ECHO="false"
27
+ export SPAKKY_SQLALCHEMY__ECHO_POOL="false"
28
+
29
+ # Connection pool options (optional)
30
+ export SPAKKY_SQLALCHEMY__POOL_SIZE="5"
31
+ export SPAKKY_SQLALCHEMY__POOL_MAX_OVERFLOW="10"
32
+ export SPAKKY_SQLALCHEMY__POOL_TIMEOUT="30.0"
33
+ export SPAKKY_SQLALCHEMY__POOL_RECYCLE="-1"
34
+ export SPAKKY_SQLALCHEMY__POOL_PRE_PING="false"
35
+
36
+ # Session options (optional)
37
+ export SPAKKY_SQLALCHEMY__SESSION_AUTOFLUSH="true"
38
+ export SPAKKY_SQLALCHEMY__SESSION_EXPIRE_ON_COMMIT="true"
39
+
40
+ # Transaction options (optional)
41
+ export SPAKKY_SQLALCHEMY__AUTOCOMMIT="true"
42
+
43
+ # Async support (optional)
44
+ export SPAKKY_SQLALCHEMY__SUPPORT_ASYNC_MODE="true"
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ### Defining Tables with Domain Mapping
50
+
51
+ Use `@Table` decorator and inherit from `AbstractTable` to define ORM tables with domain model mapping:
52
+
53
+ ```python
54
+ from uuid import UUID
55
+ from sqlalchemy import String
56
+ from sqlalchemy.orm import Mapped, mapped_column
57
+ from spakky.plugins.sqlalchemy.orm.table import AbstractTable, Table
58
+ from spakky.domain.models.aggregate_root import AbstractAggregateRoot
59
+
60
+ # Domain model
61
+ class User(AbstractAggregateRoot[UUID]):
62
+ id: UUID
63
+ name: str
64
+ email: str
65
+
66
+ # Table definition with domain mapping
67
+ @Table()
68
+ class UserTable(AbstractMappableTable[User]):
69
+ __tablename__ = "users"
70
+
71
+ id: Mapped[UUID] = mapped_column(primary_key=True)
72
+ name: Mapped[str] = mapped_column(String(255))
73
+ email: Mapped[str] = mapped_column(String(255))
74
+
75
+ @classmethod
76
+ def from_domain(cls, domain: User) -> "UserTable":
77
+ return cls(id=domain.id, name=domain.name, email=domain.email)
78
+
79
+ def to_domain(self) -> User:
80
+ return User(id=self.id, name=self.name, email=self.email)
81
+ ```
82
+
83
+ ### Repository Implementation
84
+
85
+ Extend `AbstractGenericRepository` or `AbstractAsyncGenericRepository`:
86
+
87
+ ```python
88
+ from uuid import UUID
89
+ from spakky.data.stereotype.repository import Repository
90
+ from spakky.plugins.sqlalchemy.persistency.repository import (
91
+ AbstractGenericRepository,
92
+ AbstractAsyncGenericRepository,
93
+ )
94
+
95
+ # Synchronous repository
96
+ @Repository()
97
+ class UserRepository(AbstractGenericRepository[User, UUID]):
98
+ pass # CRUD methods inherited
99
+
100
+ # Asynchronous repository
101
+ @Repository()
102
+ class AsyncUserRepository(AbstractAsyncGenericRepository[User, UUID]):
103
+ pass # Async CRUD methods inherited
104
+ ```
105
+
106
+ ### Using Transactions
107
+
108
+ Use the `@Transactional` decorator from `spakky-data`:
109
+
110
+ ```python
111
+ from spakky.core.stereotype.usecase import UseCase
112
+ from spakky.data.aspects.transactional import Transactional
113
+
114
+ @UseCase()
115
+ class CreateUserUseCase:
116
+ def __init__(self, user_repo: UserRepository) -> None:
117
+ self._user_repo = user_repo
118
+
119
+ @Transactional()
120
+ def execute(self, name: str, email: str) -> User:
121
+ user = User.create(name, email)
122
+ return self._user_repo.save(user)
123
+ ```
124
+
125
+ ### Async Transactions
126
+
127
+ The same `@Transactional()` decorator works for both sync and async methods.
128
+ The framework automatically selects the correct aspect based on whether the method is a coroutine.
129
+
130
+ ```python
131
+ from spakky.data.aspects.transactional import Transactional
132
+
133
+ @UseCase()
134
+ class AsyncCreateUserUseCase:
135
+ def __init__(self, user_repo: AsyncUserRepository) -> None:
136
+ self._user_repo = user_repo
137
+
138
+ @Transactional()
139
+ async def execute(self, name: str, email: str) -> User:
140
+ user = User.create(name, email)
141
+ return await self._user_repo.save(user)
142
+ ```
143
+
144
+ ### Accessing Session Directly
145
+
146
+ For complex queries, access the SQLAlchemy session directly in **QueryUseCase**.
147
+ Following CQRS principles, queries should be implemented directly rather than
148
+ adding query methods to repositories.
149
+
150
+ ```python
151
+ from spakky.core.common.mutability import immutable
152
+ from spakky.core.stereotype.usecase import UseCase
153
+ from spakky.domain.application.query import AbstractQuery, IAsyncQueryUseCase
154
+ from spakky.plugins.sqlalchemy.persistency.session_manager import AsyncSessionManager
155
+
156
+
157
+ @immutable
158
+ class FindUserByEmailQuery(AbstractQuery):
159
+ email: str
160
+
161
+
162
+ @immutable
163
+ class UserDTO:
164
+ id: UUID
165
+ name: str
166
+ email: str
167
+
168
+
169
+ @UseCase()
170
+ class FindUserByEmailUseCase(IAsyncQueryUseCase[FindUserByEmailQuery, UserDTO | None]):
171
+ def __init__(self, session_manager: AsyncSessionManager) -> None:
172
+ self._session_manager = session_manager
173
+
174
+ async def run(self, query: FindUserByEmailQuery) -> UserDTO | None:
175
+ result = await self._session_manager.session.execute(
176
+ select(UserTable).where(UserTable.email == query.email)
177
+ )
178
+ table = result.scalar_one_or_none()
179
+ if table is None:
180
+ return None
181
+ return UserDTO(id=table.id, name=table.name, email=table.email)
182
+ ```
183
+
184
+ ### Schema Registry
185
+
186
+ Access `SchemaRegistry` to get table metadata for migrations:
187
+
188
+ ```python
189
+ from spakky.plugins.sqlalchemy.orm.schema_registry import SchemaRegistry
190
+
191
+ @Pod()
192
+ class MigrationService:
193
+ def __init__(self, schema_registry: SchemaRegistry) -> None:
194
+ self._schema_registry = schema_registry
195
+
196
+ def get_metadata(self) -> MetaData:
197
+ return self._schema_registry.metadata
198
+ ```
199
+
200
+ ## Features
201
+
202
+ - **Domain-Table mapping**: Bidirectional conversion between domain models and ORM tables
203
+ - **Generic repositories**: Pre-built CRUD operations with composite PK support
204
+ - **Sync and Async support**: Full support for both synchronous and asynchronous operations
205
+ - **Scoped sessions**: Thread/context-safe session management
206
+ - **Optimistic locking**: Built-in `VersionConflictError` for concurrent updates
207
+ - **Schema registry**: Centralized table metadata management
208
+
209
+ ## Components
210
+
211
+ | Component | Description |
212
+ |-----------|-------------|
213
+ | `@Table` | Decorator for registering ORM tables with domain mapping |
214
+ | `AbstractTable` | Base class for ORM tables with `from_domain`/`to_domain` |
215
+ | `AbstractGenericRepository` | Sync repository with CRUD operations |
216
+ | `AbstractAsyncGenericRepository` | Async repository with CRUD operations |
217
+ | `SchemaRegistry` | Central registry for table-domain mappings |
218
+ | `SessionManager` | Sync scoped session management |
219
+ | `AsyncSessionManager` | Async scoped session management |
220
+ | `Transaction` | Sync transaction implementation |
221
+ | `AsyncTransaction` | Async transaction implementation |
222
+ | `ConnectionManager` | Sync SQLAlchemy engine lifecycle |
223
+ | `AsyncConnectionManager` | Async SQLAlchemy engine lifecycle |
224
+ | `SQLAlchemyConnectionConfig` | Configuration via environment variables |
225
+
226
+ ## Repository Methods
227
+
228
+ Both sync and async repositories provide:
229
+
230
+ | Method | Description |
231
+ |--------|-------------|
232
+ | `get(id)` | Get aggregate by ID, raises `EntityNotFoundError` if not found |
233
+ | `get_or_none(id)` | Get aggregate by ID, returns `None` if not found |
234
+ | `contains(id)` | Check if aggregate exists |
235
+ | `range(ids)` | Get multiple aggregates by ID list |
236
+ | `save(aggregate)` | Save (insert or update) an aggregate |
237
+ | `save_all(aggregates)` | Save multiple aggregates |
238
+ | `delete(aggregate)` | Delete an aggregate |
239
+ | `delete_all(aggregates)` | Delete multiple aggregates |
240
+
241
+ ## License
242
+
243
+ MIT License
@@ -0,0 +1,80 @@
1
+ [project]
2
+ name = "spakky-sqlalchemy"
3
+ version = "5.0.1"
4
+ description = "SQLAlchemy plugin for Spakky framework"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Spakky", email = "sejong418@icloud.com" }]
9
+ dependencies = [
10
+ "spakky-data>=5.0.1",
11
+ "sqlalchemy>=2.0.45",
12
+ ]
13
+
14
+ [dependency-groups]
15
+ dev = [
16
+ "testcontainers[postgres]>=4.13.3",
17
+ "psycopg[binary]>=3.2.6",
18
+ "greenlet>=3.1.0",
19
+ ]
20
+
21
+ [project.entry-points."spakky.plugins"]
22
+ spakky-sqlalchemy = "spakky.plugins.sqlalchemy.main:initialize"
23
+
24
+ [build-system]
25
+ requires = ["uv_build>=0.9.13,<0.10.0"]
26
+ build-backend = "uv_build"
27
+
28
+ [tool.uv.build-backend]
29
+ module-root = "src"
30
+ module-name = "spakky.plugins.sqlalchemy"
31
+
32
+ [tool.pyrefly]
33
+ python-version = "3.14"
34
+ search_path = ["src", "."]
35
+ project_excludes = ["**/__pycache__", "**/*.pyc"]
36
+
37
+ [tool.ruff]
38
+ builtins = ["_"]
39
+ cache-dir = "~/.cache/ruff"
40
+
41
+ [tool.pytest.ini_options]
42
+ pythonpath = "src/spakky/plugins/sqlalchemy"
43
+ testpaths = "tests"
44
+ python_files = ["test_*.py"]
45
+ asyncio_mode = "auto"
46
+ addopts = """
47
+ --cov
48
+ --cov-report=term
49
+ --cov-report=xml
50
+ --no-cov-on-fail
51
+ --strict-markers
52
+ --dist=loadfile
53
+ -p no:warnings
54
+ -n auto
55
+ --spec
56
+ """
57
+ spec_test_format = "{result} {docstring_summary}"
58
+
59
+ [tool.coverage.run]
60
+ include = ["src/spakky/plugins/sqlalchemy/*"]
61
+ branch = true
62
+
63
+ [tool.coverage.report]
64
+ show_missing = true
65
+ precision = 2
66
+ fail_under = 90
67
+ skip_empty = true
68
+ exclude_lines = [
69
+ "pragma: no cover",
70
+ "def __repr__",
71
+ "raise AssertionError",
72
+ "raise NotImplementedError",
73
+ "@(abc\\.)?abstractmethod",
74
+ "@(typing\\.)?overload",
75
+ "\\.\\.\\.\\.",
76
+ "pass",
77
+ ]
78
+
79
+ [tool.uv.sources]
80
+ spakky-data = { workspace = true }
@@ -0,0 +1,4 @@
1
+ from spakky.core.application.plugin import Plugin
2
+
3
+ PLUGIN_NAME = Plugin(name="spakky-sqlalchemy")
4
+ """Plugin identifier for the SQLAlchemy integration."""
@@ -0,0 +1,71 @@
1
+ from typing import ClassVar
2
+
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+ from spakky.core.stereotype.configuration import Configuration
5
+
6
+ from spakky.plugins.sqlalchemy.common.constants import (
7
+ SPAKKY_SQLALCHEMY_CONFIG_ENV_PREFIX,
8
+ )
9
+
10
+
11
+ @Configuration()
12
+ class SQLAlchemyConnectionConfig(BaseSettings):
13
+ model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
14
+ env_prefix=SPAKKY_SQLALCHEMY_CONFIG_ENV_PREFIX,
15
+ env_file_encoding="utf-8",
16
+ env_nested_delimiter="__",
17
+ )
18
+
19
+ # --- Connection ---
20
+ connection_string: str
21
+ """SQLAlchemy database connection URL (e.g. postgresql+psycopg://user:pass@host/db)."""
22
+
23
+ # --- Engine ---
24
+ echo: bool = False
25
+ """If True, the engine logs all SQL statements to the default logger."""
26
+ echo_pool: bool = False
27
+ """If True, the connection pool logs all checkout/checkin events."""
28
+
29
+ # --- Connection Pool (QueuePool) Defaults ---
30
+ DEFAULT_POOL_SIZE: ClassVar[int] = 5
31
+ """Default number of connections in the pool."""
32
+ DEFAULT_POOL_MAX_OVERFLOW: ClassVar[int] = 10
33
+ """Default maximum overflow connections."""
34
+ DEFAULT_POOL_TIMEOUT: ClassVar[float] = 30.0
35
+ """Default seconds to wait when pool is exhausted."""
36
+ DEFAULT_POOL_RECYCLE: ClassVar[int] = -1
37
+ """Default connection recycle time (-1 = disabled)."""
38
+ DEFAULT_POOL_PRE_PING: ClassVar[bool] = False
39
+ """Default pre-ping setting."""
40
+
41
+ # --- Connection Pool (QueuePool) ---
42
+ pool_size: int = DEFAULT_POOL_SIZE
43
+ """Number of connections to maintain persistently in the pool."""
44
+ pool_max_overflow: int = DEFAULT_POOL_MAX_OVERFLOW
45
+ """Maximum number of connections that can be opened beyond pool_size."""
46
+ pool_timeout: float = DEFAULT_POOL_TIMEOUT
47
+ """Seconds to wait before raising an error when the pool is exhausted."""
48
+ pool_recycle: int = DEFAULT_POOL_RECYCLE
49
+ """Recycle connections after this many seconds to prevent stale connections.
50
+ Recommended when connecting through a proxy or firewall with idle timeouts."""
51
+ pool_pre_ping: bool = DEFAULT_POOL_PRE_PING
52
+ """If True, tests each connection for liveness before returning it from the pool.
53
+ Prevents errors caused by connections dropped by the database server."""
54
+
55
+ # --- Session ---
56
+ session_autoflush: bool = True
57
+ """If True, the session automatically flushes pending changes before queries."""
58
+ session_expire_on_commit: bool = True
59
+ """If True, ORM objects are expired after each commit, forcing a reload on next access."""
60
+
61
+ # --- Transaction ---
62
+ autocommit: bool = True
63
+ """If True, transactions are automatically committed after with statements. If False, transactions must be manually committed or rolled back."""
64
+
65
+ # --- Async Mode ---
66
+ support_async_mode: bool = True
67
+ """If True, registers async Pods (AsyncSessionManager, AsyncTransaction).
68
+ Set to False when using database drivers that don't support async operations."""
69
+
70
+ def __init__(self) -> None:
71
+ super().__init__()
@@ -0,0 +1 @@
1
+ SPAKKY_SQLALCHEMY_CONFIG_ENV_PREFIX = "SPAKKY_SQLALCHEMY__"
@@ -0,0 +1,7 @@
1
+ from abc import ABC
2
+
3
+ from spakky.core.common.error import AbstractSpakkyFrameworkError
4
+
5
+
6
+ class AbstractSpakkySqlAlchemyError(AbstractSpakkyFrameworkError, ABC):
7
+ """Base exception for Spakky SQLAlchemy errors."""