hexkit 7.1.0__tar.gz → 7.2.1__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.1.0/src/hexkit.egg-info → hexkit-7.2.1}/PKG-INFO +14 -3
- {hexkit-7.1.0 → hexkit-7.2.1}/README.md +13 -2
- {hexkit-7.1.0 → hexkit-7.2.1}/pyproject.toml +1 -1
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/correlation.py +10 -5
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/testing/__init__.py +11 -0
- hexkit-7.2.1/src/hexkit/providers/testing/dao.py +146 -0
- hexkit-7.2.1/src/hexkit/providers/testing/s3.py +402 -0
- {hexkit-7.1.0 → hexkit-7.2.1/src/hexkit.egg-info}/PKG-INFO +14 -3
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit.egg-info/SOURCES.txt +3 -1
- {hexkit-7.1.0 → hexkit-7.2.1}/LICENSE +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/MANIFEST.in +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/setup.cfg +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/__main__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/base.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/config.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/custom_types.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/log.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/opentelemetry.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/protocols/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/protocols/dao.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/protocols/daopub.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/protocols/daosub.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/protocols/eventpub.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/protocols/eventsub.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/protocols/objstorage.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/akafka/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/akafka/config.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/akafka/provider/eventsub.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/akafka/provider/utils.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/akafka/testcontainer.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/akafka/testutils.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongodb/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongodb/migrations/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongodb/migrations/_manager.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongodb/migrations/_utils.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongodb/migrations/helpers.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongodb/provider.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongodb/testutils.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongokafka/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongokafka/provider/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongokafka/provider/config.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongokafka/provider/daopub.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/mongokafka/testutils.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/s3/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/s3/provider.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/s3/testutils/_fixtures.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/providers/testing/eventpub.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/py.typed +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit/utils.py +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/src/hexkit.egg-info/requires.txt +0 -0
- {hexkit-7.1.0 → hexkit-7.2.1}/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: 7.1
|
|
3
|
+
Version: 7.2.1
|
|
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
|
|
@@ -78,10 +78,12 @@ Dynamic: license-file
|
|
|
78
78
|
[](https://coveralls.io/github/ghga-de/hexkit?branch=main)
|
|
79
79
|
|
|
80
80
|
# hexkit
|
|
81
|
+
|
|
81
82
|
A chassis library for building domain-focused, infrastructure-agnostic, and event-driven
|
|
82
83
|
microservices in Python.
|
|
83
84
|
|
|
84
85
|
## In a Nutshell
|
|
86
|
+
|
|
85
87
|
This project implements the Triple Hexagonal Architecture pattern which is an
|
|
86
88
|
optimization of the ordinary
|
|
87
89
|
[Hexagonal Architecture](https://alistair.cockburn.us/hexagonal-architecture/) for
|
|
@@ -110,6 +112,7 @@ a limited collection of protocol-provider pairs that are of immediate interest t
|
|
|
110
112
|
authors. We like to add support for more protocols and technologies over time.
|
|
111
113
|
|
|
112
114
|
The following protocols and providers are currently available:
|
|
115
|
+
|
|
113
116
|
| Protocol | Providers |
|
|
114
117
|
|---|---|
|
|
115
118
|
| Event Publishing | Apache Kafka |
|
|
@@ -140,15 +143,18 @@ of jumping directly into it. You can find the protocols being defined at
|
|
|
140
143
|
A more elaborate documentation and tutorial is on the way.
|
|
141
144
|
|
|
142
145
|
## Installation
|
|
146
|
+
|
|
143
147
|
This package is available at PyPI:
|
|
144
|
-
https://pypi.org/project/hexkit
|
|
148
|
+
<https://pypi.org/project/hexkit>
|
|
145
149
|
|
|
146
150
|
You can install it from there using:
|
|
147
|
-
|
|
151
|
+
|
|
152
|
+
```sh
|
|
148
153
|
pip install hexkit
|
|
149
154
|
```
|
|
150
155
|
|
|
151
156
|
The following extras are available:
|
|
157
|
+
|
|
152
158
|
- `akafka`: when using the Apache Kafka-based event publishing or subscription
|
|
153
159
|
- `s3`: when interacting with S3-compatible Object Storages
|
|
154
160
|
- `mongodb`: when using mongodb as backend for the DAO protocol
|
|
@@ -158,6 +164,7 @@ The following extras are available:
|
|
|
158
164
|
- `all`: a union of all the above
|
|
159
165
|
|
|
160
166
|
## Development
|
|
167
|
+
|
|
161
168
|
For setting up the development environment, we rely on the
|
|
162
169
|
[devcontainer feature](https://code.visualstudio.com/docs/remote/containers) of vscode.
|
|
163
170
|
|
|
@@ -167,6 +174,7 @@ Then, you just have to open this repo in vscode and run the command
|
|
|
167
174
|
`Remote-Containers: Reopen in Container` from the vscode "Command Palette".
|
|
168
175
|
|
|
169
176
|
This will give you a full-fledged, pre-configured development environment including:
|
|
177
|
+
|
|
170
178
|
- infrastructural dependencies of the service (databases, etc.)
|
|
171
179
|
- all relevant vscode extensions pre-installed
|
|
172
180
|
- pre-configured linting and auto-formatting
|
|
@@ -175,12 +183,14 @@ This will give you a full-fledged, pre-configured development environment includ
|
|
|
175
183
|
|
|
176
184
|
Additionally, the following convenience command is available inside the devcontainer
|
|
177
185
|
(type it in the integrated terminal of VS Code):
|
|
186
|
+
|
|
178
187
|
- `dev_install` - install the lib with all development dependencies and pre-commit hooks
|
|
179
188
|
(please run that if you are starting the devcontainer for the first time
|
|
180
189
|
or if added any python dependencies to the [`./pyproject.toml`](./pyproject.toml))
|
|
181
190
|
|
|
182
191
|
If you prefer not to use vscode, you could get a similar setup (without the editor
|
|
183
192
|
specific features) by running the following commands:
|
|
193
|
+
|
|
184
194
|
``` bash
|
|
185
195
|
# Execute in the repo's root dir:
|
|
186
196
|
cd ./.devcontainer
|
|
@@ -192,4 +202,5 @@ docker run -it hexkit /bin/bash
|
|
|
192
202
|
```
|
|
193
203
|
|
|
194
204
|
## License
|
|
205
|
+
|
|
195
206
|
This repository is free to use and modify according to the [Apache 2.0 License](./LICENSE).
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
[](https://coveralls.io/github/ghga-de/hexkit?branch=main)
|
|
5
5
|
|
|
6
6
|
# hexkit
|
|
7
|
+
|
|
7
8
|
A chassis library for building domain-focused, infrastructure-agnostic, and event-driven
|
|
8
9
|
microservices in Python.
|
|
9
10
|
|
|
10
11
|
## In a Nutshell
|
|
12
|
+
|
|
11
13
|
This project implements the Triple Hexagonal Architecture pattern which is an
|
|
12
14
|
optimization of the ordinary
|
|
13
15
|
[Hexagonal Architecture](https://alistair.cockburn.us/hexagonal-architecture/) for
|
|
@@ -36,6 +38,7 @@ a limited collection of protocol-provider pairs that are of immediate interest t
|
|
|
36
38
|
authors. We like to add support for more protocols and technologies over time.
|
|
37
39
|
|
|
38
40
|
The following protocols and providers are currently available:
|
|
41
|
+
|
|
39
42
|
| Protocol | Providers |
|
|
40
43
|
|---|---|
|
|
41
44
|
| Event Publishing | Apache Kafka |
|
|
@@ -66,15 +69,18 @@ of jumping directly into it. You can find the protocols being defined at
|
|
|
66
69
|
A more elaborate documentation and tutorial is on the way.
|
|
67
70
|
|
|
68
71
|
## Installation
|
|
72
|
+
|
|
69
73
|
This package is available at PyPI:
|
|
70
|
-
https://pypi.org/project/hexkit
|
|
74
|
+
<https://pypi.org/project/hexkit>
|
|
71
75
|
|
|
72
76
|
You can install it from there using:
|
|
73
|
-
|
|
77
|
+
|
|
78
|
+
```sh
|
|
74
79
|
pip install hexkit
|
|
75
80
|
```
|
|
76
81
|
|
|
77
82
|
The following extras are available:
|
|
83
|
+
|
|
78
84
|
- `akafka`: when using the Apache Kafka-based event publishing or subscription
|
|
79
85
|
- `s3`: when interacting with S3-compatible Object Storages
|
|
80
86
|
- `mongodb`: when using mongodb as backend for the DAO protocol
|
|
@@ -84,6 +90,7 @@ The following extras are available:
|
|
|
84
90
|
- `all`: a union of all the above
|
|
85
91
|
|
|
86
92
|
## Development
|
|
93
|
+
|
|
87
94
|
For setting up the development environment, we rely on the
|
|
88
95
|
[devcontainer feature](https://code.visualstudio.com/docs/remote/containers) of vscode.
|
|
89
96
|
|
|
@@ -93,6 +100,7 @@ Then, you just have to open this repo in vscode and run the command
|
|
|
93
100
|
`Remote-Containers: Reopen in Container` from the vscode "Command Palette".
|
|
94
101
|
|
|
95
102
|
This will give you a full-fledged, pre-configured development environment including:
|
|
103
|
+
|
|
96
104
|
- infrastructural dependencies of the service (databases, etc.)
|
|
97
105
|
- all relevant vscode extensions pre-installed
|
|
98
106
|
- pre-configured linting and auto-formatting
|
|
@@ -101,12 +109,14 @@ This will give you a full-fledged, pre-configured development environment includ
|
|
|
101
109
|
|
|
102
110
|
Additionally, the following convenience command is available inside the devcontainer
|
|
103
111
|
(type it in the integrated terminal of VS Code):
|
|
112
|
+
|
|
104
113
|
- `dev_install` - install the lib with all development dependencies and pre-commit hooks
|
|
105
114
|
(please run that if you are starting the devcontainer for the first time
|
|
106
115
|
or if added any python dependencies to the [`./pyproject.toml`](./pyproject.toml))
|
|
107
116
|
|
|
108
117
|
If you prefer not to use vscode, you could get a similar setup (without the editor
|
|
109
118
|
specific features) by running the following commands:
|
|
119
|
+
|
|
110
120
|
``` bash
|
|
111
121
|
# Execute in the repo's root dir:
|
|
112
122
|
cd ./.devcontainer
|
|
@@ -118,4 +128,5 @@ docker run -it hexkit /bin/bash
|
|
|
118
128
|
```
|
|
119
129
|
|
|
120
130
|
## License
|
|
131
|
+
|
|
121
132
|
This repository is free to use and modify according to the [Apache 2.0 License](./LICENSE).
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Intended Audience :: Developers",
|
|
24
24
|
]
|
|
25
25
|
name = "hexkit"
|
|
26
|
-
version = "7.1
|
|
26
|
+
version = "7.2.1"
|
|
27
27
|
description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
|
|
28
28
|
dependencies = [
|
|
29
29
|
"opentelemetry-api >=1.37, <2",
|
|
@@ -63,13 +63,13 @@ def new_correlation_id() -> UUID4:
|
|
|
63
63
|
return uuid4()
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def validate_correlation_id(correlation_id: Any):
|
|
66
|
+
def validate_correlation_id(correlation_id: Any) -> None:
|
|
67
67
|
"""Validate the correlation ID.
|
|
68
68
|
|
|
69
69
|
Raises:
|
|
70
|
-
InvalidCorrelationIdError: If the correlation ID is not a
|
|
70
|
+
InvalidCorrelationIdError: If the correlation ID is not a UUID4.
|
|
71
71
|
"""
|
|
72
|
-
if not isinstance(correlation_id, UUID):
|
|
72
|
+
if not isinstance(correlation_id, UUID) or correlation_id.version != 4:
|
|
73
73
|
raise InvalidCorrelationIdError(correlation_id=correlation_id)
|
|
74
74
|
|
|
75
75
|
|
|
@@ -77,13 +77,18 @@ def correlation_id_from_str(correlation_id: str) -> UUID4:
|
|
|
77
77
|
"""Convert a string to a UUID4.
|
|
78
78
|
|
|
79
79
|
Raises:
|
|
80
|
-
InvalidCorrelationIdError: If the string is not a valid
|
|
80
|
+
InvalidCorrelationIdError: If the string is not a valid UUID4.
|
|
81
81
|
"""
|
|
82
82
|
try:
|
|
83
|
-
|
|
83
|
+
converted_id = UUID(correlation_id)
|
|
84
84
|
except ValueError as err:
|
|
85
85
|
raise InvalidCorrelationIdError(correlation_id=correlation_id) from err
|
|
86
86
|
|
|
87
|
+
if converted_id.version != 4:
|
|
88
|
+
raise InvalidCorrelationIdError(correlation_id=correlation_id)
|
|
89
|
+
|
|
90
|
+
return converted_id
|
|
91
|
+
|
|
87
92
|
|
|
88
93
|
@asynccontextmanager
|
|
89
94
|
async def set_correlation_id(correlation_id: UUID4):
|
|
@@ -21,3 +21,14 @@ the standard library.
|
|
|
21
21
|
|
|
22
22
|
ATTENTION: Do not use these providers in production.
|
|
23
23
|
"""
|
|
24
|
+
|
|
25
|
+
from .dao import MockDAOEmptyError, new_mock_dao_class
|
|
26
|
+
from .eventpub import InMemEventPublisher, InMemEventStore, TopicExhaustedError
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"InMemEventPublisher",
|
|
30
|
+
"InMemEventStore",
|
|
31
|
+
"MockDAOEmptyError",
|
|
32
|
+
"TopicExhaustedError",
|
|
33
|
+
"new_mock_dao_class",
|
|
34
|
+
]
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
"""A mock (in-memory) DAO"""
|
|
17
|
+
|
|
18
|
+
from collections.abc import AsyncIterator, Mapping
|
|
19
|
+
from contextlib import suppress
|
|
20
|
+
from copy import deepcopy
|
|
21
|
+
from typing import Any, Generic, TypeVar
|
|
22
|
+
from unittest.mock import AsyncMock, Mock
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel
|
|
25
|
+
|
|
26
|
+
from hexkit.custom_types import ID
|
|
27
|
+
from hexkit.protocols.dao import (
|
|
28
|
+
MultipleHitsFoundError,
|
|
29
|
+
NoHitsFoundError,
|
|
30
|
+
ResourceAlreadyExistsError,
|
|
31
|
+
ResourceNotFoundError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = ["MockDAOEmptyError", "new_mock_dao_class"]
|
|
35
|
+
|
|
36
|
+
DTO = TypeVar("DTO", bound=BaseModel)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MockDAOEmptyError(RuntimeError):
|
|
40
|
+
"""Raised when attempting to access the `latest` property of an empty mock DAO"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BaseInMemDao(Generic[DTO]):
|
|
44
|
+
"""DAO with proper typing and in-memory storage for use in testing"""
|
|
45
|
+
|
|
46
|
+
_id_field: str
|
|
47
|
+
publish_pending = AsyncMock()
|
|
48
|
+
republish = AsyncMock()
|
|
49
|
+
with_transaction = Mock()
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self.resources: dict[ID, DTO] = {}
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def latest(self) -> DTO:
|
|
56
|
+
"""Return the most recently inserted resource.
|
|
57
|
+
|
|
58
|
+
Raises a MockDAOEmptyError if there are no resources stored.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
return deepcopy(next(reversed(self.resources.values())))
|
|
62
|
+
except StopIteration as err:
|
|
63
|
+
raise MockDAOEmptyError() from err
|
|
64
|
+
|
|
65
|
+
async def get_by_id(self, id_: ID) -> DTO:
|
|
66
|
+
"""Get the resource via ID.
|
|
67
|
+
|
|
68
|
+
Raises a ResourceNotFoundError if no resource with a matching ID is found.
|
|
69
|
+
"""
|
|
70
|
+
with suppress(KeyError):
|
|
71
|
+
return deepcopy(self.resources[id_])
|
|
72
|
+
raise ResourceNotFoundError(id_=id_)
|
|
73
|
+
|
|
74
|
+
async def find_one(self, *, mapping: Mapping[str, Any]) -> DTO:
|
|
75
|
+
"""Find the resource that matches the specified mapping.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
NoHitsFoundError: If no matching resource is found.
|
|
79
|
+
MultipleHitsFoundError: If more than one matching resource is found.
|
|
80
|
+
"""
|
|
81
|
+
hits = self.find_all(mapping=mapping)
|
|
82
|
+
try:
|
|
83
|
+
dto = await hits.__anext__()
|
|
84
|
+
except StopAsyncIteration as error:
|
|
85
|
+
raise NoHitsFoundError(mapping=mapping) from error
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
_ = await hits.__anext__()
|
|
89
|
+
except StopAsyncIteration:
|
|
90
|
+
# This is expected:
|
|
91
|
+
return dto
|
|
92
|
+
|
|
93
|
+
raise MultipleHitsFoundError(mapping=mapping)
|
|
94
|
+
|
|
95
|
+
async def find_all(self, *, mapping: Mapping[str, Any]) -> AsyncIterator[DTO]:
|
|
96
|
+
"""Find all resources that match the specified mapping."""
|
|
97
|
+
for resource in self.resources.values():
|
|
98
|
+
if all(getattr(resource, k) == v for k, v in mapping.items()):
|
|
99
|
+
yield deepcopy(resource)
|
|
100
|
+
|
|
101
|
+
async def insert(self, dto: DTO) -> None:
|
|
102
|
+
"""Insert a resource.
|
|
103
|
+
|
|
104
|
+
Raises a ResourceAlreadyExistsError if a resource with a matching ID exists already.
|
|
105
|
+
"""
|
|
106
|
+
dto_id = getattr(dto, self._id_field)
|
|
107
|
+
if dto_id in self.resources:
|
|
108
|
+
raise ResourceAlreadyExistsError(id_=dto_id)
|
|
109
|
+
self.resources[dto_id] = deepcopy(dto)
|
|
110
|
+
|
|
111
|
+
async def update(self, dto: DTO) -> None:
|
|
112
|
+
"""Update a resource.
|
|
113
|
+
|
|
114
|
+
Raises a ResourceNotFoundError if no resource with a matching ID is found.
|
|
115
|
+
"""
|
|
116
|
+
dto_id = getattr(dto, self._id_field)
|
|
117
|
+
if dto_id not in self.resources:
|
|
118
|
+
raise ResourceNotFoundError(id_=getattr(dto, self._id_field))
|
|
119
|
+
self.resources[dto_id] = deepcopy(dto)
|
|
120
|
+
|
|
121
|
+
async def delete(self, id_: ID) -> None:
|
|
122
|
+
"""Delete a resource by ID.
|
|
123
|
+
|
|
124
|
+
Raises a ResourceNotFoundError if no resource with a matching ID is found.
|
|
125
|
+
"""
|
|
126
|
+
if id_ not in self.resources:
|
|
127
|
+
raise ResourceNotFoundError(id_=id_)
|
|
128
|
+
del self.resources[id_]
|
|
129
|
+
|
|
130
|
+
async def upsert(self, dto: DTO) -> None:
|
|
131
|
+
"""Upsert a resource."""
|
|
132
|
+
dto_id = getattr(dto, self._id_field)
|
|
133
|
+
self.resources[dto_id] = deepcopy(dto)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def new_mock_dao_class(
|
|
137
|
+
*, dto_model: type[DTO], id_field: str
|
|
138
|
+
) -> type[BaseInMemDao[DTO]]:
|
|
139
|
+
"""Produce a mock DAO for the given DTO model and ID field"""
|
|
140
|
+
|
|
141
|
+
class MockDao(BaseInMemDao[DTO]):
|
|
142
|
+
"""Mock dao that stores data in memory"""
|
|
143
|
+
|
|
144
|
+
_id_field: str = id_field
|
|
145
|
+
|
|
146
|
+
return MockDao
|
|
@@ -0,0 +1,402 @@
|
|
|
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
|
+
"""Mock object storage class"""
|
|
17
|
+
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
from uuid import uuid4
|
|
20
|
+
|
|
21
|
+
from hexkit.protocols.objstorage import ObjectStorageProtocol, PresignedPostURL
|
|
22
|
+
from hexkit.providers.s3 import S3Config
|
|
23
|
+
|
|
24
|
+
UploadID = str
|
|
25
|
+
BucketID = str
|
|
26
|
+
ObjectID = str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InMemObjectStorage(ObjectStorageProtocol):
|
|
30
|
+
"""In-memory object storage mock just the method used in this service"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, *, config: S3Config):
|
|
33
|
+
"""Set bucket ID.
|
|
34
|
+
|
|
35
|
+
Type aliases are used here only to clarify dictionary structure.
|
|
36
|
+
"""
|
|
37
|
+
self.buckets: dict[BucketID, set[ObjectID]] = defaultdict(set)
|
|
38
|
+
self.uploads: dict[BucketID, dict[ObjectID, set[UploadID]]] = defaultdict(dict)
|
|
39
|
+
self.endpoint_url = config.s3_endpoint_url
|
|
40
|
+
|
|
41
|
+
async def _does_bucket_exist(self, bucket_id: str) -> bool:
|
|
42
|
+
"""Check whether a bucket with the specified ID (`bucket_id`) exists.
|
|
43
|
+
|
|
44
|
+
Returns `True` if it exists and `False` otherwise.
|
|
45
|
+
"""
|
|
46
|
+
return bucket_id in self.buckets
|
|
47
|
+
|
|
48
|
+
async def _assert_bucket_exists(self, bucket_id: str):
|
|
49
|
+
"""Assert that the bucket with the specified ID (`bucket_id`) exists.
|
|
50
|
+
|
|
51
|
+
Raises a `BucketNotFoundError` if the bucket does not exist.
|
|
52
|
+
"""
|
|
53
|
+
if not await self.does_bucket_exist(bucket_id):
|
|
54
|
+
raise self.BucketNotFoundError(bucket_id=bucket_id)
|
|
55
|
+
|
|
56
|
+
async def _create_bucket(self, bucket_id: str) -> None:
|
|
57
|
+
"""Create a bucket (= a structure that can hold multiple file objects) with the
|
|
58
|
+
specified unique ID.
|
|
59
|
+
|
|
60
|
+
Raises a `BucketAlreadyExistsError` if the bucket already exists.
|
|
61
|
+
"""
|
|
62
|
+
if bucket_id in self.buckets:
|
|
63
|
+
raise self.BucketAlreadyExistsError(bucket_id=bucket_id)
|
|
64
|
+
self.buckets[bucket_id] = set()
|
|
65
|
+
|
|
66
|
+
async def _delete_bucket(
|
|
67
|
+
self, bucket_id: str, *, delete_content: bool = False
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Delete a bucket with the specified unique ID.
|
|
71
|
+
|
|
72
|
+
Raises a `BucketNotEmptyError` if the bucket is not empty and `delete_content`
|
|
73
|
+
is set to False.
|
|
74
|
+
"""
|
|
75
|
+
await self._assert_bucket_exists(bucket_id)
|
|
76
|
+
if self.buckets[bucket_id] and not delete_content:
|
|
77
|
+
raise self.BucketNotEmptyError(bucket_id)
|
|
78
|
+
del self.buckets[bucket_id]
|
|
79
|
+
|
|
80
|
+
async def _list_all_object_ids(self, *, bucket_id: str) -> list[str]:
|
|
81
|
+
"""Retrieve a list of IDs for all objects currently present in the specified bucket.
|
|
82
|
+
|
|
83
|
+
Raises a `BucketNotFoundError` if the bucket does not exist.
|
|
84
|
+
"""
|
|
85
|
+
await self._assert_bucket_exists(bucket_id)
|
|
86
|
+
return sorted(self.buckets[bucket_id])
|
|
87
|
+
|
|
88
|
+
async def _does_object_exist(
|
|
89
|
+
self, *, bucket_id: str, object_id: str, object_md5sum: str | None = None
|
|
90
|
+
) -> bool:
|
|
91
|
+
"""Check whether an object with specified ID (`object_id`) exists in the bucket
|
|
92
|
+
with the specified id (`bucket_id`). Optionally, a md5 checksum (`object_md5sum`)
|
|
93
|
+
may be provided to check the objects content.
|
|
94
|
+
Return `True` if checks succeed and `False` otherwise.
|
|
95
|
+
"""
|
|
96
|
+
if object_md5sum is not None:
|
|
97
|
+
raise NotImplementedError("MD5 checking is not yet implemented.")
|
|
98
|
+
return object_id in self.buckets[bucket_id]
|
|
99
|
+
|
|
100
|
+
async def _assert_object_exists(self, *, bucket_id: str, object_id: str) -> None:
|
|
101
|
+
"""Asserts that the file with specified ID exists in the given bucket.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
105
|
+
`ObjectNotFoundError`: If the object does not exist in the bucket.
|
|
106
|
+
"""
|
|
107
|
+
# first check if bucket exists:
|
|
108
|
+
await self._assert_bucket_exists(bucket_id)
|
|
109
|
+
|
|
110
|
+
if not await self.does_object_exist(bucket_id=bucket_id, object_id=object_id):
|
|
111
|
+
raise self.ObjectNotFoundError(bucket_id=bucket_id, object_id=object_id)
|
|
112
|
+
|
|
113
|
+
async def _assert_object_not_exists(
|
|
114
|
+
self, *, bucket_id: str, object_id: str
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Asserts that the file with specified ID does NOT exist in the given bucket.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
120
|
+
`ObjectAlreadyExistsError`: If the object exists in the bucket.
|
|
121
|
+
"""
|
|
122
|
+
# first check if bucket exists:
|
|
123
|
+
await self._assert_bucket_exists(bucket_id)
|
|
124
|
+
|
|
125
|
+
if await self.does_object_exist(bucket_id=bucket_id, object_id=object_id):
|
|
126
|
+
raise self.ObjectAlreadyExistsError(
|
|
127
|
+
bucket_id=bucket_id, object_id=object_id
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def _get_object_upload_url(
|
|
131
|
+
self,
|
|
132
|
+
*,
|
|
133
|
+
bucket_id: str,
|
|
134
|
+
object_id: str,
|
|
135
|
+
expires_after: int = 86400,
|
|
136
|
+
max_upload_size: int | None = None,
|
|
137
|
+
) -> PresignedPostURL:
|
|
138
|
+
"""Generates and returns a FAKE HTTP URL to upload a new file object with the
|
|
139
|
+
given id (`object_id`) to the bucket with the specified id (`bucket_id`).
|
|
140
|
+
You may also specify a custom expiry duration in seconds (`expires_after`) and
|
|
141
|
+
a maximum size (bytes) for uploads (`max_upload_size`).
|
|
142
|
+
|
|
143
|
+
This URL does not point to an actual resource or API, it is merely for testing.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
147
|
+
`ObjectAlreadyExistsError`: If the object exists in the bucket.
|
|
148
|
+
"""
|
|
149
|
+
await self._assert_object_not_exists(bucket_id=bucket_id, object_id=object_id)
|
|
150
|
+
fields: dict[str, str] = {"expires_after": str(expires_after)}
|
|
151
|
+
if max_upload_size:
|
|
152
|
+
fields["max_upload_size"] = str(max_upload_size)
|
|
153
|
+
return PresignedPostURL(
|
|
154
|
+
url=f"https://s3.example.com/{bucket_id}/{object_id}", fields=fields
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
async def _list_multipart_upload_for_object(
|
|
158
|
+
self, *, bucket_id: str, object_id: str
|
|
159
|
+
) -> list[str]:
|
|
160
|
+
"""Lists all active multipart uploads for the given object. Returns a list of
|
|
161
|
+
their IDs.
|
|
162
|
+
|
|
163
|
+
(S3 allows multiple ongoing multi-part uploads.)
|
|
164
|
+
|
|
165
|
+
Raises a `BucketNotFoundError` if the bucket does not exist.
|
|
166
|
+
"""
|
|
167
|
+
await self._assert_bucket_exists(bucket_id)
|
|
168
|
+
uploads_for_object = self.uploads[bucket_id].get(object_id, set())
|
|
169
|
+
return sorted(uploads_for_object)
|
|
170
|
+
|
|
171
|
+
async def _assert_no_multipart_upload(self, *, bucket_id: str, object_id: str):
|
|
172
|
+
"""Ensure that there are no active multi-part uploads for the given object.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
`MultiPartUploadAlreadyExistsError`: If an upload exists for the object.
|
|
176
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
177
|
+
"""
|
|
178
|
+
upload_ids = await self._list_multipart_upload_for_object(
|
|
179
|
+
bucket_id=bucket_id, object_id=object_id
|
|
180
|
+
)
|
|
181
|
+
if upload_ids:
|
|
182
|
+
raise self.MultiPartUploadAlreadyExistsError(
|
|
183
|
+
bucket_id=bucket_id, object_id=object_id
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
async def _assert_multipart_upload_exists(
|
|
187
|
+
self,
|
|
188
|
+
*,
|
|
189
|
+
upload_id: str,
|
|
190
|
+
bucket_id: str,
|
|
191
|
+
object_id: str,
|
|
192
|
+
assert_exclusiveness: bool = True,
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Checks if a multipart upload with the given ID exists and whether it maps
|
|
195
|
+
to the specified object and bucket.
|
|
196
|
+
|
|
197
|
+
By default, also verifies that this upload is the only upload active for
|
|
198
|
+
that file.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
202
|
+
`MultiPartUploadNotFoundError`: If the upload ID doesn't exist.
|
|
203
|
+
`MultipleActiveUploadsError`: If `assert_exclusiveness` is True and there are
|
|
204
|
+
multiple open uploads for the object ID.
|
|
205
|
+
"""
|
|
206
|
+
upload_ids = await self._list_multipart_upload_for_object(
|
|
207
|
+
bucket_id=bucket_id, object_id=object_id
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if assert_exclusiveness and len(upload_ids) > 1:
|
|
211
|
+
raise self.MultipleActiveUploadsError(
|
|
212
|
+
bucket_id=bucket_id, object_id=object_id, upload_ids=upload_ids
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if upload_id not in upload_ids:
|
|
216
|
+
raise self.MultiPartUploadNotFoundError(
|
|
217
|
+
upload_id=upload_id,
|
|
218
|
+
bucket_id=bucket_id,
|
|
219
|
+
object_id=object_id,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
async def _delete_object(self, *, bucket_id: str, object_id: str) -> None:
|
|
223
|
+
"""Delete an object with the specified id (`object_id`) in the bucket with the
|
|
224
|
+
specified id (`bucket_id`).
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
228
|
+
`ObjectNotFoundError`: If the object does not exist in the bucket.
|
|
229
|
+
"""
|
|
230
|
+
await self._assert_object_exists(bucket_id=bucket_id, object_id=object_id)
|
|
231
|
+
self.buckets[bucket_id].remove(object_id)
|
|
232
|
+
|
|
233
|
+
async def _init_multipart_upload(self, *, bucket_id: str, object_id: str) -> str:
|
|
234
|
+
"""Initiates a multipart upload procedure. Returns the upload ID.
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
`MultiPartUploadAlreadyExistsError`: If an upload exists for the object.
|
|
238
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
239
|
+
"""
|
|
240
|
+
await self._assert_no_multipart_upload(bucket_id=bucket_id, object_id=object_id)
|
|
241
|
+
upload_id = str(uuid4())
|
|
242
|
+
if object_id not in self.buckets[bucket_id]:
|
|
243
|
+
self.uploads[bucket_id][object_id] = set()
|
|
244
|
+
self.uploads[bucket_id][object_id].add(upload_id)
|
|
245
|
+
return upload_id
|
|
246
|
+
|
|
247
|
+
async def _get_part_upload_url( # noqa: PLR0913
|
|
248
|
+
self,
|
|
249
|
+
*,
|
|
250
|
+
upload_id: str,
|
|
251
|
+
bucket_id: str,
|
|
252
|
+
object_id: str,
|
|
253
|
+
part_number: int,
|
|
254
|
+
expires_after: int = 3600,
|
|
255
|
+
part_md5: str | None = None,
|
|
256
|
+
) -> str:
|
|
257
|
+
"""
|
|
258
|
+
Return a FAKE presigned part upload URL based on the bucket, object, and part no.
|
|
259
|
+
|
|
260
|
+
This URL does not point to an actual resource or API, it is merely for testing.
|
|
261
|
+
|
|
262
|
+
You may also specify a custom expiry duration in seconds (`expires_after`).
|
|
263
|
+
Please note: the part number must be a non-zero, positive integer.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
267
|
+
`MultiPartUploadNotFoundError`: If the upload ID doesn't exist.
|
|
268
|
+
`MultipleActiveUploadsError`: If `assert_exclusiveness` is True and there
|
|
269
|
+
are multiple open uploads for the object ID.
|
|
270
|
+
`ValueError`: If `part_number` is less than 1 or exceeds MAX_FILE_PART_NUMBER.
|
|
271
|
+
"""
|
|
272
|
+
if not 0 < part_number <= self.MAX_FILE_PART_NUMBER:
|
|
273
|
+
raise ValueError(
|
|
274
|
+
"The part number must be a non-zero positive integer"
|
|
275
|
+
+ f" smaller or equal to {self.MAX_FILE_PART_NUMBER}"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
await self._assert_multipart_upload_exists(
|
|
279
|
+
upload_id=upload_id, bucket_id=bucket_id, object_id=object_id
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
params = {
|
|
283
|
+
"Bucket": bucket_id,
|
|
284
|
+
"Key": object_id,
|
|
285
|
+
"UploadId": upload_id,
|
|
286
|
+
"PartNumber": part_number,
|
|
287
|
+
}
|
|
288
|
+
# add additional parameters if any were passed
|
|
289
|
+
if part_md5:
|
|
290
|
+
params["ContentMD5"] = part_md5
|
|
291
|
+
return (
|
|
292
|
+
f"https://s3.example.com/{bucket_id}/{object_id}/{upload_id}"
|
|
293
|
+
+ f"/part_no_{part_number}?{expires_after=}"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
async def _abort_multipart_upload(
|
|
297
|
+
self,
|
|
298
|
+
*,
|
|
299
|
+
upload_id: str,
|
|
300
|
+
bucket_id: str,
|
|
301
|
+
object_id: str,
|
|
302
|
+
) -> None:
|
|
303
|
+
"""Abort a multipart upload with the specified ID.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
307
|
+
`MultiPartUploadNotFoundError`: If the upload ID doesn't exist.
|
|
308
|
+
`MultipleActiveUploadsError`: If `assert_exclusiveness` is True and there are
|
|
309
|
+
multiple open uploads for the object ID.
|
|
310
|
+
"""
|
|
311
|
+
await self._assert_multipart_upload_exists(
|
|
312
|
+
upload_id=upload_id,
|
|
313
|
+
bucket_id=bucket_id,
|
|
314
|
+
object_id=object_id,
|
|
315
|
+
assert_exclusiveness=False,
|
|
316
|
+
)
|
|
317
|
+
self.uploads[bucket_id][object_id].remove(upload_id)
|
|
318
|
+
|
|
319
|
+
async def _complete_multipart_upload(
|
|
320
|
+
self,
|
|
321
|
+
*,
|
|
322
|
+
upload_id: str,
|
|
323
|
+
bucket_id: str,
|
|
324
|
+
object_id: str,
|
|
325
|
+
anticipated_part_quantity: int | None = None,
|
|
326
|
+
anticipated_part_size: int | None = None,
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Completes a multipart upload with the specified ID."""
|
|
329
|
+
# Calling this abort function because in the mock, the logic is identical to
|
|
330
|
+
# what we would write here anyway -- in real life, completing an upload does not
|
|
331
|
+
# involve the abort logic
|
|
332
|
+
await self._abort_multipart_upload(
|
|
333
|
+
upload_id=upload_id,
|
|
334
|
+
bucket_id=bucket_id,
|
|
335
|
+
object_id=object_id,
|
|
336
|
+
)
|
|
337
|
+
self.buckets[bucket_id].add(object_id)
|
|
338
|
+
|
|
339
|
+
async def _get_object_download_url(
|
|
340
|
+
self, *, bucket_id: str, object_id: str, expires_after: int = 86400
|
|
341
|
+
) -> str:
|
|
342
|
+
"""Generates and returns a FAKE presigned HTTP-URL to download a file object with
|
|
343
|
+
the specified ID (`object_id`) from bucket with the specified id (`bucket_id`).
|
|
344
|
+
You may also specify a custom expiry duration in seconds (`expires_after`).
|
|
345
|
+
|
|
346
|
+
This URL does not point to an actual resource or API, it is merely for testing.
|
|
347
|
+
|
|
348
|
+
Raises:
|
|
349
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
350
|
+
`ObjectNotFoundError`: If the object does not exist in the bucket.
|
|
351
|
+
"""
|
|
352
|
+
await self._assert_object_exists(bucket_id=bucket_id, object_id=object_id)
|
|
353
|
+
return f"https://s3.example.com/{bucket_id}/{object_id}?{expires_after=}"
|
|
354
|
+
|
|
355
|
+
async def _get_object_etag(self, *, bucket_id: str, object_id: str) -> str:
|
|
356
|
+
"""Return the etag of an object, which is hardcoded to be the string
|
|
357
|
+
`"etag_for_{object_id}"`.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
361
|
+
`ObjectNotFoundError`: If the object does not exist in the bucket.
|
|
362
|
+
"""
|
|
363
|
+
await self._assert_object_exists(bucket_id=bucket_id, object_id=object_id)
|
|
364
|
+
return f"etag_for_{object_id}"
|
|
365
|
+
|
|
366
|
+
async def _get_object_size(self, *, bucket_id: str, object_id: str) -> int:
|
|
367
|
+
"""Returns the size of an object in bytes, which is hardcoded to `1024` for
|
|
368
|
+
testing purposes.
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
`BucketNotFoundError`: If the bucket does not exist.
|
|
372
|
+
`ObjectNotFoundError`: If the object does not exist in the bucket.
|
|
373
|
+
"""
|
|
374
|
+
await self._assert_object_exists(bucket_id=bucket_id, object_id=object_id)
|
|
375
|
+
return 1024
|
|
376
|
+
|
|
377
|
+
async def _copy_object(
|
|
378
|
+
self,
|
|
379
|
+
*,
|
|
380
|
+
source_bucket_id: str,
|
|
381
|
+
source_object_id: str,
|
|
382
|
+
dest_bucket_id: str,
|
|
383
|
+
dest_object_id: str,
|
|
384
|
+
abort_failed: bool = True,
|
|
385
|
+
) -> None:
|
|
386
|
+
"""Copy an object from one bucket (`source_bucket_id` and `source_object_id`) to
|
|
387
|
+
another bucket (`dest_bucket_id` and `dest_object_id`).
|
|
388
|
+
This operation always succeeds if the object exists in the source bucket and
|
|
389
|
+
does not exist in the dest bucket.
|
|
390
|
+
|
|
391
|
+
Raises:
|
|
392
|
+
`BucketNotFoundError`: If the source or dest bucket does not exist.
|
|
393
|
+
`ObjectAlreadyExistsError`: If the object already exists in the dest bucket.
|
|
394
|
+
`ObjectNotFoundError`: If the object does not exist in the source bucket.
|
|
395
|
+
"""
|
|
396
|
+
await self._assert_object_not_exists(
|
|
397
|
+
bucket_id=dest_bucket_id, object_id=dest_object_id
|
|
398
|
+
)
|
|
399
|
+
await self._assert_object_exists(
|
|
400
|
+
bucket_id=source_bucket_id, object_id=source_object_id
|
|
401
|
+
)
|
|
402
|
+
self.buckets[dest_bucket_id].add(dest_object_id)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexkit
|
|
3
|
-
Version: 7.1
|
|
3
|
+
Version: 7.2.1
|
|
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
|
|
@@ -78,10 +78,12 @@ Dynamic: license-file
|
|
|
78
78
|
[](https://coveralls.io/github/ghga-de/hexkit?branch=main)
|
|
79
79
|
|
|
80
80
|
# hexkit
|
|
81
|
+
|
|
81
82
|
A chassis library for building domain-focused, infrastructure-agnostic, and event-driven
|
|
82
83
|
microservices in Python.
|
|
83
84
|
|
|
84
85
|
## In a Nutshell
|
|
86
|
+
|
|
85
87
|
This project implements the Triple Hexagonal Architecture pattern which is an
|
|
86
88
|
optimization of the ordinary
|
|
87
89
|
[Hexagonal Architecture](https://alistair.cockburn.us/hexagonal-architecture/) for
|
|
@@ -110,6 +112,7 @@ a limited collection of protocol-provider pairs that are of immediate interest t
|
|
|
110
112
|
authors. We like to add support for more protocols and technologies over time.
|
|
111
113
|
|
|
112
114
|
The following protocols and providers are currently available:
|
|
115
|
+
|
|
113
116
|
| Protocol | Providers |
|
|
114
117
|
|---|---|
|
|
115
118
|
| Event Publishing | Apache Kafka |
|
|
@@ -140,15 +143,18 @@ of jumping directly into it. You can find the protocols being defined at
|
|
|
140
143
|
A more elaborate documentation and tutorial is on the way.
|
|
141
144
|
|
|
142
145
|
## Installation
|
|
146
|
+
|
|
143
147
|
This package is available at PyPI:
|
|
144
|
-
https://pypi.org/project/hexkit
|
|
148
|
+
<https://pypi.org/project/hexkit>
|
|
145
149
|
|
|
146
150
|
You can install it from there using:
|
|
147
|
-
|
|
151
|
+
|
|
152
|
+
```sh
|
|
148
153
|
pip install hexkit
|
|
149
154
|
```
|
|
150
155
|
|
|
151
156
|
The following extras are available:
|
|
157
|
+
|
|
152
158
|
- `akafka`: when using the Apache Kafka-based event publishing or subscription
|
|
153
159
|
- `s3`: when interacting with S3-compatible Object Storages
|
|
154
160
|
- `mongodb`: when using mongodb as backend for the DAO protocol
|
|
@@ -158,6 +164,7 @@ The following extras are available:
|
|
|
158
164
|
- `all`: a union of all the above
|
|
159
165
|
|
|
160
166
|
## Development
|
|
167
|
+
|
|
161
168
|
For setting up the development environment, we rely on the
|
|
162
169
|
[devcontainer feature](https://code.visualstudio.com/docs/remote/containers) of vscode.
|
|
163
170
|
|
|
@@ -167,6 +174,7 @@ Then, you just have to open this repo in vscode and run the command
|
|
|
167
174
|
`Remote-Containers: Reopen in Container` from the vscode "Command Palette".
|
|
168
175
|
|
|
169
176
|
This will give you a full-fledged, pre-configured development environment including:
|
|
177
|
+
|
|
170
178
|
- infrastructural dependencies of the service (databases, etc.)
|
|
171
179
|
- all relevant vscode extensions pre-installed
|
|
172
180
|
- pre-configured linting and auto-formatting
|
|
@@ -175,12 +183,14 @@ This will give you a full-fledged, pre-configured development environment includ
|
|
|
175
183
|
|
|
176
184
|
Additionally, the following convenience command is available inside the devcontainer
|
|
177
185
|
(type it in the integrated terminal of VS Code):
|
|
186
|
+
|
|
178
187
|
- `dev_install` - install the lib with all development dependencies and pre-commit hooks
|
|
179
188
|
(please run that if you are starting the devcontainer for the first time
|
|
180
189
|
or if added any python dependencies to the [`./pyproject.toml`](./pyproject.toml))
|
|
181
190
|
|
|
182
191
|
If you prefer not to use vscode, you could get a similar setup (without the editor
|
|
183
192
|
specific features) by running the following commands:
|
|
193
|
+
|
|
184
194
|
``` bash
|
|
185
195
|
# Execute in the repo's root dir:
|
|
186
196
|
cd ./.devcontainer
|
|
@@ -192,4 +202,5 @@ docker run -it hexkit /bin/bash
|
|
|
192
202
|
```
|
|
193
203
|
|
|
194
204
|
## License
|
|
205
|
+
|
|
195
206
|
This repository is free to use and modify according to the [Apache 2.0 License](./LICENSE).
|
|
@@ -59,4 +59,6 @@ src/hexkit/providers/s3/testutils/_fixtures.py
|
|
|
59
59
|
src/hexkit/providers/s3/testutils/_typical_workflow.py
|
|
60
60
|
src/hexkit/providers/s3/testutils/_utils.py
|
|
61
61
|
src/hexkit/providers/testing/__init__.py
|
|
62
|
-
src/hexkit/providers/testing/
|
|
62
|
+
src/hexkit/providers/testing/dao.py
|
|
63
|
+
src/hexkit/providers/testing/eventpub.py
|
|
64
|
+
src/hexkit/providers/testing/s3.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|