hexkit 0.10.2__tar.gz → 0.11.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-0.10.2 → hexkit-0.11.1}/PKG-INFO +23 -7
- hexkit-0.11.1/pyproject.toml +52 -0
- hexkit-0.11.1/setup.cfg +4 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/__init__.py +3 -1
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/config.py +30 -37
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/custom_types.py +5 -1
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/inject.py +13 -15
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/protocols/dao.py +22 -17
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/protocols/objstorage.py +8 -24
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/akafka/__init__.py +5 -2
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/akafka/provider.py +9 -14
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/akafka/testutils.py +29 -40
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/mongodb/__init__.py +1 -1
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/mongodb/provider.py +30 -35
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/mongodb/testutils.py +6 -7
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/s3/__init__.py +1 -1
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/s3/provider.py +23 -58
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/s3/testutils.py +36 -56
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/testing/eventpub.py +1 -5
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/testing/utils.py +5 -4
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/utils.py +5 -5
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit.egg-info/PKG-INFO +23 -7
- hexkit-0.11.1/src/hexkit.egg-info/SOURCES.txt +38 -0
- hexkit-0.11.1/src/hexkit.egg-info/requires.txt +33 -0
- hexkit-0.11.1/src/hexkit.egg-info/top_level.txt +1 -0
- hexkit-0.10.2/hexkit.egg-info/SOURCES.txt +0 -74
- hexkit-0.10.2/hexkit.egg-info/not-zip-safe +0 -1
- hexkit-0.10.2/hexkit.egg-info/requires.txt +0 -48
- hexkit-0.10.2/hexkit.egg-info/top_level.txt +0 -3
- hexkit-0.10.2/scripts/__init__.py +0 -17
- hexkit-0.10.2/scripts/get_package_name.py +0 -46
- hexkit-0.10.2/scripts/license_checker.py +0 -562
- hexkit-0.10.2/scripts/script_utils/__init__.py +0 -17
- hexkit-0.10.2/scripts/script_utils/cli.py +0 -36
- hexkit-0.10.2/scripts/update_template_files.py +0 -250
- hexkit-0.10.2/setup.cfg +0 -69
- hexkit-0.10.2/setup.py +0 -23
- hexkit-0.10.2/tests/fixtures/__init__.py +0 -20
- hexkit-0.10.2/tests/fixtures/config.py +0 -104
- hexkit-0.10.2/tests/fixtures/config_env_var_sets.yaml +0 -12
- hexkit-0.10.2/tests/fixtures/dummy_joint.py +0 -38
- hexkit-0.10.2/tests/fixtures/inject.py +0 -140
- hexkit-0.10.2/tests/fixtures/test_files/__init__.py +0 -14
- hexkit-0.10.2/tests/fixtures/test_files/test_file1.yaml +0 -3
- hexkit-0.10.2/tests/fixtures/test_files/test_file2.yaml +0 -3
- hexkit-0.10.2/tests/fixtures/test_files/test_file3.yaml +0 -3
- hexkit-0.10.2/tests/fixtures/test_files/test_file4.yaml +0 -3
- hexkit-0.10.2/tests/fixtures/utils.py +0 -35
- hexkit-0.10.2/tests/integration/__init__.py +0 -17
- hexkit-0.10.2/tests/integration/test_akafka.py +0 -97
- hexkit-0.10.2/tests/integration/test_akafka_testutils.py +0 -255
- hexkit-0.10.2/tests/integration/test_event_loop_default.py +0 -35
- hexkit-0.10.2/tests/integration/test_event_loop_overridden.py +0 -35
- hexkit-0.10.2/tests/integration/test_inject.py +0 -177
- hexkit-0.10.2/tests/integration/test_mongodb.py +0 -366
- hexkit-0.10.2/tests/integration/test_s3.py +0 -595
- hexkit-0.10.2/tests/unit/__init__.py +0 -17
- hexkit-0.10.2/tests/unit/test_akafka.py +0 -193
- hexkit-0.10.2/tests/unit/test_config.py +0 -128
- hexkit-0.10.2/tests/unit/test_dao.py +0 -164
- hexkit-0.10.2/tests/unit/test_eventpub.py +0 -76
- hexkit-0.10.2/tests/unit/test_eventsub.py +0 -67
- hexkit-0.10.2/tests/unit/test_inject.py +0 -146
- hexkit-0.10.2/tests/unit/test_testing_eventpub.py +0 -120
- hexkit-0.10.2/tests/unit/test_utils.py +0 -100
- {hexkit-0.10.2 → hexkit-0.11.1}/LICENSE +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1}/README.md +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/__main__.py +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/base.py +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/protocols/__init__.py +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/protocols/eventpub.py +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/protocols/eventsub.py +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/__init__.py +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit/providers/testing/__init__.py +0 -0
- {hexkit-0.10.2 → hexkit-0.11.1/src}/hexkit.egg-info/dependency_links.txt +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: hexkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.1
|
|
4
4
|
Summary: A Toolkit for Building Microservices using the Hexagonal Architecture
|
|
5
|
-
|
|
6
|
-
Author: German Human Genome Phenome Archive (GHGA)
|
|
7
|
-
Author-email: contact@ghga.de
|
|
5
|
+
Author-email: "German Human Genome Phenome Archive (GHGA)" <contact@ghga.de>
|
|
8
6
|
License: Apache 2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/ghga-de/hexkit
|
|
9
8
|
Classifier: Development Status :: 1 - Planning
|
|
10
9
|
Classifier: Operating System :: POSIX :: Linux
|
|
11
10
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -16,16 +15,33 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
|
16
15
|
Classifier: Topic :: Software Development :: Libraries
|
|
17
16
|
Classifier: Intended Audience :: Developers
|
|
18
17
|
Requires-Python: >=3.9
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: pydantic<3,>=2
|
|
21
|
+
Requires-Dist: pydantic_settings<3,>=2
|
|
22
|
+
Requires-Dist: PyYAML<7,>=6.0
|
|
23
|
+
Requires-Dist: dependency-injector<5,>=4.41.0
|
|
20
24
|
Provides-Extra: akafka
|
|
25
|
+
Requires-Dist: aiokafka~=0.8.0; extra == "akafka"
|
|
26
|
+
Requires-Dist: jsonschema<5,>=4.17.3; extra == "akafka"
|
|
21
27
|
Provides-Extra: s3
|
|
28
|
+
Requires-Dist: boto3<2,>=1.26.50; extra == "s3"
|
|
29
|
+
Requires-Dist: botocore<2,>=1.29.50; extra == "s3"
|
|
22
30
|
Provides-Extra: mongodb
|
|
31
|
+
Requires-Dist: motor<4,>=3.1.1; extra == "mongodb"
|
|
23
32
|
Provides-Extra: test-akafka
|
|
33
|
+
Requires-Dist: hexkit[akafka]; extra == "test-akafka"
|
|
34
|
+
Requires-Dist: testcontainers[kafka]<4,>=3.4.1; extra == "test-akafka"
|
|
24
35
|
Provides-Extra: test-s3
|
|
36
|
+
Requires-Dist: hexkit[s3]; extra == "test-s3"
|
|
37
|
+
Requires-Dist: testcontainers<4,>=3.4.1; extra == "test-s3"
|
|
25
38
|
Provides-Extra: test-mongodb
|
|
26
|
-
|
|
39
|
+
Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
|
|
40
|
+
Requires-Dist: testcontainers[mongo]<4,>=3.4.1; extra == "test-mongodb"
|
|
41
|
+
Provides-Extra: test
|
|
42
|
+
Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
|
|
27
43
|
Provides-Extra: all
|
|
28
|
-
|
|
44
|
+
Requires-Dist: hexkit[test]; extra == "all"
|
|
29
45
|
|
|
30
46
|

