spakky-data 4.0.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.
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.3
2
+ Name: spakky-data
3
+ Version: 4.0.0
4
+ Summary: Data access layer for Spakky Framework (Repository implementations, ORM integration)
5
+ Author: Spakky
6
+ Author-email: Spakky <sejong418@icloud.com>
7
+ Requires-Dist: spakky-domain>=4.0.0
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Spakky Data
12
+
13
+ Data access layer abstractions for [Spakky Framework](https://github.com/E5presso/spakky-framework).
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install spakky-data
19
+ ```
20
+
21
+ ## Features
22
+
23
+ - **Repository Pattern**: Generic repository interfaces for aggregate persistence
24
+ - **Transaction Management**: Abstract transaction classes with autocommit support
25
+ - **External Proxy**: Proxy pattern for external service data access
26
+
27
+ ## Quick Start
28
+
29
+ ### Repository Pattern
30
+
31
+ Define repository interfaces for your domain aggregates:
32
+
33
+ ```python
34
+ from abc import abstractmethod
35
+ from uuid import UUID
36
+
37
+ from spakky.data.persistency.repository import IAsyncGenericRepository
38
+ from spakky.domain.models.aggregate_root import AbstractAggregateRoot
39
+
40
+
41
+ class User(AbstractAggregateRoot[UUID]):
42
+ name: str
43
+ email: str
44
+
45
+
46
+ class IUserRepository(IAsyncGenericRepository[User, UUID]):
47
+ @abstractmethod
48
+ async def find_by_email(self, email: str) -> User | None: ...
49
+ ```
50
+
51
+ ### Transaction Management
52
+
53
+ Use abstract transactions for database operations:
54
+
55
+ ```python
56
+ from spakky.data.persistency.transaction import AbstractAsyncTransaction
57
+
58
+
59
+ class SQLAlchemyTransaction(AbstractAsyncTransaction):
60
+ def __init__(self, session_factory, autocommit: bool = True) -> None:
61
+ super().__init__(autocommit)
62
+ self.session_factory = session_factory
63
+ self.session = None
64
+
65
+ async def initialize(self) -> None:
66
+ self.session = self.session_factory()
67
+
68
+ async def dispose(self) -> None:
69
+ await self.session.close()
70
+
71
+ async def commit(self) -> None:
72
+ await self.session.commit()
73
+
74
+ async def rollback(self) -> None:
75
+ await self.session.rollback()
76
+ ```
77
+
78
+ Usage with context manager:
79
+
80
+ ```python
81
+ async with transaction:
82
+ user = await repository.get(user_id)
83
+ user.name = "New Name"
84
+ await repository.save(user)
85
+ # Automatically commits on success, rollbacks on exception
86
+ ```
87
+
88
+ ### External Proxy Pattern
89
+
90
+ Access external service data with proxy interfaces:
91
+
92
+ ```python
93
+ from spakky.data.external.proxy import ProxyModel, IAsyncGenericProxy
94
+
95
+
96
+ class ExternalUser(ProxyModel[int]):
97
+ name: str
98
+ email: str
99
+
100
+
101
+ class IExternalUserProxy(IAsyncGenericProxy[ExternalUser, int]):
102
+ pass
103
+ ```
104
+
105
+ ## API Reference
106
+
107
+ ### Persistency
108
+
109
+ | Class | Description |
110
+ |-------|-------------|
111
+ | `IGenericRepository` | Sync generic repository interface |
112
+ | `IAsyncGenericRepository` | Async generic repository interface |
113
+ | `AbstractTransaction` | Sync transaction with context manager |
114
+ | `AbstractAsyncTransaction` | Async transaction with context manager |
115
+ | `EntityNotFoundError` | Raised when entity not found |
116
+
117
+ ### External
118
+
119
+ | Class | Description |
120
+ |-------|-------------|
121
+ | `ProxyModel` | Base class for external service data models |
122
+ | `IGenericProxy` | Sync proxy interface |
123
+ | `IAsyncGenericProxy` | Async proxy interface |
124
+
125
+ ### Errors
126
+
127
+ | Class | Description |
128
+ |-------|-------------|
129
+ | `AbstractSpakkyPersistencyError` | Base error for persistency operations |
130
+ | `AbstractSpakkyExternalError` | Base error for external service operations |
131
+
132
+ ## Related Packages
133
+
134
+ | Package | Description |
135
+ |---------|-------------|
136
+ | `spakky-domain` | DDD building blocks (Entity, AggregateRoot, ValueObject) |
137
+ | `spakky-event` | Event publisher/consumer interfaces |
138
+
139
+ ## License
140
+
141
+ MIT License
@@ -0,0 +1,131 @@
1
+ # Spakky Data
2
+
3
+ Data access layer abstractions for [Spakky Framework](https://github.com/E5presso/spakky-framework).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install spakky-data
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Repository Pattern**: Generic repository interfaces for aggregate persistence
14
+ - **Transaction Management**: Abstract transaction classes with autocommit support
15
+ - **External Proxy**: Proxy pattern for external service data access
16
+
17
+ ## Quick Start
18
+
19
+ ### Repository Pattern
20
+
21
+ Define repository interfaces for your domain aggregates:
22
+
23
+ ```python
24
+ from abc import abstractmethod
25
+ from uuid import UUID
26
+
27
+ from spakky.data.persistency.repository import IAsyncGenericRepository
28
+ from spakky.domain.models.aggregate_root import AbstractAggregateRoot
29
+
30
+
31
+ class User(AbstractAggregateRoot[UUID]):
32
+ name: str
33
+ email: str
34
+
35
+
36
+ class IUserRepository(IAsyncGenericRepository[User, UUID]):
37
+ @abstractmethod
38
+ async def find_by_email(self, email: str) -> User | None: ...
39
+ ```
40
+
41
+ ### Transaction Management
42
+
43
+ Use abstract transactions for database operations:
44
+
45
+ ```python
46
+ from spakky.data.persistency.transaction import AbstractAsyncTransaction
47
+
48
+
49
+ class SQLAlchemyTransaction(AbstractAsyncTransaction):
50
+ def __init__(self, session_factory, autocommit: bool = True) -> None:
51
+ super().__init__(autocommit)
52
+ self.session_factory = session_factory
53
+ self.session = None
54
+
55
+ async def initialize(self) -> None:
56
+ self.session = self.session_factory()
57
+
58
+ async def dispose(self) -> None:
59
+ await self.session.close()
60
+
61
+ async def commit(self) -> None:
62
+ await self.session.commit()
63
+
64
+ async def rollback(self) -> None:
65
+ await self.session.rollback()
66
+ ```
67
+
68
+ Usage with context manager:
69
+
70
+ ```python
71
+ async with transaction:
72
+ user = await repository.get(user_id)
73
+ user.name = "New Name"
74
+ await repository.save(user)
75
+ # Automatically commits on success, rollbacks on exception
76
+ ```
77
+
78
+ ### External Proxy Pattern
79
+
80
+ Access external service data with proxy interfaces:
81
+
82
+ ```python
83
+ from spakky.data.external.proxy import ProxyModel, IAsyncGenericProxy
84
+
85
+
86
+ class ExternalUser(ProxyModel[int]):
87
+ name: str
88
+ email: str
89
+
90
+
91
+ class IExternalUserProxy(IAsyncGenericProxy[ExternalUser, int]):
92
+ pass
93
+ ```
94
+
95
+ ## API Reference
96
+
97
+ ### Persistency
98
+
99
+ | Class | Description |
100
+ |-------|-------------|
101
+ | `IGenericRepository` | Sync generic repository interface |
102
+ | `IAsyncGenericRepository` | Async generic repository interface |
103
+ | `AbstractTransaction` | Sync transaction with context manager |
104
+ | `AbstractAsyncTransaction` | Async transaction with context manager |
105
+ | `EntityNotFoundError` | Raised when entity not found |
106
+
107
+ ### External
108
+
109
+ | Class | Description |
110
+ |-------|-------------|
111
+ | `ProxyModel` | Base class for external service data models |
112
+ | `IGenericProxy` | Sync proxy interface |
113
+ | `IAsyncGenericProxy` | Async proxy interface |
114
+
115
+ ### Errors
116
+
117
+ | Class | Description |
118
+ |-------|-------------|
119
+ | `AbstractSpakkyPersistencyError` | Base error for persistency operations |
120
+ | `AbstractSpakkyExternalError` | Base error for external service operations |
121
+
122
+ ## Related Packages
123
+
124
+ | Package | Description |
125
+ |---------|-------------|
126
+ | `spakky-domain` | DDD building blocks (Entity, AggregateRoot, ValueObject) |
127
+ | `spakky-event` | Event publisher/consumer interfaces |
128
+
129
+ ## License
130
+
131
+ MIT License
@@ -0,0 +1,63 @@
1
+ [project]
2
+ name = "spakky-data"
3
+ version = "4.0.0"
4
+ description = "Data access layer for Spakky Framework (Repository implementations, ORM integration)"
5
+ readme = "README.md"
6
+ authors = [{ name = "Spakky", email = "sejong418@icloud.com" }]
7
+ requires-python = ">=3.11"
8
+ dependencies = ["spakky-domain>=4.0.0"]
9
+
10
+ [build-system]
11
+ requires = ["uv_build>=0.9.5,<0.10.0"]
12
+ build-backend = "uv_build"
13
+
14
+ [tool.uv.build-backend]
15
+ module-root = "src"
16
+ module-name = "spakky.data"
17
+
18
+ [tool.pyrefly]
19
+ python-version = "3.14"
20
+ search_path = ["src", "."]
21
+ project_excludes = ["**/__pycache__", "**/*.pyc"]
22
+
23
+ [tool.ruff]
24
+ builtins = ["_"]
25
+ cache-dir = "~/.cache/ruff"
26
+
27
+ [tool.pytest.ini_options]
28
+ pythonpath = "src/spakky/data"
29
+ testpaths = "tests"
30
+ python_files = ["test_*.py"]
31
+ asyncio_mode = "auto"
32
+ addopts = """
33
+ --cov
34
+ --cov-report=term
35
+ --cov-report=xml
36
+ --no-cov-on-fail
37
+ --strict-markers
38
+ --dist=load
39
+ -p no:warnings
40
+ -n auto
41
+ -vv
42
+ """
43
+
44
+ [tool.coverage.run]
45
+ include = ["src/spakky/data/*"]
46
+ branch = true
47
+
48
+ [tool.coverage.report]
49
+ show_missing = true
50
+ precision = 2
51
+ fail_under = 90
52
+ skip_empty = true
53
+ exclude_lines = [
54
+ "pragma: no cover",
55
+ "def __repr__",
56
+ "raise AssertionError",
57
+ "raise NotImplementedError",
58
+ "@(abc\\.)?abstractmethod",
59
+ "@(typing\\.)?overload",
60
+ ]
61
+
62
+ [tool.uv.sources]
63
+ spakky-domain = { workspace = true }
@@ -0,0 +1,40 @@
1
+ """Spakky Data package - Data access abstractions.
2
+
3
+ This package provides:
4
+ - Repository pattern for aggregate persistence
5
+ - Transaction management
6
+ - External service proxy pattern
7
+
8
+ Usage:
9
+ from spakky.data import IGenericRepository, AbstractTransaction
10
+ from spakky.data import IGenericProxy, IAsyncGenericProxy
11
+ """
12
+
13
+ # Persistency
14
+ # External
15
+ from spakky.data.external.error import AbstractSpakkyExternalError
16
+ from spakky.data.external.proxy import IAsyncGenericProxy, IGenericProxy
17
+ from spakky.data.persistency.error import AbstractSpakkyPersistencyError
18
+ from spakky.data.persistency.repository import (
19
+ EntityNotFoundError,
20
+ IGenericRepository,
21
+ )
22
+ from spakky.data.persistency.transaction import (
23
+ AbstractAsyncTransaction,
24
+ AbstractTransaction,
25
+ )
26
+
27
+ __all__ = [
28
+ # Repository
29
+ "EntityNotFoundError",
30
+ "IGenericRepository",
31
+ # Transaction
32
+ "AbstractAsyncTransaction",
33
+ "AbstractTransaction",
34
+ # Proxy
35
+ "IAsyncGenericProxy",
36
+ "IGenericProxy",
37
+ # Errors
38
+ "AbstractSpakkyExternalError",
39
+ "AbstractSpakkyPersistencyError",
40
+ ]
@@ -0,0 +1,17 @@
1
+ """External service abstractions.
2
+
3
+ This module provides:
4
+ - Proxy pattern for external services
5
+ - External service errors
6
+ """
7
+
8
+ from spakky.data.external.error import AbstractSpakkyExternalError
9
+ from spakky.data.external.proxy import IAsyncGenericProxy, IGenericProxy
10
+
11
+ __all__ = [
12
+ # Proxy
13
+ "IAsyncGenericProxy",
14
+ "IGenericProxy",
15
+ # Errors
16
+ "AbstractSpakkyExternalError",
17
+ ]
@@ -0,0 +1,6 @@
1
+ from abc import ABC
2
+
3
+ from spakky.core.common.error import AbstractSpakkyFrameworkError
4
+
5
+
6
+ class AbstractSpakkyExternalError(AbstractSpakkyFrameworkError, ABC): ...
@@ -0,0 +1,55 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Generic, Sequence, TypeVar
3
+
4
+ from spakky.core.common.interfaces.equatable import IEquatable
5
+ from spakky.core.common.mutability import immutable
6
+
7
+ ProxyIdT_contra = TypeVar("ProxyIdT_contra", bound=IEquatable, contravariant=True)
8
+
9
+
10
+ @immutable
11
+ class ProxyModel(IEquatable, Generic[ProxyIdT_contra]):
12
+ id: ProxyIdT_contra
13
+
14
+ def __eq__(self, other: object) -> bool:
15
+ if not isinstance(other, type(self)):
16
+ return False
17
+ return self.id == other.id
18
+
19
+ def __hash__(self) -> int:
20
+ return hash(self.id)
21
+
22
+
23
+ ProxyModelT_co = TypeVar("ProxyModelT_co", bound=ProxyModel[Any], covariant=True)
24
+
25
+
26
+ class IGenericProxy(ABC, Generic[ProxyModelT_co, ProxyIdT_contra]):
27
+ @abstractmethod
28
+ def get(self, proxy_id: ProxyIdT_contra) -> ProxyModelT_co: ...
29
+
30
+ @abstractmethod
31
+ def get_or_none(self, proxy_id: ProxyIdT_contra) -> ProxyModelT_co | None: ...
32
+
33
+ @abstractmethod
34
+ def contains(self, proxy_id: ProxyIdT_contra) -> bool: ...
35
+
36
+ @abstractmethod
37
+ def range(
38
+ self, proxy_ids: Sequence[ProxyIdT_contra]
39
+ ) -> Sequence[ProxyModelT_co]: ...
40
+
41
+
42
+ class IAsyncGenericProxy(ABC, Generic[ProxyModelT_co, ProxyIdT_contra]):
43
+ @abstractmethod
44
+ async def get(self, proxy_id: ProxyIdT_contra) -> ProxyModelT_co: ...
45
+
46
+ @abstractmethod
47
+ async def get_or_none(self, proxy_id: ProxyIdT_contra) -> ProxyModelT_co | None: ...
48
+
49
+ @abstractmethod
50
+ async def contains(self, proxy_id: ProxyIdT_contra) -> bool: ...
51
+
52
+ @abstractmethod
53
+ async def range(
54
+ self, proxy_ids: Sequence[ProxyIdT_contra]
55
+ ) -> Sequence[ProxyModelT_co]: ...
@@ -0,0 +1,28 @@
1
+ """Data persistence abstractions.
2
+
3
+ This module provides:
4
+ - Repository pattern interfaces
5
+ - Transaction management
6
+ - Persistence-related errors
7
+ """
8
+
9
+ from spakky.data.persistency.error import AbstractSpakkyPersistencyError
10
+ from spakky.data.persistency.repository import (
11
+ EntityNotFoundError,
12
+ IGenericRepository,
13
+ )
14
+ from spakky.data.persistency.transaction import (
15
+ AbstractAsyncTransaction,
16
+ AbstractTransaction,
17
+ )
18
+
19
+ __all__ = [
20
+ # Repository
21
+ "EntityNotFoundError",
22
+ "IGenericRepository",
23
+ # Transaction
24
+ "AbstractAsyncTransaction",
25
+ "AbstractTransaction",
26
+ # Errors
27
+ "AbstractSpakkyPersistencyError",
28
+ ]
@@ -0,0 +1,6 @@
1
+ from abc import ABC
2
+
3
+ from spakky.core.common.error import AbstractSpakkyFrameworkError
4
+
5
+
6
+ class AbstractSpakkyPersistencyError(AbstractSpakkyFrameworkError, ABC): ...
@@ -0,0 +1,83 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Generic, Sequence, TypeVar
3
+
4
+ from spakky.core.common.interfaces.equatable import IEquatable
5
+
6
+ from spakky.domain.error import AbstractSpakkyDomainError
7
+ from spakky.domain.models.aggregate_root import AggregateRootT
8
+
9
+ AggregateIdT_contra = TypeVar(
10
+ "AggregateIdT_contra", bound=IEquatable, contravariant=True
11
+ )
12
+
13
+
14
+ class EntityNotFoundError(AbstractSpakkyDomainError):
15
+ message = "Entity not found by given id"
16
+
17
+
18
+ class IGenericRepository(ABC, Generic[AggregateRootT, AggregateIdT_contra]):
19
+ @abstractmethod
20
+ def get(self, aggregate_id: AggregateIdT_contra) -> AggregateRootT: ...
21
+
22
+ @abstractmethod
23
+ def get_or_none(
24
+ self, aggregate_id: AggregateIdT_contra
25
+ ) -> AggregateRootT | None: ...
26
+
27
+ @abstractmethod
28
+ def contains(self, aggregate_id: AggregateIdT_contra) -> bool: ...
29
+
30
+ @abstractmethod
31
+ def range(
32
+ self, aggregate_ids: Sequence[AggregateIdT_contra]
33
+ ) -> Sequence[AggregateRootT]: ...
34
+
35
+ @abstractmethod
36
+ def save(self, aggregate: AggregateRootT) -> AggregateRootT: ...
37
+
38
+ @abstractmethod
39
+ def save_all(
40
+ self, aggregates: Sequence[AggregateRootT]
41
+ ) -> Sequence[AggregateRootT]: ...
42
+
43
+ @abstractmethod
44
+ def delete(self, aggregate: AggregateRootT) -> AggregateRootT: ...
45
+
46
+ @abstractmethod
47
+ def delete_all(
48
+ self, aggregates: Sequence[AggregateRootT]
49
+ ) -> Sequence[AggregateRootT]: ...
50
+
51
+
52
+ class IAsyncGenericRepository(ABC, Generic[AggregateRootT, AggregateIdT_contra]):
53
+ @abstractmethod
54
+ async def get(self, aggregate_id: AggregateIdT_contra) -> AggregateRootT: ...
55
+
56
+ @abstractmethod
57
+ async def get_or_none(
58
+ self, aggregate_id: AggregateIdT_contra
59
+ ) -> AggregateRootT | None: ...
60
+
61
+ @abstractmethod
62
+ async def contains(self, aggregate_id: AggregateIdT_contra) -> bool: ...
63
+
64
+ @abstractmethod
65
+ async def range(
66
+ self, aggregate_ids: Sequence[AggregateIdT_contra]
67
+ ) -> Sequence[AggregateRootT]: ...
68
+
69
+ @abstractmethod
70
+ async def save(self, aggregate: AggregateRootT) -> AggregateRootT: ...
71
+
72
+ @abstractmethod
73
+ async def save_all(
74
+ self, aggregates: Sequence[AggregateRootT]
75
+ ) -> Sequence[AggregateRootT]: ...
76
+
77
+ @abstractmethod
78
+ async def delete(self, aggregate: AggregateRootT) -> AggregateRootT: ...
79
+
80
+ @abstractmethod
81
+ async def delete_all(
82
+ self, aggregates: Sequence[AggregateRootT]
83
+ ) -> Sequence[AggregateRootT]: ...
@@ -0,0 +1,93 @@
1
+ from abc import ABC, abstractmethod
2
+ from types import TracebackType
3
+ from typing import Self, final
4
+
5
+ from spakky.core.common.interfaces.disposable import IAsyncDisposable, IDisposable
6
+
7
+
8
+ class AbstractTransaction(IDisposable, ABC):
9
+ autocommit_enabled: bool
10
+
11
+ def __init__(self, autocommit: bool = True) -> None:
12
+ self.autocommit_enabled = autocommit
13
+
14
+ @final
15
+ def __enter__(self) -> Self:
16
+ self.initialize()
17
+ return self
18
+
19
+ @final
20
+ def __exit__(
21
+ self,
22
+ __exc_type: type[BaseException] | None,
23
+ __exc_value: BaseException | None,
24
+ __traceback: TracebackType | None,
25
+ ) -> bool | None:
26
+ if __exc_value is not None:
27
+ self.rollback()
28
+ self.dispose()
29
+ return
30
+ try:
31
+ if self.autocommit_enabled:
32
+ self.commit()
33
+ except:
34
+ self.rollback()
35
+ raise
36
+ finally:
37
+ self.dispose()
38
+
39
+ @abstractmethod
40
+ def initialize(self) -> None: ...
41
+
42
+ @abstractmethod
43
+ def dispose(self) -> None: ...
44
+
45
+ @abstractmethod
46
+ def commit(self) -> None: ...
47
+
48
+ @abstractmethod
49
+ def rollback(self) -> None: ...
50
+
51
+
52
+ class AbstractAsyncTransaction(IAsyncDisposable, ABC):
53
+ autocommit_enabled: bool
54
+
55
+ def __init__(self, autocommit: bool = True) -> None:
56
+ self.autocommit_enabled = autocommit
57
+
58
+ @final
59
+ async def __aenter__(self) -> Self:
60
+ await self.initialize()
61
+ return self
62
+
63
+ @final
64
+ async def __aexit__(
65
+ self,
66
+ __exc_type: type[BaseException] | None,
67
+ __exc_value: BaseException | None,
68
+ __traceback: TracebackType | None,
69
+ ) -> bool | None:
70
+ if __exc_value is not None:
71
+ await self.rollback()
72
+ await self.dispose()
73
+ return
74
+ try:
75
+ if self.autocommit_enabled:
76
+ await self.commit()
77
+ except:
78
+ await self.rollback()
79
+ raise
80
+ finally:
81
+ await self.dispose()
82
+
83
+ @abstractmethod
84
+ async def initialize(self) -> None: ...
85
+
86
+ @abstractmethod
87
+ async def dispose(self) -> None: ...
88
+
89
+ @abstractmethod
90
+ async def commit(self) -> None: ...
91
+
92
+ @abstractmethod
93
+ async def rollback(self) -> None: ...
@@ -0,0 +1,20 @@
1
+ """Repository stereotype for data access layer.
2
+
3
+ This module provides @Repository stereotype for organizing classes
4
+ that handle data persistence and retrieval.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+
9
+ from spakky.core.pod.annotations.pod import Pod
10
+
11
+
12
+ @dataclass(eq=False)
13
+ class Repository(Pod):
14
+ """Stereotype for repository classes handling data access.
15
+
16
+ Repositories provide an abstraction over data sources,
17
+ implementing data access patterns and queries.
18
+ """
19
+
20
+ ...