yaddd-sqlalchemy 0.1.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.
@@ -0,0 +1,8 @@
1
+ """SQLAlchemy integration for yaddd."""
2
+
3
+ from yaddd_sqlalchemy.repositories import SqlCrudRepository
4
+ from yaddd_sqlalchemy.uow import SqlUnitOfWork
5
+ from yaddd_sqlalchemy.value_objects import VOTypeDecorator
6
+
7
+
8
+ __all__ = ["SqlCrudRepository", "SqlUnitOfWork", "VOTypeDecorator"]
File without changes
@@ -0,0 +1,87 @@
1
+ """SQLAlchemy Core implementation of the ``CrudRepository`` port."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, ClassVar
5
+
6
+ from sqlalchemy import Column, Table, delete, insert, select, update
7
+ from sqlalchemy.engine import Row
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+ from yaddd.domain.entities import AggregateRoot, PrimaryKey
10
+ from yaddd.domain.repositories import CrudRepository
11
+ from yaddd.exceptions import EntityNotFoundError
12
+
13
+
14
+ __all__ = ["SqlCrudRepository"]
15
+
16
+
17
+ class SqlCrudRepository[T: AggregateRoot](CrudRepository[T], ABC):
18
+ """SQLAlchemy Core-based repository for a single aggregate type.
19
+
20
+ No ORM declarative opinions: the subclass declares a Core ``table`` and
21
+ the mapping between rows and aggregates. The repository never commits —
22
+ transaction boundaries belong to ``SqlUnitOfWork``.
23
+
24
+ Primary keys must be single-column, and ``pk`` values must be natively
25
+ bindable by the primary-key column type (use a ``TypeDecorator`` column
26
+ or string/integer primary keys otherwise).
27
+
28
+ Example:
29
+ class OrderRepository(SqlCrudRepository[Order]):
30
+ table = orders_table
31
+
32
+ def to_domain(self, row: Row[Any]) -> Order:
33
+ return Order(id=row.id, status=row.status, total=row.total)
34
+
35
+ def to_row(self, instance: Order) -> dict[str, Any]:
36
+ return {"id": instance.pk, "status": instance.status, "total": instance.total}
37
+ """
38
+
39
+ table: ClassVar[Table]
40
+
41
+ def __init__(self, session: AsyncSession) -> None:
42
+ self._session = session
43
+
44
+ @abstractmethod
45
+ def to_domain(self, row: Row[Any]) -> T:
46
+ """Reconstitute the aggregate from a table row."""
47
+
48
+ @abstractmethod
49
+ def to_row(self, instance: T) -> dict[str, Any]:
50
+ """Map the aggregate to a dict of column values."""
51
+
52
+ @property
53
+ def _pk_column(self) -> Column[Any]:
54
+ columns = list(self.table.primary_key.columns)
55
+ if len(columns) != 1:
56
+ raise NotImplementedError("SqlCrudRepository supports single-column primary keys only")
57
+ return columns[0]
58
+
59
+ async def get(self, pk: PrimaryKey) -> T | None:
60
+ result = await self._session.execute(select(self.table).where(self._pk_column == pk))
61
+ row = result.first()
62
+ return None if row is None else self.to_domain(row)
63
+
64
+ async def read(self, filter: dict[str, Any] | None = None, *, slice: tuple[int, int] = (0, 0)) -> list[T]:
65
+ stmt = select(self.table)
66
+ for key, value in (filter or {}).items():
67
+ stmt = stmt.where(self.table.c[key] == value)
68
+ offset, limit = slice
69
+ stmt = stmt.offset(offset)
70
+ if limit:
71
+ stmt = stmt.limit(limit)
72
+ result = await self._session.execute(stmt)
73
+ return [self.to_domain(row) for row in result.all()]
74
+
75
+ async def create(self, instance: T) -> T:
76
+ await self._session.execute(insert(self.table).values(**self.to_row(instance)))
77
+ return instance
78
+
79
+ async def update(self, instance: T) -> T:
80
+ if await self.get(instance.pk) is None:
81
+ raise EntityNotFoundError(f"{type(instance).__name__}({instance.pk})")
82
+ stmt = update(self.table).where(self._pk_column == instance.pk).values(**self.to_row(instance))
83
+ await self._session.execute(stmt)
84
+ return instance
85
+
86
+ async def delete(self, pk: PrimaryKey) -> None:
87
+ await self._session.execute(delete(self.table).where(self._pk_column == pk))
@@ -0,0 +1,47 @@
1
+ """``UnitOfWork`` implementation over a SQLAlchemy ``AsyncSession``."""
2
+
3
+ from types import TracebackType
4
+ from typing import Self
5
+
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+ from yaddd.application.uow import UnitOfWork
8
+
9
+
10
+ __all__ = ["SqlUnitOfWork"]
11
+
12
+
13
+ class SqlUnitOfWork(UnitOfWork):
14
+ """Transactional boundary over a single ``AsyncSession``.
15
+
16
+ ``commit()`` flushes and commits the session. Leaving the context without
17
+ a commit — or with an exception — rolls back; the session is always
18
+ closed on exit.
19
+ """
20
+
21
+ def __init__(self, session: AsyncSession) -> None:
22
+ self._session = session
23
+ self._committed = False
24
+
25
+ async def __aenter__(self) -> Self:
26
+ return self
27
+
28
+ async def __aexit__(
29
+ self,
30
+ exc_type: type[BaseException] | None,
31
+ exc_value: BaseException | None,
32
+ traceback: TracebackType | None,
33
+ ) -> None:
34
+ try:
35
+ if exc_type is not None or not self._committed:
36
+ await self.rollback()
37
+ finally:
38
+ await self._session.close()
39
+
40
+ async def commit(self) -> None:
41
+ """Persist all changes made within the unit of work."""
42
+ await self._session.commit()
43
+ self._committed = True
44
+
45
+ async def rollback(self) -> None:
46
+ """Discard all changes made within the unit of work."""
47
+ await self._session.rollback()
@@ -0,0 +1,33 @@
1
+ """SQLAlchemy type decorator persisting ``ValueObject`` instances."""
2
+
3
+ from typing import Any, ClassVar
4
+
5
+ from sqlalchemy.engine import Dialect
6
+ from sqlalchemy.types import TypeDecorator
7
+ from yaddd.domain.value_object import ValueObject
8
+
9
+
10
+ __all__ = ["VOTypeDecorator"]
11
+
12
+
13
+ class VOTypeDecorator(TypeDecorator[ValueObject[Any]]):
14
+ """Persist a ``ValueObject`` as its raw value and rebuild it on load.
15
+
16
+ Subclass, declare ``vo_class`` and pick the storage type via ``impl``::
17
+
18
+ class EmailType(VOTypeDecorator):
19
+ impl = String(255)
20
+ cache_ok = True
21
+ vo_class = Email
22
+
23
+ Binding unwraps ``value.value``; loading revalidates through
24
+ ``vo_class(value)``. ``None`` passes through untouched in both directions.
25
+ """
26
+
27
+ vo_class: ClassVar[type[ValueObject[Any]]]
28
+
29
+ def process_bind_param(self, value: ValueObject[Any] | None, dialect: Dialect) -> Any:
30
+ return None if value is None else value.value
31
+
32
+ def process_result_value(self, value: Any, dialect: Dialect) -> ValueObject[Any] | None:
33
+ return None if value is None else self.vo_class(value)
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.5
2
+ Name: yaddd-sqlalchemy
3
+ Version: 0.1.0
4
+ Summary: SQLAlchemy integration for yaddd: SqlCrudRepository, SqlUnitOfWork, VOTypeDecorator
5
+ Project-URL: Homepage, https://github.com/netstuff/yaddd
6
+ Author-email: Yuri Andreev <andreev.jr@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Internet
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
20
+ Requires-Dist: yaddd>=0.1
21
+ Description-Content-Type: text/markdown
22
+
23
+ # yaddd-sqlalchemy
24
+ SQLAlchemy integration for [yaddd](https://github.com/pyDDD/yaddd).
25
+
26
+ - `SqlCrudRepository` — SQLAlchemy Core-based implementation of the `CrudRepository` port;
27
+ - `SqlUnitOfWork` — `UnitOfWork` over `AsyncSession`;
28
+ - `VOTypeDecorator` — `TypeDecorator` persisting `ValueObject` instances as raw values.
29
+
30
+ Wiring is explicit: instantiate in your composition root, no auto-discovery.
31
+ Port conformance is proven by the contract suites in `yaddd.testing`.
@@ -0,0 +1,9 @@
1
+ yaddd_sqlalchemy/__init__.py,sha256=if6Kyp_p9P6aLWVF5fqnUSE5dcbTcDYgsImYYqCfoZk,277
2
+ yaddd_sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ yaddd_sqlalchemy/repositories.py,sha256=tKlhzIEUxbLGZ_85MRT7lSkyOl1UVBKdZXnWdIqtEa0,3452
4
+ yaddd_sqlalchemy/uow.py,sha256=GCcFfLkZICPMuxP8tLzIJEZ3Ph4_GDiENWTc3QmnqrE,1376
5
+ yaddd_sqlalchemy/value_objects.py,sha256=X1M6yoJeylhcibzJ3t6lcNiDZRI7pPAsi90j4Y_n3t4,1112
6
+ yaddd_sqlalchemy-0.1.0.dist-info/METADATA,sha256=euvzt9HtmCtarllZH5v9LmcSdLJTo2nOenfjsIZOn3k,1345
7
+ yaddd_sqlalchemy-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
8
+ yaddd_sqlalchemy-0.1.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ yaddd_sqlalchemy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
File without changes