yaddd-sqlalchemy 0.1.1__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/.gitignore +6 -2
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/PKG-INFO +2 -2
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/pyproject.toml +2 -2
- yaddd_sqlalchemy-0.2.0/src/yaddd_sqlalchemy/agents/manifest.toml +20 -0
- yaddd_sqlalchemy-0.2.0/src/yaddd_sqlalchemy/agents/rules/infrastructure.md +12 -0
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/src/yaddd_sqlalchemy/repositories.py +15 -5
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/src/yaddd_sqlalchemy/session.py +8 -2
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/src/yaddd_sqlalchemy/uow.py +0 -1
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/src/yaddd_sqlalchemy/value_objects.py +1 -0
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/tests/test_uow.py +14 -1
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/LICENSE +0 -0
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/README.md +0 -0
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/src/yaddd_sqlalchemy/__init__.py +0 -0
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/src/yaddd_sqlalchemy/py.typed +0 -0
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/tests/conftest.py +0 -0
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/tests/test_repository.py +0 -0
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/tests/test_session.py +0 -0
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/tests/test_transaction_bound_repository.py +0 -0
- {yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/tests/test_value_objects.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: yaddd-sqlalchemy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: SQLAlchemy integration for yaddd: SqlCrudRepository, SqlUnitOfWork, VOTypeDecorator
|
|
5
5
|
Project-URL: Homepage, https://github.com/netstuff/yaddd
|
|
6
6
|
Author-email: Yuri Andreev <andreev.jr@gmail.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Topic :: Internet
|
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
19
|
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
20
|
-
Requires-Dist: yaddd>=0.
|
|
20
|
+
Requires-Dist: yaddd>=0.5
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
|
|
23
23
|
# yaddd-sqlalchemy
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
schema = 1
|
|
2
|
+
name = "yaddd-sqlalchemy"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
|
|
5
|
+
[[relaxes]]
|
|
6
|
+
rule = "IMPORTS_INFRASTRUCTURE"
|
|
7
|
+
allow_add = ["sqlalchemy", "yaddd_sqlalchemy"]
|
|
8
|
+
reason = "SqlCrudRepository, SqlUnitOfWork and VOTypeDecorator implement persistence ports."
|
|
9
|
+
when = { dependency = "yaddd-sqlalchemy", specifier = ">=0.1" }
|
|
10
|
+
|
|
11
|
+
[[restricts]]
|
|
12
|
+
rule = "IMPORTS_APPLICATION"
|
|
13
|
+
deny_add = ["sqlalchemy", "yaddd_sqlalchemy"]
|
|
14
|
+
reason = "Application stays persistence-agnostic; only infrastructure may know SQLAlchemy."
|
|
15
|
+
when = { dependency = "yaddd-sqlalchemy" }
|
|
16
|
+
|
|
17
|
+
[rules.SQLALCHEMY_PERSISTENCE]
|
|
18
|
+
layer = "infrastructure"
|
|
19
|
+
kind = "guidance"
|
|
20
|
+
doc = "rules/infrastructure.md"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# SQLAlchemy persistence
|
|
2
|
+
|
|
3
|
+
Additive to the base infrastructure rules. This pack relaxes exactly one import
|
|
4
|
+
rule: infrastructure may use `sqlalchemy` and `yaddd_sqlalchemy`.
|
|
5
|
+
|
|
6
|
+
- **Use `SqlCrudRepository`** for Core-based repositories and
|
|
7
|
+
`SqlUnitOfWork` for transaction boundaries. Do not commit inside a
|
|
8
|
+
repository.
|
|
9
|
+
- **Use `VOTypeDecorator`** for columns that hold value objects, so values are
|
|
10
|
+
validated on the way in and on the way out.
|
|
11
|
+
- **Keep SQLAlchemy out of `application`.** The pack also restricts that layer:
|
|
12
|
+
use cases depend on ports, not sessions or tables.
|
|
@@ -5,11 +5,12 @@ from typing import Any, ClassVar
|
|
|
5
5
|
|
|
6
6
|
from sqlalchemy import Column, Table, delete, insert, select, update
|
|
7
7
|
from sqlalchemy.engine import Row
|
|
8
|
+
from sqlalchemy.exc import IntegrityError
|
|
8
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
|
|
9
11
|
from yaddd.domain.entities import AggregateRoot, PrimaryKey
|
|
10
12
|
from yaddd.domain.repositories import CrudRepository
|
|
11
|
-
from yaddd.exceptions import EntityNotFoundError
|
|
12
|
-
|
|
13
|
+
from yaddd.exceptions import ConflictError, EntityNotFoundError
|
|
13
14
|
from yaddd_sqlalchemy.session import SqlTransaction
|
|
14
15
|
|
|
15
16
|
|
|
@@ -79,18 +80,27 @@ class SqlCrudRepository[T: AggregateRoot](CrudRepository[T], ABC):
|
|
|
79
80
|
return [self.to_domain(row) for row in result.all()]
|
|
80
81
|
|
|
81
82
|
async def create(self, instance: T) -> T:
|
|
82
|
-
|
|
83
|
+
try:
|
|
84
|
+
await self._session.execute(insert(self.table).values(**self.to_row(instance)))
|
|
85
|
+
except IntegrityError as exc:
|
|
86
|
+
raise ConflictError(str(exc)) from exc
|
|
83
87
|
return instance
|
|
84
88
|
|
|
85
89
|
async def update(self, instance: T) -> T:
|
|
86
90
|
if await self.get(instance.pk) is None:
|
|
87
91
|
raise EntityNotFoundError(f"{type(instance).__name__}({instance.pk})")
|
|
88
92
|
stmt = update(self.table).where(self._pk_column == instance.pk).values(**self.to_row(instance))
|
|
89
|
-
|
|
93
|
+
try:
|
|
94
|
+
await self._session.execute(stmt)
|
|
95
|
+
except IntegrityError as exc:
|
|
96
|
+
raise ConflictError(str(exc)) from exc
|
|
90
97
|
return instance
|
|
91
98
|
|
|
92
99
|
async def delete(self, pk: PrimaryKey) -> None:
|
|
93
|
-
|
|
100
|
+
try:
|
|
101
|
+
await self._session.execute(delete(self.table).where(self._pk_column == pk))
|
|
102
|
+
except IntegrityError as exc:
|
|
103
|
+
raise ConflictError(str(exc)) from exc
|
|
94
104
|
|
|
95
105
|
|
|
96
106
|
class SqlTransactionBoundRepository[T: AggregateRoot](SqlCrudRepository[T], ABC):
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"""SQLAlchemy implementation of the application ``Session`` protocol."""
|
|
2
2
|
|
|
3
|
+
from sqlalchemy.exc import IntegrityError
|
|
3
4
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
5
|
+
|
|
4
6
|
from yaddd.application.session import Session
|
|
7
|
+
from yaddd.exceptions import ConflictError
|
|
5
8
|
from yaddd.infrastructure.transaction import BaseTransaction
|
|
6
9
|
|
|
7
10
|
|
|
@@ -28,9 +31,12 @@ class SqlSession(Session):
|
|
|
28
31
|
await self._session.__aenter__()
|
|
29
32
|
|
|
30
33
|
async def commit(self) -> None:
|
|
31
|
-
"""Commit the current session."""
|
|
34
|
+
"""Commit the current session, translating integrity failures."""
|
|
32
35
|
if self._session is not None:
|
|
33
|
-
|
|
36
|
+
try:
|
|
37
|
+
await self._session.commit()
|
|
38
|
+
except IntegrityError as exc:
|
|
39
|
+
raise ConflictError(str(exc)) from exc
|
|
34
40
|
|
|
35
41
|
async def rollback(self) -> None:
|
|
36
42
|
"""Roll back the current session."""
|
|
@@ -5,7 +5,7 @@ import pytest
|
|
|
5
5
|
from sqlalchemy import select
|
|
6
6
|
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker
|
|
7
7
|
|
|
8
|
-
from yaddd import DomainEvent, InMemoryEventPublisher
|
|
8
|
+
from yaddd import ConflictError, DomainEvent, InMemoryEventPublisher
|
|
9
9
|
from yaddd_sqlalchemy import SqlSession, SqlTransaction, SqlTransactionBoundRepository, SqlUnitOfWork
|
|
10
10
|
|
|
11
11
|
from conftest import Order, orders_table
|
|
@@ -124,3 +124,16 @@ async def test_commit_publishes_tracked_events(engine: AsyncEngine):
|
|
|
124
124
|
|
|
125
125
|
assert len(events) == 1
|
|
126
126
|
assert isinstance(events[0], OrderPlaced)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def test_duplicate_commit_raises_conflict(engine: AsyncEngine):
|
|
130
|
+
order_id = uuid4().hex
|
|
131
|
+
|
|
132
|
+
async with build_uow(engine) as uow:
|
|
133
|
+
await uow.repos.orders.create(Order(id=order_id, status="new", total=10))
|
|
134
|
+
await uow.commit()
|
|
135
|
+
|
|
136
|
+
with pytest.raises(ConflictError):
|
|
137
|
+
async with build_uow(engine) as uow:
|
|
138
|
+
await uow.repos.orders.create(Order(id=order_id, status="new", total=10))
|
|
139
|
+
await uow.commit()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{yaddd_sqlalchemy-0.1.1 → yaddd_sqlalchemy-0.2.0}/tests/test_transaction_bound_repository.py
RENAMED
|
File without changes
|
|
File without changes
|