|
|
31
47
|
[](https://pypi.python.org/pypi/hexkit/)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=67.7.2"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hexkit"
|
|
7
|
+
version = "0.11.1"
|
|
8
|
+
description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "German Human Genome Phenome Archive (GHGA)", email = "contact@ghga.de" },
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
license = { text = "Apache 2.0" }
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 1 - Planning",
|
|
17
|
+
"Operating System :: POSIX :: Linux",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"License :: OSI Approved :: Apache Software License",
|
|
22
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"pydantic >=2, <3",
|
|
28
|
+
"pydantic_settings >=2, <3",
|
|
29
|
+
"PyYAML >=6.0, <7",
|
|
30
|
+
"dependency-injector >=4.41.0, <5",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
akafka = ["aiokafka~=0.8.0", "jsonschema >=4.17.3, <5"]
|
|
35
|
+
s3 = ["boto3 >=1.26.50, <2", "botocore >=1.29.50, <2"]
|
|
36
|
+
mongodb = ["motor >=3.1.1, <4"]
|
|
37
|
+
|
|
38
|
+
test-akafka = ["hexkit[akafka]", "testcontainers[kafka] >=3.4.1, <4"]
|
|
39
|
+
test-s3 = ["hexkit[s3]", "testcontainers >=3.4.1, <4"]
|
|
40
|
+
test-mongodb = ["hexkit[mongodb]", "testcontainers[mongo] >=3.4.1, <4"]
|
|
41
|
+
test = ["hexkit[test-akafka,test-s3,test-mongodb]"]
|
|
42
|
+
|
|
43
|
+
all = ["hexkit[test]"]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Repository = "https://github.com/ghga-de/hexkit"
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["src"]
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.package-data]
|
|
52
|
+
"hexkit.providers.s3.test_files" = ["*.yaml"]
|
hexkit-0.11.1/setup.cfg
ADDED
|
@@ -17,10 +17,14 @@
|
|
|
17
17
|
|
|
18
18
|
import os
|
|
19
19
|
from pathlib import Path
|
|
20
|
-
from typing import Any, Callable,
|
|
20
|
+
from typing import Any, Callable, Final, Optional
|
|
21
21
|
|
|
22
22
|
import yaml
|
|
23
|
-
from
|
|
23
|
+
from pydantic_settings import (
|
|
24
|
+
BaseSettings,
|
|
25
|
+
PydanticBaseSettingsSource,
|
|
26
|
+
SettingsConfigDict,
|
|
27
|
+
)
|
|
24
28
|
|
|
25
29
|
# Default config prefix:
|
|
26
30
|
DEFAULT_CONFIG_PREFIX: Final = "ghga_services"
|
|
@@ -77,7 +81,7 @@ def get_default_config_yaml(prefix: str) -> Optional[Path]:
|
|
|
77
81
|
|
|
78
82
|
def yaml_settings_factory(
|
|
79
83
|
config_yaml: Optional[Path] = None,
|
|
80
|
-
) -> Callable[[
|
|
84
|
+
) -> Callable[[], dict[str, Any]]:
|
|
81
85
|
"""
|
|
82
86
|
A factory of source methods for pydantic's BaseSettings Config that load
|
|
83
87
|
settings from a yaml file.
|
|
@@ -87,14 +91,12 @@ def yaml_settings_factory(
|
|
|
87
91
|
Path to the yaml file to read from.
|
|
88
92
|
"""
|
|
89
93
|
|
|
90
|
-
def yaml_settings(
|
|
91
|
-
|
|
92
|
-
) -> Dict[str, Any]:
|
|
93
|
-
"""source method for loading pydantic BaseSettings from a yaml file"""
|
|
94
|
+
def yaml_settings() -> dict[str, Any]:
|
|
95
|
+
"""Source method for loading pydantic BaseSettings from a yaml file"""
|
|
94
96
|
if config_yaml is None:
|
|
95
97
|
return {}
|
|
96
98
|
|
|
97
|
-
with open(config_yaml,
|
|
99
|
+
with open(config_yaml, encoding="utf8") as yaml_file:
|
|
98
100
|
return yaml.safe_load(yaml_file)
|
|
99
101
|
|
|
100
102
|
return yaml_settings
|
|
@@ -105,8 +107,8 @@ def config_from_yaml(
|
|
|
105
107
|
) -> Callable:
|
|
106
108
|
"""A factory that returns decorator functions which extends a
|
|
107
109
|
pydantic BaseSettings class to read in parameters from a config yaml.
|
|
108
|
-
It replaces (or adds)
|
|
109
|
-
the priorities for parameter sources as follows (highest
|
|
110
|
+
It replaces (or adds) config settings to the BaseSettings class that configures
|
|
111
|
+
the priorities for parameter sources as follows (highest priority first):
|
|
110
112
|
- parameters passed using **kwargs
|
|
111
113
|
- environment variables
|
|
112
114
|
- file secrets
|
|
@@ -136,11 +138,10 @@ def config_from_yaml(
|
|
|
136
138
|
settings (BaseSettings):
|
|
137
139
|
A pydantic BaseSettings class to be modified.
|
|
138
140
|
"""
|
|
139
|
-
|
|
140
141
|
# check if settings inherits from pydantic BaseSettings:
|
|
141
142
|
if not issubclass(settings, BaseSettings):
|
|
142
143
|
raise TypeError(
|
|
143
|
-
"The specified settings class is not a subclass of
|
|
144
|
+
"The specified settings class is not a subclass of pydantic_settings.BaseSettings"
|
|
144
145
|
)
|
|
145
146
|
|
|
146
147
|
def constructor_wrapper(
|
|
@@ -153,41 +154,33 @@ def config_from_yaml(
|
|
|
153
154
|
config_yaml (str, optional):
|
|
154
155
|
Path to a config yaml. Overwrites the default location.
|
|
155
156
|
"""
|
|
156
|
-
|
|
157
157
|
# get default path if config_yaml not specified:
|
|
158
158
|
if config_yaml is None:
|
|
159
159
|
config_yaml = get_default_config_yaml(prefix)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
raise ConfigYamlDoesNotExist(path=config_yaml)
|
|
160
|
+
elif not config_yaml.is_file():
|
|
161
|
+
raise ConfigYamlDoesNotExist(path=config_yaml)
|
|
163
162
|
|
|
164
163
|
class ModSettings(settings):
|
|
165
164
|
"""Modifies the orginal Settings class provided by the user"""
|
|
166
165
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
166
|
+
model_config = SettingsConfigDict(frozen=True, env_prefix=f"{prefix}_")
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def settings_customise_sources( # noqa: PLR0913
|
|
170
|
+
cls,
|
|
171
|
+
settings_cls: type[BaseSettings],
|
|
172
|
+
init_settings: PydanticBaseSettingsSource,
|
|
173
|
+
env_settings: PydanticBaseSettingsSource,
|
|
174
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
|
175
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
|
176
|
+
):
|
|
177
|
+
"""Add custom yaml source"""
|
|
178
|
+
return (
|
|
180
179
|
init_settings,
|
|
181
180
|
env_settings,
|
|
182
181
|
file_secret_settings,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return (
|
|
186
|
-
init_settings,
|
|
187
|
-
env_settings,
|
|
188
|
-
file_secret_settings,
|
|
189
|
-
yaml_settings_factory(config_yaml),
|
|
190
|
-
)
|
|
182
|
+
yaml_settings_factory(config_yaml),
|
|
183
|
+
)
|
|
191
184
|
|
|
192
185
|
# construct settings class:
|
|
193
186
|
return ModSettings(**kwargs)
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"""Module collecting custom types."""
|
|
18
18
|
|
|
19
19
|
from collections.abc import Mapping, Sequence
|
|
20
|
-
from typing import Any, Union
|
|
20
|
+
from typing import Any, Literal, Union
|
|
21
21
|
|
|
22
22
|
# This is intended to type objects that could be a potential output of `json.loads`.
|
|
23
23
|
# (Scalar types as well as arrays are excluded from the above assumption.)
|
|
@@ -74,3 +74,7 @@ AsyncConstructable = Any
|
|
|
74
74
|
# ```
|
|
75
75
|
# Thus using a type alias for now:
|
|
76
76
|
AsyncContextConstructable = Any
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# The possible scopes for pytest fixtures
|
|
80
|
+
PytestScope = Literal["session", "package", "module", "class", "function"]
|
|
@@ -33,7 +33,7 @@ from typing import Any, Callable, Generic, Optional, TypeVar, Union
|
|
|
33
33
|
|
|
34
34
|
import dependency_injector.containers
|
|
35
35
|
import dependency_injector.providers
|
|
36
|
-
from
|
|
36
|
+
from pydantic_settings import BaseSettings
|
|
37
37
|
|
|
38
38
|
from hexkit.custom_types import AsyncConstructable, AsyncContextConstructable
|
|
39
39
|
|
|
@@ -55,7 +55,8 @@ class NotConstructableError(TypeError):
|
|
|
55
55
|
|
|
56
56
|
class AsyncInitShutdownError(TypeError):
|
|
57
57
|
"""Thrown when a container has sync `init_resource` or `shutdown_resource` methods
|
|
58
|
-
but coroutines are needed.
|
|
58
|
+
but coroutines are needed.
|
|
59
|
+
"""
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
def assert_async_constructable(
|
|
@@ -68,7 +69,6 @@ def assert_async_constructable(
|
|
|
68
69
|
not check whether `construct` really returns an awaitable or an async context
|
|
69
70
|
manager.
|
|
70
71
|
"""
|
|
71
|
-
|
|
72
72
|
if not callable(getattr(constructable, "construct", None)):
|
|
73
73
|
raise NotConstructableError(
|
|
74
74
|
"Async(Context)Constructable class must have a callable `construct` attribute."
|
|
@@ -77,7 +77,8 @@ def assert_async_constructable(
|
|
|
77
77
|
|
|
78
78
|
class AsyncConstructor(dependency_injector.providers.Resource):
|
|
79
79
|
"""Maps an Async(Context)Constructable onto the Resource class from the
|
|
80
|
-
`dependency_injector` framework.
|
|
80
|
+
`dependency_injector` framework.
|
|
81
|
+
"""
|
|
81
82
|
|
|
82
83
|
@staticmethod
|
|
83
84
|
def constructable_to_resource(
|
|
@@ -87,7 +88,6 @@ class AsyncConstructor(dependency_injector.providers.Resource):
|
|
|
87
88
|
Converts an Async(Context)Constructable to an async generator that is compatible
|
|
88
89
|
with the Resource definition of the `dependency_injector` framework.
|
|
89
90
|
"""
|
|
90
|
-
|
|
91
91
|
assert_async_constructable(constructable)
|
|
92
92
|
|
|
93
93
|
async def resource(*args: Any, **kwargs: Any) -> AsyncIterator[Any]:
|
|
@@ -120,7 +120,6 @@ class AsyncConstructor(dependency_injector.providers.Resource):
|
|
|
120
120
|
**kwargs: dependency_injector.providers.Injection,
|
|
121
121
|
):
|
|
122
122
|
"""Initialize `dependency_injector`'s Resource with an AbstractAsyncContextManager."""
|
|
123
|
-
|
|
124
123
|
if provides is None:
|
|
125
124
|
super().__init__()
|
|
126
125
|
else:
|
|
@@ -130,8 +129,8 @@ class AsyncConstructor(dependency_injector.providers.Resource):
|
|
|
130
129
|
|
|
131
130
|
def get_constructor(provides: type, *args, **kwargs):
|
|
132
131
|
"""Automatically selects and applies the right constructor for the class given to
|
|
133
|
-
`provides`.
|
|
134
|
-
|
|
132
|
+
`provides`.
|
|
133
|
+
"""
|
|
135
134
|
constructor_cls: type
|
|
136
135
|
|
|
137
136
|
try:
|
|
@@ -148,11 +147,11 @@ def get_constructor(provides: type, *args, **kwargs):
|
|
|
148
147
|
|
|
149
148
|
class CMDynamicContainer(dependency_injector.containers.DynamicContainer):
|
|
150
149
|
"""Adds a async context manager interface to the DynamicContainer base class from
|
|
151
|
-
the `dependency_injector` framework.
|
|
150
|
+
the `dependency_injector` framework.
|
|
151
|
+
"""
|
|
152
152
|
|
|
153
153
|
async def __aenter__(self):
|
|
154
154
|
"""Init/setup resources."""
|
|
155
|
-
|
|
156
155
|
init_future = self.init_resources()
|
|
157
156
|
|
|
158
157
|
if not inspect.isawaitable(init_future):
|
|
@@ -165,7 +164,6 @@ class CMDynamicContainer(dependency_injector.containers.DynamicContainer):
|
|
|
165
164
|
|
|
166
165
|
async def __aexit__(self, exc_type, exc_value, exc_trace):
|
|
167
166
|
"""Shutdown/teardown resources"""
|
|
168
|
-
|
|
169
167
|
shutdown_future = self.shutdown_resources()
|
|
170
168
|
|
|
171
169
|
if not inspect.isawaitable(shutdown_future):
|
|
@@ -206,11 +204,11 @@ PydanticConfig = TypeVar("PydanticConfig", bound=BaseSettings)
|
|
|
206
204
|
|
|
207
205
|
class Configurator(dependency_injector.providers.Factory, Generic[PydanticConfig]):
|
|
208
206
|
"""A configuration constructor that holds configuration parameters using a pydantic
|
|
209
|
-
model.
|
|
207
|
+
model.
|
|
208
|
+
"""
|
|
210
209
|
|
|
211
210
|
def load_config(self, config: PydanticConfig):
|
|
212
211
|
"""Loading config parameters form an pydantic config instance."""
|
|
213
|
-
|
|
214
212
|
self.override(dependency_injector.providers.Callable(lambda: config))
|
|
215
213
|
|
|
216
214
|
|
|
@@ -220,6 +218,6 @@ def get_configurator(
|
|
|
220
218
|
"""Initializes a configuration provider.
|
|
221
219
|
|
|
222
220
|
This helper function is necessary because the __init__ of Providers used by the
|
|
223
|
-
dependency_injector framework need to always use the same signature.
|
|
224
|
-
|
|
221
|
+
dependency_injector framework need to always use the same signature.
|
|
222
|
+
"""
|
|
225
223
|
return Configurator[PydanticConfig](pydantic_cls)
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
#
|
|
16
16
|
|
|
17
17
|
"""Protocol for creating Data Access Objects to perform CRUD (plus find) interactions
|
|
18
|
-
with the database.
|
|
18
|
+
with the database.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# ruff: noqa: PLR0913
|
|
19
22
|
|
|
20
23
|
import typing
|
|
21
24
|
from abc import ABC, abstractmethod
|
|
@@ -72,7 +75,8 @@ class InvalidFindMappingError(FindError):
|
|
|
72
75
|
|
|
73
76
|
class MultipleHitsFoundError(FindError):
|
|
74
77
|
"""Raised when a DAO find operation did result in multiple hits while only a
|
|
75
|
-
single hit was expected.
|
|
78
|
+
single hit was expected.
|
|
79
|
+
"""
|
|
76
80
|
|
|
77
81
|
def __init__(self, *, mapping: Mapping[str, str]):
|
|
78
82
|
message = (
|
|
@@ -84,7 +88,8 @@ class MultipleHitsFoundError(FindError):
|
|
|
84
88
|
|
|
85
89
|
class NoHitsFoundError(FindError):
|
|
86
90
|
"""Raised when a DAO find operation did result in no hits while a
|
|
87
|
-
single hit was expected.
|
|
91
|
+
single hit was expected.
|
|
92
|
+
"""
|
|
88
93
|
|
|
89
94
|
def __init__(self, *, mapping: Mapping[str, str]):
|
|
90
95
|
message = (
|
|
@@ -259,7 +264,6 @@ async def uuid4_id_generator() -> AsyncGenerator[str, None]:
|
|
|
259
264
|
This is an AsyncGenerator to be compliant with the id_generator requirements of the
|
|
260
265
|
DaoFactoryProtocol.
|
|
261
266
|
"""
|
|
262
|
-
|
|
263
267
|
while True:
|
|
264
268
|
yield str(uuid4())
|
|
265
269
|
|
|
@@ -274,7 +278,8 @@ class DaoFactoryProtocol(ABC):
|
|
|
274
278
|
|
|
275
279
|
class CreationModelInvalidError(ValueError):
|
|
276
280
|
"""Raised when the DtoCreationModel was invalid in relation to the main
|
|
277
|
-
DTO model.
|
|
281
|
+
DTO model.
|
|
282
|
+
"""
|
|
278
283
|
|
|
279
284
|
class IndexFieldsInvalidError(ValueError):
|
|
280
285
|
"""Raised when providing an invalid list of fields to index."""
|
|
@@ -282,9 +287,9 @@ class DaoFactoryProtocol(ABC):
|
|
|
282
287
|
@classmethod
|
|
283
288
|
def _validate_dto_model_id(cls, *, dto_model: type[Dto], id_field: str) -> None:
|
|
284
289
|
"""Checks whether the dto_model contains the expected id_field.
|
|
285
|
-
Raises IdFieldNotFoundError otherwise.
|
|
286
|
-
|
|
287
|
-
if id_field not in dto_model.
|
|
290
|
+
Raises IdFieldNotFoundError otherwise.
|
|
291
|
+
"""
|
|
292
|
+
if id_field not in dto_model.model_json_schema()["properties"]:
|
|
288
293
|
raise cls.IdFieldNotFoundError()
|
|
289
294
|
|
|
290
295
|
@classmethod
|
|
@@ -296,16 +301,16 @@ class DaoFactoryProtocol(ABC):
|
|
|
296
301
|
id_field: str,
|
|
297
302
|
) -> None:
|
|
298
303
|
"""Checks that the dto_creation_model has the same fields as the dto_model
|
|
299
|
-
except missing the ID. Raises CreationModelInvalidError otherwise.
|
|
300
|
-
|
|
304
|
+
except missing the ID. Raises CreationModelInvalidError otherwise.
|
|
305
|
+
"""
|
|
301
306
|
if dto_creation_model is None:
|
|
302
307
|
return
|
|
303
308
|
|
|
304
|
-
expected_properties = copy(dto_model.
|
|
309
|
+
expected_properties = copy(dto_model.model_json_schema()["properties"])
|
|
305
310
|
# (the schema method returns an attribute of the class, making a copy to not
|
|
306
311
|
# alter the class)
|
|
307
312
|
del expected_properties[id_field]
|
|
308
|
-
observed_properties = dto_creation_model.
|
|
313
|
+
observed_properties = dto_creation_model.model_json_schema()["properties"]
|
|
309
314
|
|
|
310
315
|
if observed_properties != expected_properties:
|
|
311
316
|
raise cls.CreationModelInvalidError()
|
|
@@ -318,8 +323,8 @@ class DaoFactoryProtocol(ABC):
|
|
|
318
323
|
fields_to_index: Optional[Collection[str]],
|
|
319
324
|
) -> None:
|
|
320
325
|
"""Checks that all provided fields are present in the dto_model.
|
|
321
|
-
Raises IndexFieldsInvalidError otherwise.
|
|
322
|
-
|
|
326
|
+
Raises IndexFieldsInvalidError otherwise.
|
|
327
|
+
"""
|
|
323
328
|
if fields_to_index is None:
|
|
324
329
|
return
|
|
325
330
|
|
|
@@ -403,7 +408,6 @@ class DaoFactoryProtocol(ABC):
|
|
|
403
408
|
self.IdFieldNotFoundError:
|
|
404
409
|
Raised when the dto_model did not contain the expected id_field.
|
|
405
410
|
"""
|
|
406
|
-
|
|
407
411
|
self._validate_dto_model_id(dto_model=dto_model, id_field=id_field)
|
|
408
412
|
|
|
409
413
|
self._validate_dto_creation_model(
|
|
@@ -417,7 +421,7 @@ class DaoFactoryProtocol(ABC):
|
|
|
417
421
|
)
|
|
418
422
|
|
|
419
423
|
if id_generator is None:
|
|
420
|
-
#
|
|
424
|
+
# instantiate the default ID generator:
|
|
421
425
|
id_generator = uuid4_id_generator()
|
|
422
426
|
|
|
423
427
|
return await self._get_dao(
|
|
@@ -470,5 +474,6 @@ class DaoFactoryProtocol(ABC):
|
|
|
470
474
|
id_generator: AsyncGenerator[str, None],
|
|
471
475
|
) -> Union[DaoSurrogateId[Dto, DtoCreation], DaoNaturalId[Dto]]:
|
|
472
476
|
"""*To be implemented by the provider. Input validation is done outside of this
|
|
473
|
-
method.*
|
|
477
|
+
method.*
|
|
478
|
+
"""
|
|
474
479
|
...
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
"""Protocol for interacting with S3-like Object Storages."""
|
|
18
18
|
|
|
19
|
+
# ruff: noqa: PLR0913
|
|
19
20
|
|
|
20
21
|
import re
|
|
21
22
|
from abc import ABC, abstractmethod
|
|
@@ -28,16 +29,15 @@ DEFAULT_URL_EXPIRATION_PERIOD = 24 * 60 * 60 # default expiration time 24 hours
|
|
|
28
29
|
|
|
29
30
|
class PresignedPostURL(NamedTuple):
|
|
30
31
|
"""Container for presigned POST URLs along with additional metadata fields that
|
|
31
|
-
should be attached as body data when sending the POST request.
|
|
32
|
+
should be attached as body data when sending the POST request.
|
|
33
|
+
"""
|
|
32
34
|
|
|
33
35
|
url: str
|
|
34
36
|
fields: dict[str, str]
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
class ObjectStorageProtocol(ABC):
|
|
38
|
-
"""
|
|
39
|
-
Protocol for interacting with S3-like Object Storages.
|
|
40
|
-
"""
|
|
40
|
+
"""Protocol for interacting with S3-like Object Storages."""
|
|
41
41
|
|
|
42
42
|
# constants for multipart uploads:
|
|
43
43
|
# (shall not be changed by provider implementations)
|
|
@@ -51,7 +51,6 @@ class ObjectStorageProtocol(ABC):
|
|
|
51
51
|
"""Check whether a bucket with the specified ID (`bucket_id`) exists.
|
|
52
52
|
Returns `True` if it exists and `False` otherwise.
|
|
53
53
|
"""
|
|
54
|
-
|
|
55
54
|
self._validate_bucket_id(bucket_id)
|
|
56
55
|
return await self._does_bucket_exist(bucket_id)
|
|
57
56
|
|
|
@@ -60,7 +59,6 @@ class ObjectStorageProtocol(ABC):
|
|
|
60
59
|
Create a bucket (= a structure that can hold multiple file objects) with the
|
|
61
60
|
specified unique ID.
|
|
62
61
|
"""
|
|
63
|
-
|
|
64
62
|
self._validate_bucket_id(bucket_id)
|
|
65
63
|
await self._create_bucket(bucket_id)
|
|
66
64
|
|
|
@@ -73,14 +71,11 @@ class ObjectStorageProtocol(ABC):
|
|
|
73
71
|
will be deleted, if False (the default) a BucketNotEmptyError will be raised if
|
|
74
72
|
the bucket is not empty.
|
|
75
73
|
"""
|
|
76
|
-
|
|
77
74
|
self._validate_bucket_id(bucket_id)
|
|
78
75
|
await self._delete_bucket(bucket_id, delete_content=delete_content)
|
|
79
76
|
|
|
80
77
|
async def list_all_object_ids(self, bucket_id: str) -> list[str]:
|
|
81
|
-
"""
|
|
82
|
-
Retrieve a list of IDs for all objects currently present in the specified bucket
|
|
83
|
-
"""
|
|
78
|
+
"""Retrieve a list of IDs for all objects currently present in the specified bucket"""
|
|
84
79
|
self._validate_bucket_id(bucket_id)
|
|
85
80
|
return await self._list_all_object_ids(bucket_id=bucket_id)
|
|
86
81
|
|
|
@@ -97,7 +92,6 @@ class ObjectStorageProtocol(ABC):
|
|
|
97
92
|
You may also specify a custom expiry duration in seconds (`expires_after`) and
|
|
98
93
|
a maximum size (bytes) for uploads (`max_upload_size`).
|
|
99
94
|
"""
|
|
100
|
-
|
|
101
95
|
self._validate_bucket_id(bucket_id)
|
|
102
96
|
self._validate_object_id(object_id)
|
|
103
97
|
return await self._get_object_upload_url(
|
|
@@ -114,7 +108,6 @@ class ObjectStorageProtocol(ABC):
|
|
|
114
108
|
object_id: str,
|
|
115
109
|
) -> str:
|
|
116
110
|
"""Initiates a multipart upload procedure. Returns the upload ID."""
|
|
117
|
-
|
|
118
111
|
self._validate_bucket_id(bucket_id)
|
|
119
112
|
self._validate_object_id(object_id)
|
|
120
113
|
return await self._init_multipart_upload(
|
|
@@ -136,7 +129,6 @@ class ObjectStorageProtocol(ABC):
|
|
|
136
129
|
Please note: the part number must be a non-zero, positive integer and parts
|
|
137
130
|
should be uploaded in sequence.
|
|
138
131
|
"""
|
|
139
|
-
|
|
140
132
|
self._validate_bucket_id(bucket_id)
|
|
141
133
|
self._validate_object_id(object_id)
|
|
142
134
|
return await self._get_part_upload_url(
|
|
@@ -157,7 +149,6 @@ class ObjectStorageProtocol(ABC):
|
|
|
157
149
|
"""Cancel a multipart upload with the specified ID. All uploaded content is
|
|
158
150
|
deleted.
|
|
159
151
|
"""
|
|
160
|
-
|
|
161
152
|
self._validate_bucket_id(bucket_id)
|
|
162
153
|
self._validate_object_id(object_id)
|
|
163
154
|
await self._abort_multipart_upload(
|
|
@@ -180,7 +171,6 @@ class ObjectStorageProtocol(ABC):
|
|
|
180
171
|
This ensures that exactly the specified number of parts exist and that all parts
|
|
181
172
|
(except the last one) have the specified size.
|
|
182
173
|
"""
|
|
183
|
-
|
|
184
174
|
self._validate_bucket_id(bucket_id)
|
|
185
175
|
self._validate_object_id(object_id)
|
|
186
176
|
await self._complete_multipart_upload(
|
|
@@ -198,7 +188,6 @@ class ObjectStorageProtocol(ABC):
|
|
|
198
188
|
the specified ID (`object_id`) from bucket with the specified id (`bucket_id`).
|
|
199
189
|
You may also specify a custom expiry duration in seconds (`expires_after`).
|
|
200
190
|
"""
|
|
201
|
-
|
|
202
191
|
self._validate_bucket_id(bucket_id)
|
|
203
192
|
self._validate_object_id(object_id)
|
|
204
193
|
return await self._get_object_download_url(
|
|
@@ -213,7 +202,6 @@ class ObjectStorageProtocol(ABC):
|
|
|
213
202
|
may be provided to check the objects content.
|
|
214
203
|
Returns `True` if checks succeed and `False` otherwise.
|
|
215
204
|
"""
|
|
216
|
-
|
|
217
205
|
self._validate_bucket_id(bucket_id)
|
|
218
206
|
self._validate_object_id(object_id)
|
|
219
207
|
return await self._does_object_exist(
|
|
@@ -221,10 +209,7 @@ class ObjectStorageProtocol(ABC):
|
|
|
221
209
|
)
|
|
222
210
|
|
|
223
211
|
async def get_object_size(self, *, bucket_id: str, object_id: str) -> int:
|
|
224
|
-
"""
|
|
225
|
-
Returns the size of an object in bytes.
|
|
226
|
-
"""
|
|
227
|
-
|
|
212
|
+
"""Returns the size of an object in bytes."""
|
|
228
213
|
self._validate_bucket_id(bucket_id)
|
|
229
214
|
self._validate_object_id(object_id)
|
|
230
215
|
return await self._get_object_size(bucket_id=bucket_id, object_id=object_id)
|
|
@@ -240,7 +225,6 @@ class ObjectStorageProtocol(ABC):
|
|
|
240
225
|
"""Copy an object from one bucket (`source_bucket_id` and `source_object_id`) to
|
|
241
226
|
another bucket (`dest_bucket_id` and `dest_object_id`).
|
|
242
227
|
"""
|
|
243
|
-
|
|
244
228
|
self._validate_bucket_id(source_bucket_id)
|
|
245
229
|
self._validate_object_id(source_object_id)
|
|
246
230
|
self._validate_bucket_id(dest_bucket_id)
|
|
@@ -256,7 +240,6 @@ class ObjectStorageProtocol(ABC):
|
|
|
256
240
|
"""Delete an object with the specified id (`object_id`) in the bucket with the
|
|
257
241
|
specified id (`bucket_id`).
|
|
258
242
|
"""
|
|
259
|
-
|
|
260
243
|
self._validate_bucket_id(bucket_id)
|
|
261
244
|
self._validate_object_id(object_id)
|
|
262
245
|
await self._delete_object(bucket_id=bucket_id, object_id=object_id)
|
|
@@ -601,7 +584,8 @@ class ObjectStorageProtocol(ABC):
|
|
|
601
584
|
|
|
602
585
|
class MultiPartUploadAlreadyExistsError(MultiPartUploadError):
|
|
603
586
|
"""Thrown when trying to create a multipart upload for an object for which another
|
|
604
|
-
upload is already active.
|
|
587
|
+
upload is already active.
|
|
588
|
+
"""
|
|
605
589
|
|
|
606
590
|
def __init__(self, bucket_id: str, object_id: str):
|
|
607
591
|
message = (
|
|
@@ -19,7 +19,10 @@ They correspond to the `EventPublisherProtocol` and `EventSubscriberProtocol`,
|
|
|
19
19
|
respectively.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
from .provider import
|
|
23
|
-
|
|
22
|
+
from .provider import (
|
|
23
|
+
KafkaConfig,
|
|
24
|
+
KafkaEventPublisher,
|
|
25
|
+
KafkaEventSubscriber,
|
|
26
|
+
)
|
|
24
27
|
|
|
25
28
|
__all__ = ["KafkaEventPublisher", "KafkaEventSubscriber", "KafkaConfig"]
|