hexkit 7.3.0__tar.gz → 8.1.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.3.0 → hexkit-8.1.0}/LICENSE +1 -1
- {hexkit-7.3.0/src/hexkit.egg-info → hexkit-8.1.0}/PKG-INFO +6 -6
- {hexkit-7.3.0 → hexkit-8.1.0}/pyproject.toml +9 -9
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/__main__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/base.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/config.py +14 -2
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/correlation.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/custom_types.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/log.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/opentelemetry.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/dao.py +55 -21
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/daopub.py +10 -18
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/daosub.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/eventpub.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/eventsub.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/kvstore.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/objstorage.py +49 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/config.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/provider/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/provider/daosub.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/provider/eventpub.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/provider/eventsub.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/provider/utils.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/testcontainer.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/testutils.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/__init__.py +3 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/config.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/migrations/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/migrations/_manager.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/migrations/_utils.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/migrations/helpers.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/provider/__init__.py +3 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/provider/client.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/provider/dao.py +104 -15
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/provider/kvstore.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/provider/utils.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/testutils.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/config.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/provider/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/provider/daopub.py +48 -20
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/testutils.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/config.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/provider/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/provider/client.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/provider/kvstore.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/testutils.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/provider.py +42 -11
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/test_files/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/testutils/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/testutils/_fixtures.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/testutils/_utils.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/testing/__init__.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/testing/dao.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/testing/eventpub.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/testing/s3.py +27 -1
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/utils.py +1 -1
- {hexkit-7.3.0 → hexkit-8.1.0/src/hexkit.egg-info}/PKG-INFO +6 -6
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit.egg-info/requires.txt +5 -5
- {hexkit-7.3.0 → hexkit-8.1.0}/MANIFEST.in +0 -0
- {hexkit-7.3.0 → hexkit-8.1.0}/README.md +0 -0
- {hexkit-7.3.0 → hexkit-8.1.0}/setup.cfg +0 -0
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/py.typed +0 -0
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit.egg-info/top_level.txt +0 -0
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright 2021 -
|
|
189
|
+
Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
190
190
|
for the German Human Genome-Phenome Archive (GHGA)
|
|
191
191
|
|
|
192
192
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexkit
|
|
3
|
-
Version:
|
|
3
|
+
Version: 8.1.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
|
|
@@ -29,7 +29,7 @@ Provides-Extra: s3
|
|
|
29
29
|
Requires-Dist: boto3<2,>=1.42; extra == "s3"
|
|
30
30
|
Requires-Dist: botocore<2,>=1.42; extra == "s3"
|
|
31
31
|
Provides-Extra: mongodb
|
|
32
|
-
Requires-Dist: pymongo<5,>=4.
|
|
32
|
+
Requires-Dist: pymongo<5,>=4.16; extra == "mongodb"
|
|
33
33
|
Provides-Extra: redis
|
|
34
34
|
Requires-Dist: redis<8,>=7.1; extra == "redis"
|
|
35
35
|
Provides-Extra: opentelemetry-base
|
|
@@ -64,16 +64,16 @@ Requires-Dist: hexkit[opentelemetry-fastapi]; extra == "opentelemetry"
|
|
|
64
64
|
Requires-Dist: hexkit[opentelemetry-s3]; extra == "opentelemetry"
|
|
65
65
|
Provides-Extra: test-akafka
|
|
66
66
|
Requires-Dist: hexkit[akafka]; extra == "test-akafka"
|
|
67
|
-
Requires-Dist: testcontainers[kafka]<5,>=4.
|
|
67
|
+
Requires-Dist: testcontainers[kafka]<5,>=4.14; extra == "test-akafka"
|
|
68
68
|
Provides-Extra: test-s3
|
|
69
69
|
Requires-Dist: hexkit[s3]; extra == "test-s3"
|
|
70
|
-
Requires-Dist: testcontainers<5,>=4.
|
|
70
|
+
Requires-Dist: testcontainers<5,>=4.14; extra == "test-s3"
|
|
71
71
|
Provides-Extra: test-mongodb
|
|
72
72
|
Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
|
|
73
|
-
Requires-Dist: testcontainers[mongo]<5,>=4.
|
|
73
|
+
Requires-Dist: testcontainers[mongo]<5,>=4.14; extra == "test-mongodb"
|
|
74
74
|
Provides-Extra: test-redis
|
|
75
75
|
Requires-Dist: hexkit[redis]; extra == "test-redis"
|
|
76
|
-
Requires-Dist: testcontainers[redis]<5,>=4.
|
|
76
|
+
Requires-Dist: testcontainers[redis]<5,>=4.14; extra == "test-redis"
|
|
77
77
|
Provides-Extra: test
|
|
78
78
|
Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
|
|
79
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,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Intended Audience :: Developers",
|
|
24
24
|
]
|
|
25
25
|
name = "hexkit"
|
|
26
|
-
version = "
|
|
26
|
+
version = "8.1.0"
|
|
27
27
|
description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
|
|
28
28
|
dependencies = [
|
|
29
29
|
"opentelemetry-api >=1.39, <2",
|
|
@@ -45,7 +45,7 @@ s3 = [
|
|
|
45
45
|
"botocore >=1.42, <2",
|
|
46
46
|
]
|
|
47
47
|
mongodb = [
|
|
48
|
-
"pymongo >=4.
|
|
48
|
+
"pymongo >=4.16, <5",
|
|
49
49
|
]
|
|
50
50
|
redis = [
|
|
51
51
|
"redis >=7.1, <8",
|
|
@@ -89,19 +89,19 @@ opentelemetry = [
|
|
|
89
89
|
]
|
|
90
90
|
test-akafka = [
|
|
91
91
|
"hexkit[akafka]",
|
|
92
|
-
"testcontainers[kafka] >=4.
|
|
92
|
+
"testcontainers[kafka] >=4.14, <5",
|
|
93
93
|
]
|
|
94
94
|
test-s3 = [
|
|
95
95
|
"hexkit[s3]",
|
|
96
|
-
"testcontainers >=4.
|
|
96
|
+
"testcontainers >=4.14, <5",
|
|
97
97
|
]
|
|
98
98
|
test-mongodb = [
|
|
99
99
|
"hexkit[mongodb]",
|
|
100
|
-
"testcontainers[mongo] >=4.
|
|
100
|
+
"testcontainers[mongo] >=4.14, <5",
|
|
101
101
|
]
|
|
102
102
|
test-redis = [
|
|
103
103
|
"hexkit[redis]",
|
|
104
|
-
"testcontainers[redis] >=4.
|
|
104
|
+
"testcontainers[redis] >=4.14, <5",
|
|
105
105
|
]
|
|
106
106
|
test = [
|
|
107
107
|
"hexkit[test-akafka,test-s3,test-mongodb]",
|
|
@@ -221,7 +221,7 @@ check_untyped_defs = true
|
|
|
221
221
|
no_site_packages = false
|
|
222
222
|
|
|
223
223
|
[tool.pytest.ini_options]
|
|
224
|
-
minversion = "
|
|
224
|
+
minversion = "9.0"
|
|
225
225
|
asyncio_mode = "strict"
|
|
226
226
|
asyncio_default_fixture_loop_scope = "function"
|
|
227
227
|
|
|
@@ -251,6 +251,6 @@ legacy_tox_ini = """
|
|
|
251
251
|
deps =
|
|
252
252
|
--no-deps -r ./lock/requirements-dev.txt
|
|
253
253
|
commands =
|
|
254
|
-
py310: pip install --no-deps backports.asyncio.runner==1.2.0 exceptiongroup==1.3.
|
|
254
|
+
py310: pip install --no-deps backports.asyncio.runner==1.2.0 exceptiongroup==1.3.1 tomli==2.2.1
|
|
255
255
|
pytest {posargs}
|
|
256
256
|
"""
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -29,6 +29,8 @@ from pydantic_settings import (
|
|
|
29
29
|
|
|
30
30
|
# Default config prefix:
|
|
31
31
|
DEFAULT_CONFIG_PREFIX: Final = "ghga_services"
|
|
32
|
+
# Default dotenv path:
|
|
33
|
+
DEFAULT_DOTENV_PATH: Final[Path] = Path("/secrets")
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
class ConfigYamlDoesNotExist(RuntimeError):
|
|
@@ -82,6 +84,7 @@ def get_default_config_yaml(prefix: str) -> Path | None:
|
|
|
82
84
|
|
|
83
85
|
def config_from_yaml(
|
|
84
86
|
prefix: str = DEFAULT_CONFIG_PREFIX,
|
|
87
|
+
dotenv_prefix: Path = DEFAULT_DOTENV_PATH,
|
|
85
88
|
) -> Callable:
|
|
86
89
|
"""A factory that returns decorator functions which extends a
|
|
87
90
|
pydantic BaseSettings class to read in parameters from a config yaml.
|
|
@@ -102,6 +105,10 @@ def config_from_yaml(
|
|
|
102
105
|
"{prefix}_{actual_variable_name}". Moreover, this prefix is used
|
|
103
106
|
to derive the default location for the config yaml file
|
|
104
107
|
("~/.{prefix}.yaml"). Defaults to "ghga_services".
|
|
108
|
+
dotenv_prefix: (Path, optional):
|
|
109
|
+
When defining parameters via dotenv files, this prefix is used to set the path
|
|
110
|
+
for the dotenv files. The final path used is constructed as
|
|
111
|
+
"{dotenv_prefix}/.env". Defaults to "/secrets".
|
|
105
112
|
"""
|
|
106
113
|
|
|
107
114
|
def decorator(settings) -> Callable:
|
|
@@ -141,7 +148,11 @@ def config_from_yaml(
|
|
|
141
148
|
class ModSettings(settings):
|
|
142
149
|
"""Modifies the original Settings class provided by the user"""
|
|
143
150
|
|
|
144
|
-
model_config = SettingsConfigDict(
|
|
151
|
+
model_config = SettingsConfigDict(
|
|
152
|
+
frozen=True,
|
|
153
|
+
env_prefix=f"{prefix}_",
|
|
154
|
+
env_file=(".env", dotenv_prefix / ".env"),
|
|
155
|
+
)
|
|
145
156
|
|
|
146
157
|
@classmethod
|
|
147
158
|
def settings_customise_sources(
|
|
@@ -156,6 +167,7 @@ def config_from_yaml(
|
|
|
156
167
|
return (
|
|
157
168
|
init_settings,
|
|
158
169
|
env_settings,
|
|
170
|
+
dotenv_settings,
|
|
159
171
|
file_secret_settings,
|
|
160
172
|
YamlConfigSettingsSource(settings_cls, config_yaml),
|
|
161
173
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -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
|
|
|
@@ -152,6 +178,9 @@ class Dao(typing.Protocol[Dto]):
|
|
|
152
178
|
Raises:
|
|
153
179
|
ResourceNotFoundError:
|
|
154
180
|
when resource with the id specified in the dto was not found
|
|
181
|
+
UniqueConstraintViolationError:
|
|
182
|
+
when updating the dto would violate a unique index constraint over some
|
|
183
|
+
field other than the ID field.
|
|
155
184
|
"""
|
|
156
185
|
...
|
|
157
186
|
|
|
@@ -224,6 +253,9 @@ class Dao(typing.Protocol[Dto]):
|
|
|
224
253
|
Raises:
|
|
225
254
|
ResourceAlreadyExistsError:
|
|
226
255
|
when a resource with the ID specified in the dto does already exist.
|
|
256
|
+
UniqueConstraintViolationError:
|
|
257
|
+
when inserting the dto would violate a unique index constraint over some
|
|
258
|
+
field other than the ID field.
|
|
227
259
|
"""
|
|
228
260
|
...
|
|
229
261
|
|
|
@@ -234,6 +266,11 @@ class Dao(typing.Protocol[Dto]):
|
|
|
234
266
|
dto:
|
|
235
267
|
Resource content as a pydantic-based data transfer object including the
|
|
236
268
|
resource ID.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
UniqueConstraintViolationError:
|
|
272
|
+
when upserting the dto would violate a unique index constraint over some
|
|
273
|
+
field other than the ID field.
|
|
237
274
|
"""
|
|
238
275
|
...
|
|
239
276
|
|
|
@@ -268,16 +305,19 @@ class DaoFactoryBase:
|
|
|
268
305
|
cls,
|
|
269
306
|
*,
|
|
270
307
|
dto_model: type[Dto],
|
|
271
|
-
|
|
308
|
+
indexes: Collection[Index] | None,
|
|
272
309
|
) -> None:
|
|
273
310
|
"""Checks that all provided fields are present in the dto_model.
|
|
274
311
|
Raises IndexFieldsInvalidError otherwise.
|
|
275
312
|
"""
|
|
276
|
-
if
|
|
313
|
+
if not indexes:
|
|
277
314
|
return
|
|
278
315
|
|
|
279
316
|
try:
|
|
280
|
-
|
|
317
|
+
for index in indexes:
|
|
318
|
+
validate_fields_in_model(
|
|
319
|
+
model=dto_model, fields=index.list_field_names()
|
|
320
|
+
)
|
|
281
321
|
except FieldNotInModelError as error:
|
|
282
322
|
raise cls.IndexFieldsInvalidError(
|
|
283
323
|
f"Provided index fields are invalid: {error}"
|
|
@@ -289,16 +329,14 @@ class DaoFactoryBase:
|
|
|
289
329
|
*,
|
|
290
330
|
dto_model: type[Dto],
|
|
291
331
|
id_field: str,
|
|
292
|
-
|
|
332
|
+
indexes: Collection[Index] | None,
|
|
293
333
|
) -> None:
|
|
294
334
|
"""Validates the input parameters of the get_dao method."""
|
|
295
335
|
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
|
-
)
|
|
336
|
+
cls._validate_fields_to_index(dto_model=dto_model, indexes=indexes)
|
|
299
337
|
|
|
300
338
|
|
|
301
|
-
class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
339
|
+
class DaoFactoryProtocol(DaoFactoryBase, ABC, Generic[Index]):
|
|
302
340
|
"""A protocol describing a factory to produce Data Access Objects (DAO) objects
|
|
303
341
|
that are enclosed in transactional scopes.
|
|
304
342
|
"""
|
|
@@ -309,7 +347,7 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
309
347
|
name: str,
|
|
310
348
|
dto_model: type[Dto],
|
|
311
349
|
id_field: str,
|
|
312
|
-
|
|
350
|
+
indexes: Collection[Index] | None = None,
|
|
313
351
|
) -> Dao[Dto]:
|
|
314
352
|
"""Constructs a DAO for interacting with resources in a database.
|
|
315
353
|
|
|
@@ -322,9 +360,9 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
322
360
|
id_field:
|
|
323
361
|
The name of the field of the `dto_model` that serves as resource ID.
|
|
324
362
|
(DAO implementation might use this field as primary key.)
|
|
325
|
-
|
|
326
|
-
Optionally, provide any
|
|
327
|
-
`id_field`. Defaults to None.
|
|
363
|
+
indexes:
|
|
364
|
+
Optionally, provide any indexes that should be created in addition to
|
|
365
|
+
the provider's default index on `id_field`. Defaults to None.
|
|
328
366
|
Returns:
|
|
329
367
|
A DAO specific to the provided DTO model.
|
|
330
368
|
|
|
@@ -334,17 +372,13 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
334
372
|
self.IdTypeNotSupportedError:
|
|
335
373
|
Raised when the id_field of the dto_model has an unexpected type.
|
|
336
374
|
"""
|
|
337
|
-
self._validate(
|
|
338
|
-
dto_model=dto_model,
|
|
339
|
-
id_field=id_field,
|
|
340
|
-
fields_to_index=fields_to_index,
|
|
341
|
-
)
|
|
375
|
+
self._validate(dto_model=dto_model, id_field=id_field, indexes=indexes)
|
|
342
376
|
|
|
343
377
|
return await self._get_dao(
|
|
344
378
|
name=name,
|
|
345
379
|
dto_model=dto_model,
|
|
346
380
|
id_field=id_field,
|
|
347
|
-
|
|
381
|
+
indexes=indexes,
|
|
348
382
|
)
|
|
349
383
|
|
|
350
384
|
@abstractmethod
|
|
@@ -354,7 +388,7 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
|
354
388
|
name: str,
|
|
355
389
|
dto_model: type[Dto],
|
|
356
390
|
id_field: str,
|
|
357
|
-
|
|
391
|
+
indexes: Collection[Index] | None,
|
|
358
392
|
) -> Dao[Dto]:
|
|
359
393
|
"""*To be implemented by the provider. Input validation is done outside of this
|
|
360
394
|
method.*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -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,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021 -
|
|
1
|
+
# Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
2
|
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|