hexkit 7.2.1__tar.gz → 8.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-7.2.1/src/hexkit.egg-info → hexkit-8.0.0}/PKG-INFO +25 -16
- {hexkit-7.2.1 → hexkit-8.0.0}/pyproject.toml +30 -18
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/dao.py +43 -20
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/daopub.py +9 -17
- hexkit-8.0.0/src/hexkit/protocols/kvstore.py +55 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/objstorage.py +48 -0
- hexkit-8.0.0/src/hexkit/providers/mongodb/__init__.py +43 -0
- hexkit-8.0.0/src/hexkit/providers/mongodb/config.py +55 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongodb/migrations/_manager.py +1 -1
- hexkit-8.0.0/src/hexkit/providers/mongodb/provider/__init__.py +54 -0
- hexkit-8.0.0/src/hexkit/providers/mongodb/provider/client.py +102 -0
- hexkit-7.2.1/src/hexkit/providers/mongodb/provider.py → hexkit-8.0.0/src/hexkit/providers/mongodb/provider/dao.py +92 -169
- hexkit-8.0.0/src/hexkit/providers/mongodb/provider/kvstore.py +230 -0
- hexkit-8.0.0/src/hexkit/providers/mongodb/provider/utils.py +66 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongodb/testutils.py +4 -7
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongokafka/__init__.py +1 -1
- {hexkit-7.2.1/src/hexkit/providers/mongokafka/provider → hexkit-8.0.0/src/hexkit/providers/mongokafka}/config.py +1 -1
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongokafka/provider/__init__.py +2 -3
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongokafka/provider/daopub.py +11 -9
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +6 -1
- hexkit-8.0.0/src/hexkit/providers/redis/__init__.py +35 -0
- hexkit-8.0.0/src/hexkit/providers/redis/config.py +48 -0
- {hexkit-7.2.1/src/hexkit/providers/mongodb → hexkit-8.0.0/src/hexkit/providers/redis/provider}/__init__.py +17 -4
- hexkit-8.0.0/src/hexkit/providers/redis/provider/client.py +53 -0
- hexkit-8.0.0/src/hexkit/providers/redis/provider/kvstore.py +250 -0
- hexkit-8.0.0/src/hexkit/providers/redis/testutils.py +147 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/provider.py +42 -11
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/testutils/_utils.py +5 -5
- hexkit-8.0.0/src/hexkit/providers/testing/dao.py +475 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/testing/s3.py +26 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/utils.py +9 -1
- {hexkit-7.2.1 → hexkit-8.0.0/src/hexkit.egg-info}/PKG-INFO +25 -16
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit.egg-info/SOURCES.txt +14 -2
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit.egg-info/requires.txt +27 -15
- hexkit-7.2.1/src/hexkit/providers/testing/dao.py +0 -146
- {hexkit-7.2.1 → hexkit-8.0.0}/LICENSE +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/MANIFEST.in +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/README.md +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/setup.cfg +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/__init__.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/__main__.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/base.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/config.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/correlation.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/custom_types.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/log.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/opentelemetry.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/__init__.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/daosub.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/eventpub.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/eventsub.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/__init__.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/__init__.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/config.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/provider/eventsub.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/testutils.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongodb/migrations/__init__.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongodb/migrations/_utils.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongodb/migrations/helpers.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/__init__.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/testutils/_fixtures.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/testing/__init__.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/testing/eventpub.py +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/py.typed +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexkit
|
|
3
|
-
Version:
|
|
3
|
+
Version: 8.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
|
|
@@ -18,39 +18,45 @@ Classifier: Intended Audience :: Developers
|
|
|
18
18
|
Requires-Python: >=3.10
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: opentelemetry-api<2,>=1.
|
|
22
|
-
Requires-Dist: pydantic<3,>=2.
|
|
23
|
-
Requires-Dist: pydantic_settings<3,>=2.
|
|
21
|
+
Requires-Dist: opentelemetry-api<2,>=1.39
|
|
22
|
+
Requires-Dist: pydantic<3,>=2.12
|
|
23
|
+
Requires-Dist: pydantic_settings<3,>=2.12
|
|
24
24
|
Requires-Dist: PyYAML<7,>=6
|
|
25
25
|
Provides-Extra: akafka
|
|
26
|
-
Requires-Dist: aiokafka[lz4,snappy,zstd]~=0.
|
|
26
|
+
Requires-Dist: aiokafka[lz4,snappy,zstd]~=0.13.0; extra == "akafka"
|
|
27
27
|
Requires-Dist: jsonschema<5,>=4.25; extra == "akafka"
|
|
28
28
|
Provides-Extra: s3
|
|
29
|
-
Requires-Dist: boto3<2,>=1.
|
|
30
|
-
Requires-Dist: botocore<2,>=1.
|
|
29
|
+
Requires-Dist: boto3<2,>=1.42; extra == "s3"
|
|
30
|
+
Requires-Dist: botocore<2,>=1.42; extra == "s3"
|
|
31
31
|
Provides-Extra: mongodb
|
|
32
32
|
Requires-Dist: pymongo<5,>=4.15; extra == "mongodb"
|
|
33
|
+
Provides-Extra: redis
|
|
34
|
+
Requires-Dist: redis<8,>=7.1; extra == "redis"
|
|
33
35
|
Provides-Extra: opentelemetry-base
|
|
34
|
-
Requires-Dist: opentelemetry-sdk<2,>=1.
|
|
35
|
-
Requires-Dist: opentelemetry-exporter-otlp<2,>=1.
|
|
36
|
-
Requires-Dist: opentelemetry-instrumentation>=0.
|
|
37
|
-
Requires-Dist: opentelemetry-instrumentation-httpx>=0.
|
|
38
|
-
Requires-Dist: opentelemetry-distro>=0.
|
|
36
|
+
Requires-Dist: opentelemetry-sdk<2,>=1.39.1; extra == "opentelemetry-base"
|
|
37
|
+
Requires-Dist: opentelemetry-exporter-otlp<2,>=1.39.1; extra == "opentelemetry-base"
|
|
38
|
+
Requires-Dist: opentelemetry-instrumentation>=0.60b1; extra == "opentelemetry-base"
|
|
39
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.60b1; extra == "opentelemetry-base"
|
|
40
|
+
Requires-Dist: opentelemetry-distro>=0.60b1; extra == "opentelemetry-base"
|
|
39
41
|
Provides-Extra: opentelemetry-akafka
|
|
40
42
|
Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-akafka"
|
|
41
43
|
Requires-Dist: hexkit[akafka]; extra == "opentelemetry-akafka"
|
|
42
|
-
Requires-Dist: opentelemetry-instrumentation-aiokafka>=0.
|
|
44
|
+
Requires-Dist: opentelemetry-instrumentation-aiokafka>=0.60b1; extra == "opentelemetry-akafka"
|
|
43
45
|
Provides-Extra: opentelemetry-mongodb
|
|
44
46
|
Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-mongodb"
|
|
45
47
|
Requires-Dist: hexkit[mongodb]; extra == "opentelemetry-mongodb"
|
|
46
|
-
Requires-Dist: opentelemetry-instrumentation-pymongo>=0.
|
|
48
|
+
Requires-Dist: opentelemetry-instrumentation-pymongo>=0.60b1; extra == "opentelemetry-mongodb"
|
|
47
49
|
Provides-Extra: opentelemetry-fastapi
|
|
48
50
|
Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-fastapi"
|
|
49
|
-
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.
|
|
51
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.60b1; extra == "opentelemetry-fastapi"
|
|
50
52
|
Provides-Extra: opentelemetry-s3
|
|
51
53
|
Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-s3"
|
|
52
54
|
Requires-Dist: hexkit[s3]; extra == "opentelemetry-s3"
|
|
53
|
-
Requires-Dist: opentelemetry-instrumentation-botocore>=0.
|
|
55
|
+
Requires-Dist: opentelemetry-instrumentation-botocore>=0.60b1; extra == "opentelemetry-s3"
|
|
56
|
+
Provides-Extra: opentelemetry-redis
|
|
57
|
+
Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-redis"
|
|
58
|
+
Requires-Dist: hexkit[redis]; extra == "opentelemetry-redis"
|
|
59
|
+
Requires-Dist: opentelemetry-instrumentation-redis>=0.60b1; extra == "opentelemetry-redis"
|
|
54
60
|
Provides-Extra: opentelemetry
|
|
55
61
|
Requires-Dist: hexkit[opentelemetry-akafka]; extra == "opentelemetry"
|
|
56
62
|
Requires-Dist: hexkit[opentelemetry-mongodb]; extra == "opentelemetry"
|
|
@@ -65,6 +71,9 @@ Requires-Dist: testcontainers<5,>=4.13; extra == "test-s3"
|
|
|
65
71
|
Provides-Extra: test-mongodb
|
|
66
72
|
Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
|
|
67
73
|
Requires-Dist: testcontainers[mongo]<5,>=4.13; extra == "test-mongodb"
|
|
74
|
+
Provides-Extra: test-redis
|
|
75
|
+
Requires-Dist: hexkit[redis]; extra == "test-redis"
|
|
76
|
+
Requires-Dist: testcontainers[redis]<5,>=4.13; extra == "test-redis"
|
|
68
77
|
Provides-Extra: test
|
|
69
78
|
Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
|
|
70
79
|
Provides-Extra: all
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[build-system]
|
|
2
2
|
requires = [
|
|
3
|
-
"setuptools>=80.
|
|
3
|
+
"setuptools>=80.10",
|
|
4
4
|
]
|
|
5
5
|
build-backend = "setuptools.build_meta"
|
|
6
6
|
|
|
@@ -23,12 +23,12 @@ classifiers = [
|
|
|
23
23
|
"Intended Audience :: Developers",
|
|
24
24
|
]
|
|
25
25
|
name = "hexkit"
|
|
26
|
-
version = "
|
|
26
|
+
version = "8.0.0"
|
|
27
27
|
description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
|
|
28
28
|
dependencies = [
|
|
29
|
-
"opentelemetry-api >=1.
|
|
30
|
-
"pydantic >=2.
|
|
31
|
-
"pydantic_settings >=2.
|
|
29
|
+
"opentelemetry-api >=1.39, <2",
|
|
30
|
+
"pydantic >=2.12, <3",
|
|
31
|
+
"pydantic_settings >=2.12, <3",
|
|
32
32
|
"PyYAML >=6, <7",
|
|
33
33
|
]
|
|
34
34
|
|
|
@@ -37,41 +37,49 @@ text = "Apache 2.0"
|
|
|
37
37
|
|
|
38
38
|
[project.optional-dependencies]
|
|
39
39
|
akafka = [
|
|
40
|
-
"aiokafka[zstd,lz4,snappy]~=0.
|
|
40
|
+
"aiokafka[zstd,lz4,snappy]~=0.13.0",
|
|
41
41
|
"jsonschema >=4.25, <5",
|
|
42
42
|
]
|
|
43
43
|
s3 = [
|
|
44
|
-
"boto3 >=1.
|
|
45
|
-
"botocore >=1.
|
|
44
|
+
"boto3 >=1.42, <2",
|
|
45
|
+
"botocore >=1.42, <2",
|
|
46
46
|
]
|
|
47
47
|
mongodb = [
|
|
48
48
|
"pymongo >=4.15, <5",
|
|
49
49
|
]
|
|
50
|
+
redis = [
|
|
51
|
+
"redis >=7.1, <8",
|
|
52
|
+
]
|
|
50
53
|
opentelemetry-base = [
|
|
51
|
-
"opentelemetry-sdk >=1.
|
|
52
|
-
"opentelemetry-exporter-otlp >=1.
|
|
53
|
-
"opentelemetry-instrumentation >=0.
|
|
54
|
-
"opentelemetry-instrumentation-httpx >=0.
|
|
55
|
-
"opentelemetry-distro >=0.
|
|
54
|
+
"opentelemetry-sdk >=1.39.1, <2",
|
|
55
|
+
"opentelemetry-exporter-otlp >=1.39.1, <2",
|
|
56
|
+
"opentelemetry-instrumentation >=0.60b1",
|
|
57
|
+
"opentelemetry-instrumentation-httpx >=0.60b1",
|
|
58
|
+
"opentelemetry-distro >=0.60b1",
|
|
56
59
|
]
|
|
57
60
|
opentelemetry-akafka = [
|
|
58
61
|
"hexkit[opentelemetry-base]",
|
|
59
62
|
"hexkit[akafka]",
|
|
60
|
-
"opentelemetry-instrumentation-aiokafka >=0.
|
|
63
|
+
"opentelemetry-instrumentation-aiokafka >=0.60b1",
|
|
61
64
|
]
|
|
62
65
|
opentelemetry-mongodb = [
|
|
63
66
|
"hexkit[opentelemetry-base]",
|
|
64
67
|
"hexkit[mongodb]",
|
|
65
|
-
"opentelemetry-instrumentation-pymongo >=0.
|
|
68
|
+
"opentelemetry-instrumentation-pymongo >=0.60b1",
|
|
66
69
|
]
|
|
67
70
|
opentelemetry-fastapi = [
|
|
68
71
|
"hexkit[opentelemetry-base]",
|
|
69
|
-
"opentelemetry-instrumentation-fastapi >=0.
|
|
72
|
+
"opentelemetry-instrumentation-fastapi >=0.60b1",
|
|
70
73
|
]
|
|
71
74
|
opentelemetry-s3 = [
|
|
72
75
|
"hexkit[opentelemetry-base]",
|
|
73
76
|
"hexkit[s3]",
|
|
74
|
-
"opentelemetry-instrumentation-botocore >=0.
|
|
77
|
+
"opentelemetry-instrumentation-botocore >=0.60b1",
|
|
78
|
+
]
|
|
79
|
+
opentelemetry-redis = [
|
|
80
|
+
"hexkit[opentelemetry-base]",
|
|
81
|
+
"hexkit[redis]",
|
|
82
|
+
"opentelemetry-instrumentation-redis >=0.60b1",
|
|
75
83
|
]
|
|
76
84
|
opentelemetry = [
|
|
77
85
|
"hexkit[opentelemetry-akafka]",
|
|
@@ -91,6 +99,10 @@ test-mongodb = [
|
|
|
91
99
|
"hexkit[mongodb]",
|
|
92
100
|
"testcontainers[mongo] >=4.13, <5",
|
|
93
101
|
]
|
|
102
|
+
test-redis = [
|
|
103
|
+
"hexkit[redis]",
|
|
104
|
+
"testcontainers[redis] >=4.13, <5",
|
|
105
|
+
]
|
|
94
106
|
test = [
|
|
95
107
|
"hexkit[test-akafka,test-s3,test-mongodb]",
|
|
96
108
|
]
|
|
@@ -209,7 +221,7 @@ check_untyped_defs = true
|
|
|
209
221
|
no_site_packages = false
|
|
210
222
|
|
|
211
223
|
[tool.pytest.ini_options]
|
|
212
|
-
minversion = "
|
|
224
|
+
minversion = "9.0"
|
|
213
225
|
asyncio_mode = "strict"
|
|
214
226
|
asyncio_default_fixture_loop_scope = "function"
|
|
215
227
|
|
|
@@ -23,7 +23,7 @@ from abc import ABC, abstractmethod
|
|
|
23
23
|
from collections.abc import AsyncIterator, Collection, Mapping
|
|
24
24
|
from contextlib import AbstractAsyncContextManager
|
|
25
25
|
from functools import partial
|
|
26
|
-
from typing import Any, TypeVar
|
|
26
|
+
from typing import Any, Generic, TypeVar
|
|
27
27
|
from uuid import uuid4
|
|
28
28
|
|
|
29
29
|
from pydantic import BaseModel, Field
|
|
@@ -39,10 +39,22 @@ __all__ = [
|
|
|
39
39
|
"ResourceAlreadyExistsError",
|
|
40
40
|
"ResourceNotFoundError",
|
|
41
41
|
"UUID4Field",
|
|
42
|
+
"UniqueConstraintViolationError",
|
|
42
43
|
]
|
|
43
44
|
|
|
45
|
+
|
|
46
|
+
class IndexBase(ABC):
|
|
47
|
+
"""Base from which all DAO index classes must inherit"""
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def list_field_names(self) -> list[str]:
|
|
51
|
+
"""Returns a list of all fields specified by this index"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
|
|
44
55
|
# Type variable for handling Data Transfer Objects:
|
|
45
56
|
Dto = TypeVar("Dto", bound=BaseModel)
|
|
57
|
+
Index = TypeVar("Index", bound=IndexBase)
|
|
46
58
|
|
|
47
59
|
|
|
48
60
|
class DaoError(RuntimeError):
|
|
@@ -65,6 +77,20 @@ class ResourceAlreadyExistsError(DaoError):
|
|
|
65
77
|
super().__init__(message)
|
|
66
78
|
|
|
67
79
|
|
|
80
|
+
class UniqueConstraintViolationError(DaoError):
|
|
81
|
+
"""Raised when attempting to insert a resource violated a uniqueness constraint.
|
|
82
|
+
|
|
83
|
+
This does not apply to the default unique index automatically applied to
|
|
84
|
+
the ID field.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, *, unique_fields: dict[str, Any]):
|
|
88
|
+
message = (
|
|
89
|
+
f"A resource with the unique field value(s) {unique_fields} already exists."
|
|
90
|
+
)
|
|
91
|
+
super().__init__(message)
|
|
92
|
+
|
|
93
|
+
|
|
68
94
|
class FindError(DaoError):
|
|
69
95
|
"""Base for all error related to DAO find operations."""
|
|
70
96
|
|
|
@@ -268,16 +294,19 @@ class DaoFactoryBase:
|
|
|
268
294
|
cls,
|
|
269
295
|
*,
|
|
270
296
|
dto_model: type[Dto],
|
|
271
|
-
|
|
297
|
+
indexes: Collection[Index] | None,
|
|
272
298
|
) -> None:
|
|
273
299
|
"""Checks that all provided fields are present in the dto_model.
|
|
274
300
|
Raises IndexFieldsInvalidError otherwise.
|
|
275
301
|
"""
|
|
276
|
-
if
|
|
302
|
+
if not indexes:
|
|
277
303
|
return
|
|
278
304
|
|
|
279
305
|
try:
|
|
280
|
-
|
|
306
|
+
for index in indexes:
|
|
307
|
+
validate_fields_in_model(
|
|
308
|
+
model=dto_model, fields=index.list_field_names()
|
|
309
|
+
)
|
|
281
310
|
except FieldNotInModelError as error:
|
|
282
311
|
raise cls.IndexFieldsInvalidError(
|
|
283
312
|
f"Provided index fields are invalid: {error}"
|
|
@@ -289,16 +318,14 @@ class DaoFactoryBase:
|
|
|
289
318
|
*,
|
|
290
319
|
dto_model: type[Dto],
|
|
291
320
|
id_field: str,
|
|
292
|
-
|
|
321
|
+
indexes: Collection[Index] | None,
|
|
293
322
|
) -> None:
|
|
294
323
|
"""Validates the input parameters of the get_dao method."""
|
|
295
324
|
cls._validate_dto_model_id(dto_model=dto_model, id_field=id_field)
|
|
296
|
-
cls._validate_fields_to_index(
|
|
297
|
-
dto_model=dto_model, fields_to_index=fields_to_index
|
|
298
|
-
)
|
|
325
|
+
cls._validate_fields_to_index(dto_model=dto_model, indexes=indexes)
|
|
299
326
|
|
|
300
327
|
|
|
301
|
-
class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
328
|
+
class DaoFactoryProtocol(DaoFactoryBase, ABC, Generic[Index]):
|
|
302
329
|
"""A protocol describing a factory to produce Data Access Objects (DAO) objects
|
|
303
330
|
that are enclosed in transactional scopes.
|
|
304
331
|
"""
|
|
@@ -309,7 +336,7 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
309
336
|
name: str,
|
|
310
337
|
dto_model: type[Dto],
|
|
311
338
|
id_field: str,
|
|
312
|
-
|
|
339
|
+
indexes: Collection[Index] | None = None,
|
|
313
340
|
) -> Dao[Dto]:
|
|
314
341
|
"""Constructs a DAO for interacting with resources in a database.
|
|
315
342
|
|
|
@@ -322,9 +349,9 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
322
349
|
id_field:
|
|
323
350
|
The name of the field of the `dto_model` that serves as resource ID.
|
|
324
351
|
(DAO implementation might use this field as primary key.)
|
|
325
|
-
|
|
326
|
-
Optionally, provide any
|
|
327
|
-
`id_field`. Defaults to None.
|
|
352
|
+
indexes:
|
|
353
|
+
Optionally, provide any indexes that should be created in addition to
|
|
354
|
+
the provider's default index on `id_field`. Defaults to None.
|
|
328
355
|
Returns:
|
|
329
356
|
A DAO specific to the provided DTO model.
|
|
330
357
|
|
|
@@ -334,17 +361,13 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
334
361
|
self.IdTypeNotSupportedError:
|
|
335
362
|
Raised when the id_field of the dto_model has an unexpected type.
|
|
336
363
|
"""
|
|
337
|
-
self._validate(
|
|
338
|
-
dto_model=dto_model,
|
|
339
|
-
id_field=id_field,
|
|
340
|
-
fields_to_index=fields_to_index,
|
|
341
|
-
)
|
|
364
|
+
self._validate(dto_model=dto_model, id_field=id_field, indexes=indexes)
|
|
342
365
|
|
|
343
366
|
return await self._get_dao(
|
|
344
367
|
name=name,
|
|
345
368
|
dto_model=dto_model,
|
|
346
369
|
id_field=id_field,
|
|
347
|
-
|
|
370
|
+
indexes=indexes,
|
|
348
371
|
)
|
|
349
372
|
|
|
350
373
|
@abstractmethod
|
|
@@ -354,7 +377,7 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
354
377
|
name: str,
|
|
355
378
|
dto_model: type[Dto],
|
|
356
379
|
id_field: str,
|
|
357
|
-
|
|
380
|
+
indexes: Collection[Index] | None,
|
|
358
381
|
) -> Dao[Dto]:
|
|
359
382
|
"""*To be implemented by the provider. Input validation is done outside of this
|
|
360
383
|
method.*
|
|
@@ -26,11 +26,7 @@ from abc import ABC, abstractmethod
|
|
|
26
26
|
from collections.abc import Callable, Collection
|
|
27
27
|
|
|
28
28
|
from hexkit.custom_types import JsonObject
|
|
29
|
-
from hexkit.protocols.dao import
|
|
30
|
-
Dao,
|
|
31
|
-
DaoFactoryBase,
|
|
32
|
-
Dto,
|
|
33
|
-
)
|
|
29
|
+
from hexkit.protocols.dao import Dao, DaoFactoryBase, Dto, Index
|
|
34
30
|
|
|
35
31
|
|
|
36
32
|
class DaoPublisher(Dao[Dto], typing.Protocol[Dto]):
|
|
@@ -49,7 +45,7 @@ class DaoPublisher(Dao[Dto], typing.Protocol[Dto]):
|
|
|
49
45
|
...
|
|
50
46
|
|
|
51
47
|
|
|
52
|
-
class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
|
|
48
|
+
class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC, typing.Generic[Index]):
|
|
53
49
|
"""A protocol describing a factory to produce Data Access Objects (DAO) objects
|
|
54
50
|
which automatically publish changes according to the outbox pattern.
|
|
55
51
|
"""
|
|
@@ -60,7 +56,7 @@ class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
60
56
|
name: str,
|
|
61
57
|
dto_model: type[Dto],
|
|
62
58
|
id_field: str,
|
|
63
|
-
|
|
59
|
+
indexes: Collection[Index] | None = None,
|
|
64
60
|
dto_to_event: Callable[[Dto], JsonObject | None],
|
|
65
61
|
event_topic: str,
|
|
66
62
|
autopublish: bool = True,
|
|
@@ -76,9 +72,9 @@ class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
76
72
|
id_field:
|
|
77
73
|
The name of the field of the `dto_model` that serves as resource ID.
|
|
78
74
|
(DAO implementation might use this field as primary key.)
|
|
79
|
-
|
|
80
|
-
Optionally, provide any
|
|
81
|
-
`id_field`. Defaults to None.
|
|
75
|
+
indexes:
|
|
76
|
+
Optionally, provide any indexes that should be created in addition to
|
|
77
|
+
the provider's default index on `id_field`. Defaults to None.
|
|
82
78
|
dto_to_event:
|
|
83
79
|
A function that takes a DTO and returns the payload for an event.
|
|
84
80
|
If the returned payload is None, the event will not be published.
|
|
@@ -97,17 +93,13 @@ class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
97
93
|
self.IdTypeNotSupportedError:
|
|
98
94
|
Raised when the id_field of the dto_model has an unexpected type.
|
|
99
95
|
"""
|
|
100
|
-
self._validate(
|
|
101
|
-
dto_model=dto_model,
|
|
102
|
-
id_field=id_field,
|
|
103
|
-
fields_to_index=fields_to_index,
|
|
104
|
-
)
|
|
96
|
+
self._validate(dto_model=dto_model, id_field=id_field, indexes=indexes)
|
|
105
97
|
|
|
106
98
|
return await self._get_dao(
|
|
107
99
|
name=name,
|
|
108
100
|
dto_model=dto_model,
|
|
109
101
|
id_field=id_field,
|
|
110
|
-
|
|
102
|
+
indexes=indexes,
|
|
111
103
|
dto_to_event=dto_to_event,
|
|
112
104
|
event_topic=event_topic,
|
|
113
105
|
autopublish=autopublish,
|
|
@@ -120,7 +112,7 @@ class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
120
112
|
name: str,
|
|
121
113
|
dto_model: type[Dto],
|
|
122
114
|
id_field: str,
|
|
123
|
-
|
|
115
|
+
indexes: Collection[Index] | None,
|
|
124
116
|
dto_to_event: Callable[[Dto], JsonObject | None],
|
|
125
117
|
event_topic: str,
|
|
126
118
|
autopublish: bool,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
|
+
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
"""Protocol supporting key-value store operations."""
|
|
18
|
+
|
|
19
|
+
from typing import Generic, Protocol, TypeVar
|
|
20
|
+
|
|
21
|
+
V = TypeVar("V")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class KeyValueStoreProtocol(Protocol, Generic[V]):
|
|
25
|
+
"""A protocol for a key-value store."""
|
|
26
|
+
|
|
27
|
+
async def get(self, key: str, default: V | None = None) -> V | None:
|
|
28
|
+
"""Retrieve the value for the given key.
|
|
29
|
+
|
|
30
|
+
Returns the specified default value if there is no such value in the store.
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
async def set(self, key: str, value: V) -> None:
|
|
35
|
+
"""Set the value for the given key.
|
|
36
|
+
|
|
37
|
+
Providers must reject storing a value of None if they
|
|
38
|
+
support a value domain V that can contain such values.
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
async def delete(self, key: str) -> None:
|
|
43
|
+
"""Delete the value for the given key.
|
|
44
|
+
|
|
45
|
+
Does nothing if there is no such value in the store.
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
async def exists(self, key: str) -> bool:
|
|
50
|
+
"""Check if the given key exists.
|
|
51
|
+
|
|
52
|
+
Default implementation for classes inheriting from this protocol.
|
|
53
|
+
Providers can use something more efficient.
|
|
54
|
+
"""
|
|
55
|
+
return (await self.get(key)) is not None
|
|
@@ -182,6 +182,31 @@ class ObjectStorageProtocol(ABC):
|
|
|
182
182
|
anticipated_part_size=anticipated_part_size,
|
|
183
183
|
)
|
|
184
184
|
|
|
185
|
+
async def list_multipart_uploads_for_object(
|
|
186
|
+
self, *, bucket_id: str, object_id: str
|
|
187
|
+
) -> list[str]:
|
|
188
|
+
"""Lists all active multipart uploads for the given object ID.
|
|
189
|
+
|
|
190
|
+
Raises a `BucketNotFoundError` if the bucket does not exist.
|
|
191
|
+
"""
|
|
192
|
+
self._validate_bucket_id(bucket_id)
|
|
193
|
+
self._validate_object_id(object_id)
|
|
194
|
+
return await self._list_multipart_uploads_for_object(
|
|
195
|
+
bucket_id=bucket_id, object_id=object_id
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
async def get_all_multipart_uploads(self, *, bucket_id: str) -> dict[str, str]:
|
|
199
|
+
"""Gets all active multipart uploads for the given bucket ID.
|
|
200
|
+
|
|
201
|
+
Returns a dict where the keys are upload IDs and values are object IDs.
|
|
202
|
+
S3 allows multiple ongoing multi-part uploads, so it's possible for some upload
|
|
203
|
+
IDs to map to the same object ID.
|
|
204
|
+
|
|
205
|
+
Raises a `BucketNotFoundError` if the bucket does not exist.
|
|
206
|
+
"""
|
|
207
|
+
self._validate_bucket_id(bucket_id)
|
|
208
|
+
return await self._get_all_multipart_uploads(bucket_id=bucket_id)
|
|
209
|
+
|
|
185
210
|
async def get_object_download_url(
|
|
186
211
|
self, *, bucket_id: str, object_id: str, expires_after: int = 86400
|
|
187
212
|
) -> str:
|
|
@@ -404,6 +429,29 @@ class ObjectStorageProtocol(ABC):
|
|
|
404
429
|
"""
|
|
405
430
|
...
|
|
406
431
|
|
|
432
|
+
@abstractmethod
|
|
433
|
+
async def _list_multipart_uploads_for_object(
|
|
434
|
+
self, *, bucket_id: str, object_id: str
|
|
435
|
+
) -> list[str]:
|
|
436
|
+
"""Lists all active multipart uploads for the given object ID.
|
|
437
|
+
|
|
438
|
+
*To be implemented by the provider. Input validation is done outside of this
|
|
439
|
+
method.*
|
|
440
|
+
"""
|
|
441
|
+
...
|
|
442
|
+
|
|
443
|
+
@abstractmethod
|
|
444
|
+
async def _get_all_multipart_uploads(self, *, bucket_id: str) -> dict[str, str]:
|
|
445
|
+
"""Gets all active multipart uploads for the given bucket ID.
|
|
446
|
+
|
|
447
|
+
Returns a dict where the keys are upload IDs and values are object IDs.
|
|
448
|
+
S3 allows multiple ongoing multi-part uploads, so it's possible for some upload
|
|
449
|
+
IDs to map to the same object ID.
|
|
450
|
+
|
|
451
|
+
*To be implemented by the provider. Input validation is done outside of this
|
|
452
|
+
method.*
|
|
453
|
+
"""
|
|
454
|
+
|
|
407
455
|
@abstractmethod
|
|
408
456
|
async def _get_object_download_url(
|
|
409
457
|
self, *, bucket_id: str, object_id: str, expires_after: int = 86400
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
|
+
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
"""Subpackage containing MongoDB-based providers and related utilities."""
|
|
18
|
+
|
|
19
|
+
from .config import MongoDbConfig
|
|
20
|
+
from .provider import (
|
|
21
|
+
ConfiguredMongoClient,
|
|
22
|
+
MongoDbBytesKeyValueStore,
|
|
23
|
+
MongoDbDao,
|
|
24
|
+
MongoDbDaoFactory,
|
|
25
|
+
MongoDbDtoKeyValueStore,
|
|
26
|
+
MongoDbIndex,
|
|
27
|
+
MongoDbJsonKeyValueStore,
|
|
28
|
+
MongoDbStrKeyValueStore,
|
|
29
|
+
translate_pymongo_errors,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"ConfiguredMongoClient",
|
|
34
|
+
"MongoDbBytesKeyValueStore",
|
|
35
|
+
"MongoDbConfig",
|
|
36
|
+
"MongoDbDao",
|
|
37
|
+
"MongoDbDaoFactory",
|
|
38
|
+
"MongoDbDtoKeyValueStore",
|
|
39
|
+
"MongoDbIndex",
|
|
40
|
+
"MongoDbJsonKeyValueStore",
|
|
41
|
+
"MongoDbStrKeyValueStore",
|
|
42
|
+
"translate_pymongo_errors",
|
|
43
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
|
+
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
"""MongoDB specific configuration."""
|
|
18
|
+
|
|
19
|
+
from pydantic import Field, MongoDsn, PositiveInt, Secret
|
|
20
|
+
from pydantic_settings import BaseSettings
|
|
21
|
+
|
|
22
|
+
__all__ = ["MongoDbConfig"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MongoDbConfig(BaseSettings):
|
|
26
|
+
"""Configuration parameters for connecting to a MongoDB server.
|
|
27
|
+
|
|
28
|
+
Inherit your config class from this class if your application uses MongoDB.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
mongo_dsn: Secret[MongoDsn] = Field(
|
|
32
|
+
...,
|
|
33
|
+
examples=["mongodb://localhost:27017"],
|
|
34
|
+
description=(
|
|
35
|
+
"MongoDB connection string. Might include credentials."
|
|
36
|
+
+ " For more information see:"
|
|
37
|
+
+ " https://naiveskill.com/mongodb-connection-string/"
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
db_name: str = Field(
|
|
41
|
+
...,
|
|
42
|
+
examples=["my-database"],
|
|
43
|
+
description="Name of the database located on the MongoDB server.",
|
|
44
|
+
)
|
|
45
|
+
mongo_timeout: PositiveInt | None = Field(
|
|
46
|
+
default=None,
|
|
47
|
+
examples=[300, 600, None],
|
|
48
|
+
description=(
|
|
49
|
+
"Timeout in seconds for API calls to MongoDB. The timeout applies to all steps"
|
|
50
|
+
+ " needed to complete the operation, including server selection, connection"
|
|
51
|
+
+ " checkout, serialization, and server-side execution. When the timeout"
|
|
52
|
+
+ " expires, PyMongo raises a timeout exception. If set to None, the"
|
|
53
|
+
+ " operation will not time out (default MongoDB behavior)."
|
|
54
|
+
),
|
|
55
|
+
)
|
|
@@ -27,7 +27,7 @@ from pymongo import AsyncMongoClient
|
|
|
27
27
|
from pymongo.asynchronous.database import AsyncDatabase
|
|
28
28
|
from pymongo.errors import DuplicateKeyError
|
|
29
29
|
|
|
30
|
-
from hexkit.providers.mongodb import MongoDbConfig
|
|
30
|
+
from hexkit.providers.mongodb.config import MongoDbConfig
|
|
31
31
|
from hexkit.providers.mongodb.provider import ConfiguredMongoClient
|
|
32
32
|
|
|
33
33
|
from ._utils import MigrationDefinition, Reversible
|