hexkit 3.8.1__tar.gz → 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.
- {hexkit-3.8.1/src/hexkit.egg-info → hexkit-4.0.0}/PKG-INFO +10 -10
- {hexkit-3.8.1 → hexkit-4.0.0}/pyproject.toml +9 -9
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/custom_types.py +23 -5
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/protocols/dao.py +74 -217
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/protocols/daopub.py +4 -3
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/protocols/eventpub.py +34 -11
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/protocols/eventsub.py +75 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/protocols/objstorage.py +0 -1
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/akafka/config.py +40 -5
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/akafka/provider/__init__.py +2 -2
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/akafka/provider/daosub.py +18 -3
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/akafka/provider/eventpub.py +49 -13
- hexkit-4.0.0/src/hexkit/providers/akafka/provider/eventsub.py +553 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/akafka/testcontainer.py +29 -65
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/akafka/testutils.py +22 -8
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/mongodb/provider.py +138 -191
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/mongodb/testutils.py +3 -4
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/mongokafka/provider.py +106 -62
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/s3/provider.py +3 -3
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/s3/testutils/_fixtures.py +4 -1
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/testing/eventpub.py +8 -1
- {hexkit-3.8.1 → hexkit-4.0.0/src/hexkit.egg-info}/PKG-INFO +10 -10
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit.egg-info/requires.txt +8 -8
- hexkit-3.8.1/src/hexkit/providers/akafka/provider/eventsub.py +0 -268
- {hexkit-3.8.1 → hexkit-4.0.0}/LICENSE +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/MANIFEST.in +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/README.md +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/setup.cfg +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/__init__.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/__main__.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/base.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/config.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/correlation.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/log.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/protocols/__init__.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/protocols/daosub.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/__init__.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/akafka/__init__.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/s3/__init__.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/providers/testing/__init__.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/py.typed +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit/utils.py +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-3.8.1 → hexkit-4.0.0}/src/hexkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: hexkit
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: A Toolkit for Building Microservices using the Hexagonal Architecture
|
|
5
5
|
Author-email: "German Human Genome Phenome Archive (GHGA)" <contact@ghga.de>
|
|
6
6
|
License: Apache 2.0
|
|
@@ -20,24 +20,24 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: pydantic<3,>=2
|
|
22
22
|
Requires-Dist: pydantic_settings<3,>=2
|
|
23
|
-
Requires-Dist: PyYAML<7,>=6
|
|
23
|
+
Requires-Dist: PyYAML<7,>=6
|
|
24
24
|
Provides-Extra: akafka
|
|
25
|
-
Requires-Dist: aiokafka~=0.
|
|
26
|
-
Requires-Dist: jsonschema<5,>=4.
|
|
25
|
+
Requires-Dist: aiokafka~=0.12.0; extra == "akafka"
|
|
26
|
+
Requires-Dist: jsonschema<5,>=4.23; extra == "akafka"
|
|
27
27
|
Provides-Extra: s3
|
|
28
|
-
Requires-Dist: boto3<2,>=1.
|
|
29
|
-
Requires-Dist: botocore<2,>=1.
|
|
28
|
+
Requires-Dist: boto3<2,>=1.35.80; extra == "s3"
|
|
29
|
+
Requires-Dist: botocore<2,>=1.35.80; extra == "s3"
|
|
30
30
|
Provides-Extra: mongodb
|
|
31
31
|
Requires-Dist: motor<4,>=3.5.0; extra == "mongodb"
|
|
32
32
|
Provides-Extra: test-akafka
|
|
33
33
|
Requires-Dist: hexkit[akafka]; extra == "test-akafka"
|
|
34
|
-
Requires-Dist: testcontainers[kafka]<5,>=4.
|
|
34
|
+
Requires-Dist: testcontainers[kafka]<5,>=4.9.0; extra == "test-akafka"
|
|
35
35
|
Provides-Extra: test-s3
|
|
36
36
|
Requires-Dist: hexkit[s3]; extra == "test-s3"
|
|
37
|
-
Requires-Dist: testcontainers<5,>=4.
|
|
37
|
+
Requires-Dist: testcontainers<5,>=4.9.0; extra == "test-s3"
|
|
38
38
|
Provides-Extra: test-mongodb
|
|
39
39
|
Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
|
|
40
|
-
Requires-Dist: testcontainers[mongo]<5,>=4.
|
|
40
|
+
Requires-Dist: testcontainers[mongo]<5,>=4.9.0; extra == "test-mongodb"
|
|
41
41
|
Provides-Extra: test
|
|
42
42
|
Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
|
|
43
43
|
Provides-Extra: all
|
|
@@ -23,12 +23,12 @@ classifiers = [
|
|
|
23
23
|
"Intended Audience :: Developers",
|
|
24
24
|
]
|
|
25
25
|
name = "hexkit"
|
|
26
|
-
version = "
|
|
26
|
+
version = "4.0.0"
|
|
27
27
|
description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
|
|
28
28
|
dependencies = [
|
|
29
29
|
"pydantic >=2, <3",
|
|
30
30
|
"pydantic_settings >=2, <3",
|
|
31
|
-
"PyYAML >=6
|
|
31
|
+
"PyYAML >=6, <7",
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
[project.license]
|
|
@@ -36,27 +36,27 @@ text = "Apache 2.0"
|
|
|
36
36
|
|
|
37
37
|
[project.optional-dependencies]
|
|
38
38
|
akafka = [
|
|
39
|
-
"aiokafka~=0.
|
|
40
|
-
"jsonschema >=4.
|
|
39
|
+
"aiokafka~=0.12.0",
|
|
40
|
+
"jsonschema >=4.23, <5",
|
|
41
41
|
]
|
|
42
42
|
s3 = [
|
|
43
|
-
"boto3 >=1.
|
|
44
|
-
"botocore >=1.
|
|
43
|
+
"boto3 >=1.35.80, <2",
|
|
44
|
+
"botocore >=1.35.80, <2",
|
|
45
45
|
]
|
|
46
46
|
mongodb = [
|
|
47
47
|
"motor >=3.5.0, <4",
|
|
48
48
|
]
|
|
49
49
|
test-akafka = [
|
|
50
50
|
"hexkit[akafka]",
|
|
51
|
-
"testcontainers[kafka] >=4.
|
|
51
|
+
"testcontainers[kafka] >=4.9.0, <5",
|
|
52
52
|
]
|
|
53
53
|
test-s3 = [
|
|
54
54
|
"hexkit[s3]",
|
|
55
|
-
"testcontainers >=4.
|
|
55
|
+
"testcontainers >=4.9.0, <5",
|
|
56
56
|
]
|
|
57
57
|
test-mongodb = [
|
|
58
58
|
"hexkit[mongodb]",
|
|
59
|
-
"testcontainers[mongo] >=4.
|
|
59
|
+
"testcontainers[mongo] >=4.9.0, <5",
|
|
60
60
|
]
|
|
61
61
|
test = [
|
|
62
62
|
"hexkit[test-akafka,test-s3,test-mongodb]",
|
|
@@ -17,22 +17,40 @@
|
|
|
17
17
|
"""Module collecting custom types."""
|
|
18
18
|
|
|
19
19
|
from collections.abc import Mapping, Sequence
|
|
20
|
+
from datetime import date, datetime
|
|
20
21
|
from typing import Any, Literal, Union
|
|
22
|
+
from uuid import UUID
|
|
21
23
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
+
# A type for fields that can be used as identifiers (primary keys).
|
|
25
|
+
ID = Union[int, str, UUID]
|
|
26
|
+
|
|
27
|
+
# This is intended to type objects that are assumed to be JSON serializable.
|
|
28
|
+
# (Scalar types as well as arrays are excluded from the above assumption,
|
|
29
|
+
# and we serialize date, datetime and UUID objects to JSON as strings.)
|
|
24
30
|
JsonObject = Mapping[
|
|
25
|
-
str,
|
|
31
|
+
str,
|
|
32
|
+
Union[
|
|
33
|
+
int,
|
|
34
|
+
float,
|
|
35
|
+
str,
|
|
36
|
+
bool,
|
|
37
|
+
date,
|
|
38
|
+
datetime,
|
|
39
|
+
UUID,
|
|
40
|
+
Sequence[Any],
|
|
41
|
+
Mapping[str, Any],
|
|
42
|
+
None,
|
|
43
|
+
],
|
|
26
44
|
]
|
|
27
45
|
|
|
28
46
|
|
|
29
47
|
# A type indicating that a string should be ascii-compatible.
|
|
30
|
-
# Technically it is an alias for `str` so it only serves
|
|
48
|
+
# Technically it is an alias for `str` so it only serves documentation purposes.
|
|
31
49
|
Ascii = str
|
|
32
50
|
|
|
33
51
|
|
|
34
52
|
# A AsyncConstructable is a class with a async (class-)method `construct` that is used when
|
|
35
|
-
# asynchronous
|
|
53
|
+
# asynchronous construction/instantiation logic is needed (which cannot be handled in
|
|
36
54
|
# a synchronous __init__ method).
|
|
37
55
|
# With the current typing features of Python, it seems not possible to correctly type
|
|
38
56
|
# a class with that signature.
|
|
@@ -21,53 +21,53 @@ with the database.
|
|
|
21
21
|
# ruff: noqa: PLR0913
|
|
22
22
|
|
|
23
23
|
import typing
|
|
24
|
-
import warnings
|
|
25
24
|
from abc import ABC, abstractmethod
|
|
26
|
-
from collections.abc import
|
|
25
|
+
from collections.abc import AsyncIterator, Collection, Mapping
|
|
27
26
|
from contextlib import AbstractAsyncContextManager
|
|
28
|
-
from
|
|
29
|
-
from typing import Any, Optional, TypeVar
|
|
27
|
+
from functools import partial
|
|
28
|
+
from typing import Any, Optional, TypeVar
|
|
30
29
|
from uuid import uuid4
|
|
31
30
|
|
|
32
|
-
from pydantic import BaseModel
|
|
33
|
-
from typing_extensions import deprecated
|
|
31
|
+
from pydantic import BaseModel, Field
|
|
34
32
|
|
|
33
|
+
from hexkit.custom_types import ID
|
|
35
34
|
from hexkit.utils import FieldNotInModelError, validate_fields_in_model
|
|
36
35
|
|
|
37
36
|
__all__ = [
|
|
37
|
+
"Dao",
|
|
38
38
|
"DaoFactoryProtocol",
|
|
39
|
-
"DaoNaturalId",
|
|
40
|
-
"DaoSurrogateId",
|
|
41
39
|
"FindError",
|
|
42
40
|
"MultipleHitsFoundError",
|
|
43
41
|
"ResourceAlreadyExistsError",
|
|
44
42
|
"ResourceNotFoundError",
|
|
45
|
-
"
|
|
43
|
+
"UUID4Field",
|
|
46
44
|
]
|
|
47
45
|
|
|
48
|
-
# Type
|
|
46
|
+
# Type variable for handling Data Transfer Objects:
|
|
49
47
|
Dto = TypeVar("Dto", bound=BaseModel)
|
|
50
|
-
DtoCreation = TypeVar("DtoCreation", bound=BaseModel)
|
|
51
|
-
DtoCreation_contra = TypeVar("DtoCreation_contra", bound=BaseModel, contravariant=True)
|
|
52
48
|
|
|
53
49
|
|
|
54
|
-
class
|
|
50
|
+
class DaoError(RuntimeError):
|
|
51
|
+
"""Base for all errors related to DAO operations."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ResourceNotFoundError(DaoError):
|
|
55
55
|
"""Raised when a requested resource did not exist."""
|
|
56
56
|
|
|
57
|
-
def __init__(self, *, id_:
|
|
57
|
+
def __init__(self, *, id_: ID):
|
|
58
58
|
message = f'The resource with the id "{id_}" does not exist.'
|
|
59
59
|
super().__init__(message)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
class ResourceAlreadyExistsError(
|
|
62
|
+
class ResourceAlreadyExistsError(DaoError):
|
|
63
63
|
"""Raised when a resource did unexpectedly exist."""
|
|
64
64
|
|
|
65
|
-
def __init__(self, *, id_:
|
|
65
|
+
def __init__(self, *, id_: ID):
|
|
66
66
|
message = f'The resource with the id "{id_}" already exists.'
|
|
67
67
|
super().__init__(message)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
class FindError(
|
|
70
|
+
class FindError(DaoError):
|
|
71
71
|
"""Base for all error related to DAO find operations."""
|
|
72
72
|
|
|
73
73
|
|
|
@@ -101,12 +101,35 @@ class NoHitsFoundError(FindError):
|
|
|
101
101
|
super().__init__(message)
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
class
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
class DbTimeoutError(DaoError):
|
|
105
|
+
"""Raised when a database operation timed out."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, details: str):
|
|
108
|
+
message = f"The database operation timed out: {details}"
|
|
109
|
+
super().__init__(message)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# provide standardized default factory for UUID4 fields
|
|
113
|
+
UUID4Field = partial(Field, default_factory=uuid4)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Dao(typing.Protocol[Dto]):
|
|
117
|
+
"""A duck type with methods common to all DAOs."""
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def with_transaction(cls) -> AbstractAsyncContextManager["Dao[Dto]"]:
|
|
121
|
+
"""Creates a transaction manager that uses an async context manager interface:
|
|
122
|
+
|
|
123
|
+
Upon __aenter__, pens a new transactional scope. Returns a transaction-scoped
|
|
124
|
+
DAO.
|
|
108
125
|
|
|
109
|
-
|
|
126
|
+
Upon __aexit__, closes the transactional scope. A full rollback of the
|
|
127
|
+
transaction is performed in case of an exception. Otherwise, the changes to the
|
|
128
|
+
database are committed and flushed.
|
|
129
|
+
"""
|
|
130
|
+
...
|
|
131
|
+
|
|
132
|
+
async def get_by_id(self, id_: ID) -> Dto:
|
|
110
133
|
"""Get a resource by providing its ID.
|
|
111
134
|
|
|
112
135
|
Args:
|
|
@@ -134,7 +157,7 @@ class DaoCommons(typing.Protocol[Dto]):
|
|
|
134
157
|
"""
|
|
135
158
|
...
|
|
136
159
|
|
|
137
|
-
async def delete(self, id_:
|
|
160
|
+
async def delete(self, id_: ID) -> None:
|
|
138
161
|
"""Delete a resource by providing its ID.
|
|
139
162
|
|
|
140
163
|
Args:
|
|
@@ -146,9 +169,15 @@ class DaoCommons(typing.Protocol[Dto]):
|
|
|
146
169
|
...
|
|
147
170
|
|
|
148
171
|
async def find_one(self, *, mapping: Mapping[str, Any]) -> Dto:
|
|
149
|
-
"""Find the resource that matches the specified mapping.
|
|
150
|
-
|
|
151
|
-
|
|
172
|
+
"""Find the resource that matches the specified mapping.
|
|
173
|
+
|
|
174
|
+
It is expected that at most one resource matches the constraints.
|
|
175
|
+
An exception is raised if no or multiple hits are found.
|
|
176
|
+
|
|
177
|
+
The values in the mapping are used to filter the resources, these are
|
|
178
|
+
assumed to be standard JSON scalar types. Particularly, UUIDs and datetimes
|
|
179
|
+
must be represented as strings. The behavior for non-scalars types depends
|
|
180
|
+
on the specific provider.
|
|
152
181
|
|
|
153
182
|
Args:
|
|
154
183
|
mapping:
|
|
@@ -170,6 +199,11 @@ class DaoCommons(typing.Protocol[Dto]):
|
|
|
170
199
|
def find_all(self, *, mapping: Mapping[str, Any]) -> AsyncIterator[Dto]:
|
|
171
200
|
"""Find all resources that match the specified mapping.
|
|
172
201
|
|
|
202
|
+
The values in the mapping are used to filter the resources, these are
|
|
203
|
+
assumed to be standard JSON scalar types. Particularly, UUIDs and datetimes
|
|
204
|
+
must be represented as strings. The behavior for non-scalar types depends
|
|
205
|
+
on the specific provider.
|
|
206
|
+
|
|
173
207
|
Args:
|
|
174
208
|
mapping:
|
|
175
209
|
A mapping where the keys correspond to the names of resource fields
|
|
@@ -181,61 +215,6 @@ class DaoCommons(typing.Protocol[Dto]):
|
|
|
181
215
|
"""
|
|
182
216
|
...
|
|
183
217
|
|
|
184
|
-
|
|
185
|
-
class DaoSurrogateId(DaoCommons[Dto], typing.Protocol[Dto, DtoCreation_contra]):
|
|
186
|
-
"""A duck type of a DAO that generates an internal/surrogate key for
|
|
187
|
-
identifying resources in the database. ID/keys cannot be defined by the client of
|
|
188
|
-
the DAO. Thus, both a standard DTO model (first type parameter), which includes
|
|
189
|
-
the key field, as well as special DTO model (second type parameter), which is
|
|
190
|
-
identical to the first one, but does not include the ID field and is dedicated for
|
|
191
|
-
creation of new resources.
|
|
192
|
-
"""
|
|
193
|
-
|
|
194
|
-
@classmethod
|
|
195
|
-
def with_transaction(
|
|
196
|
-
cls,
|
|
197
|
-
) -> AbstractAsyncContextManager["DaoSurrogateId[Dto, DtoCreation_contra]"]:
|
|
198
|
-
"""Creates a transaction manager that uses an async context manager interface:
|
|
199
|
-
|
|
200
|
-
Upon __aenter__, pens a new transactional scope. Returns a transaction-scoped
|
|
201
|
-
DAO.
|
|
202
|
-
|
|
203
|
-
Upon __aexit__, closes the transactional scope. A full rollback of the
|
|
204
|
-
transaction is performed in case of an exception. Otherwise, the changes to the
|
|
205
|
-
database are committed and flushed.
|
|
206
|
-
"""
|
|
207
|
-
...
|
|
208
|
-
|
|
209
|
-
async def insert(self, dto: DtoCreation_contra) -> Dto:
|
|
210
|
-
"""Create a new resource.
|
|
211
|
-
|
|
212
|
-
Args:
|
|
213
|
-
dto:
|
|
214
|
-
Resource content as a pydantic-based data transfer object without the
|
|
215
|
-
resource ID (which will be set automatically).
|
|
216
|
-
|
|
217
|
-
Returns:
|
|
218
|
-
Returns a copy of the newly inserted resource including its assigned ID.
|
|
219
|
-
"""
|
|
220
|
-
...
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
class DaoNaturalId(DaoCommons[Dto], typing.Protocol[Dto]):
|
|
224
|
-
"""A duck type of a DAO that uses a natural resource ID provided by the client."""
|
|
225
|
-
|
|
226
|
-
@classmethod
|
|
227
|
-
def with_transaction(cls) -> AbstractAsyncContextManager["DaoNaturalId[Dto]"]:
|
|
228
|
-
"""Creates a transaction manager that uses an async context manager interface:
|
|
229
|
-
|
|
230
|
-
Upon __aenter__, pens a new transactional scope. Returns a transaction-scoped
|
|
231
|
-
DAO.
|
|
232
|
-
|
|
233
|
-
Upon __aexit__, closes the transactional scope. A full rollback of the
|
|
234
|
-
transaction is performed in case of an exception. Otherwise, the changes to the
|
|
235
|
-
database are committed and flushed.
|
|
236
|
-
"""
|
|
237
|
-
...
|
|
238
|
-
|
|
239
218
|
async def insert(self, dto: Dto) -> None:
|
|
240
219
|
"""Create a new resource.
|
|
241
220
|
|
|
@@ -261,28 +240,14 @@ class DaoNaturalId(DaoCommons[Dto], typing.Protocol[Dto]):
|
|
|
261
240
|
...
|
|
262
241
|
|
|
263
242
|
|
|
264
|
-
@deprecated(
|
|
265
|
-
"uuid4_id_generator is deprecated as of v3.6 and will be removed in hexkit v4."
|
|
266
|
-
)
|
|
267
|
-
async def uuid4_id_generator() -> AsyncGenerator[str, None]:
|
|
268
|
-
"""Generates a new ID using the UUID4 algorithm.
|
|
269
|
-
This is an AsyncGenerator to be compliant with the id_generator requirements of the
|
|
270
|
-
DaoFactoryProtocol.
|
|
271
|
-
"""
|
|
272
|
-
while True:
|
|
273
|
-
yield str(uuid4())
|
|
274
|
-
|
|
275
|
-
|
|
276
243
|
class DaoFactoryBase:
|
|
277
244
|
"""A base for Data Access Objects (DAO) Factory protocols."""
|
|
278
245
|
|
|
279
|
-
class IdFieldNotFoundError(
|
|
246
|
+
class IdFieldNotFoundError(TypeError):
|
|
280
247
|
"""Raised when the dto_model did not contain the expected id_field."""
|
|
281
248
|
|
|
282
|
-
class
|
|
283
|
-
"""Raised when the
|
|
284
|
-
DTO model.
|
|
285
|
-
"""
|
|
249
|
+
class IdTypeNotSupportedError(TypeError):
|
|
250
|
+
"""Raised when the id_field of the dto_model has an unexpected Type."""
|
|
286
251
|
|
|
287
252
|
class IndexFieldsInvalidError(ValueError):
|
|
288
253
|
"""Raised when providing an invalid list of fields to index."""
|
|
@@ -292,31 +257,13 @@ class DaoFactoryBase:
|
|
|
292
257
|
"""Checks whether the dto_model contains the expected id_field.
|
|
293
258
|
Raises IdFieldNotFoundError otherwise.
|
|
294
259
|
"""
|
|
295
|
-
|
|
260
|
+
properties = dto_model.model_json_schema()["properties"]
|
|
261
|
+
schema = properties.get(id_field)
|
|
262
|
+
if schema is None:
|
|
296
263
|
raise cls.IdFieldNotFoundError()
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
cls,
|
|
301
|
-
*,
|
|
302
|
-
dto_model: type[Dto],
|
|
303
|
-
dto_creation_model: Optional[type[DtoCreation]],
|
|
304
|
-
id_field: str,
|
|
305
|
-
) -> None:
|
|
306
|
-
"""Checks that the dto_creation_model has the same fields as the dto_model
|
|
307
|
-
except missing the ID. Raises CreationModelInvalidError otherwise.
|
|
308
|
-
"""
|
|
309
|
-
if dto_creation_model is None:
|
|
310
|
-
return
|
|
311
|
-
|
|
312
|
-
expected_properties = copy(dto_model.model_json_schema()["properties"])
|
|
313
|
-
# (the schema method returns an attribute of the class, making a copy to not
|
|
314
|
-
# alter the class)
|
|
315
|
-
del expected_properties[id_field]
|
|
316
|
-
observed_properties = dto_creation_model.model_json_schema()["properties"]
|
|
317
|
-
|
|
318
|
-
if observed_properties != expected_properties:
|
|
319
|
-
raise cls.CreationModelInvalidError()
|
|
264
|
+
id_type = schema.get("type")
|
|
265
|
+
if id_type not in ("integer", "string"):
|
|
266
|
+
raise cls.IdTypeNotSupportedError()
|
|
320
267
|
|
|
321
268
|
@classmethod
|
|
322
269
|
def _validate_fields_to_index(
|
|
@@ -344,16 +291,10 @@ class DaoFactoryBase:
|
|
|
344
291
|
*,
|
|
345
292
|
dto_model: type[Dto],
|
|
346
293
|
id_field: str,
|
|
347
|
-
dto_creation_model: Optional[type[DtoCreation]],
|
|
348
294
|
fields_to_index: Optional[Collection[str]],
|
|
349
295
|
) -> None:
|
|
350
296
|
"""Validates the input parameters of the get_dao method."""
|
|
351
297
|
cls._validate_dto_model_id(dto_model=dto_model, id_field=id_field)
|
|
352
|
-
cls._validate_dto_creation_model(
|
|
353
|
-
dto_model=dto_model,
|
|
354
|
-
dto_creation_model=dto_creation_model,
|
|
355
|
-
id_field=id_field,
|
|
356
|
-
)
|
|
357
298
|
cls._validate_fields_to_index(
|
|
358
299
|
dto_model=dto_model, fields_to_index=fields_to_index
|
|
359
300
|
)
|
|
@@ -364,39 +305,14 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
364
305
|
that are enclosed in transactional scopes.
|
|
365
306
|
"""
|
|
366
307
|
|
|
367
|
-
@overload
|
|
368
|
-
async def get_dao(
|
|
369
|
-
self,
|
|
370
|
-
*,
|
|
371
|
-
name: str,
|
|
372
|
-
dto_model: type[Dto],
|
|
373
|
-
id_field: str,
|
|
374
|
-
fields_to_index: Optional[Collection[str]] = None,
|
|
375
|
-
id_generator: Optional[AsyncGenerator[str, None]] = None,
|
|
376
|
-
) -> DaoNaturalId[Dto]: ...
|
|
377
|
-
|
|
378
|
-
@overload
|
|
379
308
|
async def get_dao(
|
|
380
309
|
self,
|
|
381
310
|
*,
|
|
382
311
|
name: str,
|
|
383
312
|
dto_model: type[Dto],
|
|
384
313
|
id_field: str,
|
|
385
|
-
dto_creation_model: type[DtoCreation],
|
|
386
314
|
fields_to_index: Optional[Collection[str]] = None,
|
|
387
|
-
|
|
388
|
-
) -> DaoSurrogateId[Dto, DtoCreation]: ...
|
|
389
|
-
|
|
390
|
-
async def get_dao(
|
|
391
|
-
self,
|
|
392
|
-
*,
|
|
393
|
-
name: str,
|
|
394
|
-
dto_model: type[Dto],
|
|
395
|
-
id_field: str,
|
|
396
|
-
dto_creation_model: Optional[type[DtoCreation]] = None,
|
|
397
|
-
fields_to_index: Optional[Collection[str]] = None,
|
|
398
|
-
id_generator: Optional[AsyncGenerator[str, None]] = None,
|
|
399
|
-
) -> Union[DaoSurrogateId[Dto, DtoCreation], DaoNaturalId[Dto]]:
|
|
315
|
+
) -> Dao[Dto]:
|
|
400
316
|
"""Constructs a DAO for interacting with resources in a database.
|
|
401
317
|
|
|
402
318
|
Args:
|
|
@@ -411,85 +327,28 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
411
327
|
fields_to_index:
|
|
412
328
|
Optionally, provide any fields that should be indexed in addition to the
|
|
413
329
|
`id_field`. Defaults to None.
|
|
414
|
-
dto_creation_model:
|
|
415
|
-
An optional DTO model specific for creation of a new resource. This
|
|
416
|
-
model has to be identical to the `dto_model` except that it has to miss
|
|
417
|
-
the `id_field`. If specified, the resource ID will be generated by the DAO
|
|
418
|
-
implementation upon resource creation. Otherwise (if set to None), resource IDs
|
|
419
|
-
have to be specified upon resource creation. Defaults to None.
|
|
420
|
-
id_generator:
|
|
421
|
-
A generator that yields strings that will be used as IDs when creating
|
|
422
|
-
new resources. Please note, each ID should be unique. Moreover, the
|
|
423
|
-
generator should never exhaust.
|
|
424
|
-
By default a UUID4-based generator is used.
|
|
425
330
|
Returns:
|
|
426
|
-
|
|
427
|
-
DaoSurrogateID, which autogenerates IDs upon resource creation, is returned.
|
|
428
|
-
Otherwise, returns a DAO of type DaoNaturalId, which require ID
|
|
429
|
-
specification upon resource creation.
|
|
331
|
+
A DAO specific to the provided DTO model.
|
|
430
332
|
|
|
431
333
|
Raises:
|
|
432
|
-
self.CreationModelInvalidError:
|
|
433
|
-
Raised when the DtoCreationModel was invalid in relation to the main
|
|
434
|
-
DTO model.
|
|
435
334
|
self.IdFieldNotFoundError:
|
|
436
335
|
Raised when the dto_model did not contain the expected id_field.
|
|
336
|
+
self.IdTypeNotSupportedError:
|
|
337
|
+
Raised when the id_field of the dto_model has an unexpected type.
|
|
437
338
|
"""
|
|
438
339
|
self._validate(
|
|
439
340
|
dto_model=dto_model,
|
|
440
341
|
id_field=id_field,
|
|
441
|
-
dto_creation_model=dto_creation_model,
|
|
442
342
|
fields_to_index=fields_to_index,
|
|
443
343
|
)
|
|
444
344
|
|
|
445
|
-
if id_generator is None:
|
|
446
|
-
# instantiate the default ID generator:
|
|
447
|
-
id_generator = uuid4_id_generator()
|
|
448
|
-
|
|
449
|
-
if dto_creation_model is not None:
|
|
450
|
-
dep_msg = (
|
|
451
|
-
"The DAO API (and existing providers) are deprecated as of v3.6."
|
|
452
|
-
+ " They will be simplified in hexkit v4."
|
|
453
|
-
+ " Please plan to provide IDs during resource creation. The same model"
|
|
454
|
-
+ " should be used for both creation and retrieval."
|
|
455
|
-
)
|
|
456
|
-
warnings.warn(dep_msg, category=DeprecationWarning, stacklevel=2)
|
|
457
|
-
|
|
458
345
|
return await self._get_dao(
|
|
459
346
|
name=name,
|
|
460
347
|
dto_model=dto_model,
|
|
461
348
|
id_field=id_field,
|
|
462
349
|
fields_to_index=fields_to_index,
|
|
463
|
-
dto_creation_model=dto_creation_model,
|
|
464
|
-
id_generator=id_generator,
|
|
465
350
|
)
|
|
466
351
|
|
|
467
|
-
@overload
|
|
468
|
-
@abstractmethod
|
|
469
|
-
async def _get_dao(
|
|
470
|
-
self,
|
|
471
|
-
*,
|
|
472
|
-
name: str,
|
|
473
|
-
dto_model: type[Dto],
|
|
474
|
-
id_field: str,
|
|
475
|
-
dto_creation_model: None,
|
|
476
|
-
fields_to_index: Optional[Collection[str]],
|
|
477
|
-
id_generator: AsyncGenerator[str, None],
|
|
478
|
-
) -> DaoNaturalId[Dto]: ...
|
|
479
|
-
|
|
480
|
-
@overload
|
|
481
|
-
@abstractmethod
|
|
482
|
-
async def _get_dao(
|
|
483
|
-
self,
|
|
484
|
-
*,
|
|
485
|
-
name: str,
|
|
486
|
-
dto_model: type[Dto],
|
|
487
|
-
id_field: str,
|
|
488
|
-
dto_creation_model: type[DtoCreation],
|
|
489
|
-
fields_to_index: Optional[Collection[str]],
|
|
490
|
-
id_generator: AsyncGenerator[str, None],
|
|
491
|
-
) -> DaoSurrogateId[Dto, DtoCreation]: ...
|
|
492
|
-
|
|
493
352
|
@abstractmethod
|
|
494
353
|
async def _get_dao(
|
|
495
354
|
self,
|
|
@@ -497,10 +356,8 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
497
356
|
name: str,
|
|
498
357
|
dto_model: type[Dto],
|
|
499
358
|
id_field: str,
|
|
500
|
-
dto_creation_model: Optional[type[DtoCreation]],
|
|
501
359
|
fields_to_index: Optional[Collection[str]],
|
|
502
|
-
|
|
503
|
-
) -> Union[DaoSurrogateId[Dto, DtoCreation], DaoNaturalId[Dto]]:
|
|
360
|
+
) -> Dao[Dto]:
|
|
504
361
|
"""*To be implemented by the provider. Input validation is done outside of this
|
|
505
362
|
method.*
|
|
506
363
|
"""
|
|
@@ -28,13 +28,13 @@ from typing import Callable, Optional
|
|
|
28
28
|
|
|
29
29
|
from hexkit.custom_types import JsonObject
|
|
30
30
|
from hexkit.protocols.dao import (
|
|
31
|
+
Dao,
|
|
31
32
|
DaoFactoryBase,
|
|
32
|
-
DaoNaturalId,
|
|
33
33
|
Dto,
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
class DaoPublisher(
|
|
37
|
+
class DaoPublisher(Dao[Dto], typing.Protocol[Dto]):
|
|
38
38
|
"""A Data Access Object (DAO) that automatically publishes changes according to the
|
|
39
39
|
outbox pattern.
|
|
40
40
|
"""
|
|
@@ -95,11 +95,12 @@ class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
95
95
|
Raises:
|
|
96
96
|
self.IdFieldNotFoundError:
|
|
97
97
|
Raised when the dto_model did not contain the expected id_field.
|
|
98
|
+
self.IdTypeNotSupportedError:
|
|
99
|
+
Raised when the id_field of the dto_model has an unexpected type.
|
|
98
100
|
"""
|
|
99
101
|
self._validate(
|
|
100
102
|
dto_model=dto_model,
|
|
101
103
|
id_field=id_field,
|
|
102
|
-
dto_creation_model=None,
|
|
103
104
|
fields_to_index=fields_to_index,
|
|
104
105
|
)
|
|
105
106
|
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"""Protocol related to event publishing."""
|
|
18
18
|
|
|
19
19
|
from abc import ABC, abstractmethod
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
from typing import Optional
|
|
20
22
|
|
|
21
23
|
from hexkit.custom_types import Ascii, JsonObject
|
|
22
24
|
from hexkit.utils import check_ascii
|
|
@@ -26,31 +28,52 @@ class EventPublisherProtocol(ABC):
|
|
|
26
28
|
"""A protocol for publishing events to an event broker."""
|
|
27
29
|
|
|
28
30
|
async def publish(
|
|
29
|
-
self,
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
payload: JsonObject,
|
|
34
|
+
type_: Ascii,
|
|
35
|
+
key: Ascii,
|
|
36
|
+
topic: Ascii,
|
|
37
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
30
38
|
) -> None:
|
|
31
39
|
"""Publish an event.
|
|
32
40
|
|
|
33
41
|
Args:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
- `payload` (JSON): The payload to ship with the event.
|
|
43
|
+
- `type_` (str): The event type. ASCII characters only.
|
|
44
|
+
- `key` (str): The event type. ASCII characters only.
|
|
45
|
+
- `topic` (str): The event type. ASCII characters only.
|
|
46
|
+
- `headers`: Additional headers to attach to the event.
|
|
38
47
|
"""
|
|
39
48
|
check_ascii(type_, key, topic)
|
|
49
|
+
if headers is None:
|
|
50
|
+
headers = {}
|
|
51
|
+
|
|
40
52
|
await self._publish_validated(
|
|
41
|
-
payload=payload,
|
|
53
|
+
payload=payload,
|
|
54
|
+
type_=type_,
|
|
55
|
+
key=key,
|
|
56
|
+
topic=topic,
|
|
57
|
+
headers=headers,
|
|
42
58
|
)
|
|
43
59
|
|
|
44
60
|
@abstractmethod
|
|
45
61
|
async def _publish_validated(
|
|
46
|
-
self,
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
payload: JsonObject,
|
|
65
|
+
type_: Ascii,
|
|
66
|
+
key: Ascii,
|
|
67
|
+
topic: Ascii,
|
|
68
|
+
headers: Mapping[str, str],
|
|
47
69
|
) -> None:
|
|
48
70
|
"""Publish an event with already validated topic and type.
|
|
49
71
|
|
|
50
72
|
Args:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
- `payload` (JSON): The payload to ship with the event.
|
|
74
|
+
- `type_` (str): The event type. ASCII characters only.
|
|
75
|
+
- `key` (str): The event type. ASCII characters only.
|
|
76
|
+
- `topic` (str): The event type. ASCII characters only.
|
|
77
|
+
- `headers`: Additional headers to attach to the event.
|
|
55
78
|
"""
|
|
56
79
|
...
|