hexkit 3.3.0__tar.gz → 3.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hexkit-3.3.0/src/hexkit.egg-info → hexkit-3.5.0}/PKG-INFO +1 -1
- {hexkit-3.3.0 → hexkit-3.5.0}/pyproject.toml +1 -1
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/config.py +6 -29
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/protocols/dao.py +1 -1
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/protocols/objstorage.py +21 -18
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/akafka/config.py +7 -1
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/akafka/provider/eventpub.py +4 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/akafka/provider/eventsub.py +4 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/akafka/testutils.py +1 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/mongodb/provider.py +6 -1
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/mongodb/testutils.py +16 -10
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/mongokafka/provider.py +12 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/s3/testutils/__init__.py +32 -3
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/s3/testutils/_fixtures.py +57 -11
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/testing/eventpub.py +2 -0
- {hexkit-3.3.0 → hexkit-3.5.0/src/hexkit.egg-info}/PKG-INFO +1 -1
- {hexkit-3.3.0 → hexkit-3.5.0}/LICENSE +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/MANIFEST.in +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/README.md +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/setup.cfg +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/__init__.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/__main__.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/base.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/correlation.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/custom_types.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/log.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/protocols/__init__.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/protocols/daopub.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/protocols/daosub.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/protocols/eventpub.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/protocols/eventsub.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/__init__.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/akafka/__init__.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/s3/__init__.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/s3/provider.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/providers/testing/__init__.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/py.typed +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit/utils.py +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit.egg-info/requires.txt +0 -0
- {hexkit-3.3.0 → hexkit-3.5.0}/src/hexkit.egg-info/top_level.txt +0 -0
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
|
|
18
18
|
import os
|
|
19
19
|
from pathlib import Path
|
|
20
|
-
from typing import
|
|
20
|
+
from typing import Callable, Final, Optional
|
|
21
21
|
|
|
22
|
-
import yaml
|
|
23
22
|
from pydantic_settings import (
|
|
24
23
|
BaseSettings,
|
|
25
24
|
PydanticBaseSettingsSource,
|
|
26
25
|
SettingsConfigDict,
|
|
26
|
+
YamlConfigSettingsSource,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
# Default config prefix:
|
|
@@ -79,29 +79,6 @@ def get_default_config_yaml(prefix: str) -> Optional[Path]:
|
|
|
79
79
|
return None
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
def yaml_settings_factory(
|
|
83
|
-
config_yaml: Optional[Path] = None,
|
|
84
|
-
) -> Callable[[], dict[str, Any]]:
|
|
85
|
-
"""
|
|
86
|
-
A factory of source methods for pydantic's BaseSettings Config that load
|
|
87
|
-
settings from a yaml file.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
config_yaml (str, Optional):
|
|
91
|
-
Path to the yaml file to read from.
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
def yaml_settings() -> dict[str, Any]:
|
|
95
|
-
"""Source method for loading pydantic BaseSettings from a yaml file"""
|
|
96
|
-
if config_yaml is None:
|
|
97
|
-
return {}
|
|
98
|
-
|
|
99
|
-
with open(config_yaml, encoding="utf8") as yaml_file:
|
|
100
|
-
return yaml.safe_load(yaml_file)
|
|
101
|
-
|
|
102
|
-
return yaml_settings
|
|
103
|
-
|
|
104
|
-
|
|
105
82
|
def config_from_yaml(
|
|
106
83
|
prefix: str = DEFAULT_CONFIG_PREFIX,
|
|
107
84
|
) -> Callable:
|
|
@@ -119,7 +96,7 @@ def config_from_yaml(
|
|
|
119
96
|
|
|
120
97
|
Args:
|
|
121
98
|
prefix: (str, optional):
|
|
122
|
-
When defining parameters via
|
|
99
|
+
When defining parameters via environment variables, all variables
|
|
123
100
|
have to be prefixed with this string following this pattern
|
|
124
101
|
"{prefix}_{actual_variable_name}". Moreover, this prefix is used
|
|
125
102
|
to derive the default location for the config yaml file
|
|
@@ -161,7 +138,7 @@ def config_from_yaml(
|
|
|
161
138
|
raise ConfigYamlDoesNotExist(path=config_yaml)
|
|
162
139
|
|
|
163
140
|
class ModSettings(settings):
|
|
164
|
-
"""Modifies the
|
|
141
|
+
"""Modifies the original Settings class provided by the user"""
|
|
165
142
|
|
|
166
143
|
model_config = SettingsConfigDict(frozen=True, env_prefix=f"{prefix}_")
|
|
167
144
|
|
|
@@ -174,12 +151,12 @@ def config_from_yaml(
|
|
|
174
151
|
dotenv_settings: PydanticBaseSettingsSource,
|
|
175
152
|
file_secret_settings: PydanticBaseSettingsSource,
|
|
176
153
|
):
|
|
177
|
-
"""Add custom yaml source"""
|
|
154
|
+
"""Add custom yaml source, but maintain the priority of sources."""
|
|
178
155
|
return (
|
|
179
156
|
init_settings,
|
|
180
157
|
env_settings,
|
|
181
158
|
file_secret_settings,
|
|
182
|
-
|
|
159
|
+
YamlConfigSettingsSource(settings_cls, config_yaml),
|
|
183
160
|
)
|
|
184
161
|
|
|
185
162
|
# construct settings class:
|
|
@@ -61,7 +61,7 @@ class ResourceAlreadyExistsError(RuntimeError):
|
|
|
61
61
|
"""Raised when a resource did unexpectedly exist."""
|
|
62
62
|
|
|
63
63
|
def __init__(self, *, id_: str):
|
|
64
|
-
message = f'The resource with the id "{id_}"
|
|
64
|
+
message = f'The resource with the id "{id_}" already exists.'
|
|
65
65
|
super().__init__(message)
|
|
66
66
|
|
|
67
67
|
|
|
@@ -457,7 +457,10 @@ class ObjectStorageProtocol(ABC):
|
|
|
457
457
|
|
|
458
458
|
# Validation logic for input parameter:
|
|
459
459
|
# (is typically only used by the protocol but may also be used in
|
|
460
|
-
# provider-specific code)
|
|
460
|
+
# provider-specific code or overwritten by the provider)
|
|
461
|
+
|
|
462
|
+
_re_bucket_id = re.compile(r"^[a-z0-9\-]{3,63}$")
|
|
463
|
+
_re_bucket_id_msg = "must consist of 3-63 lowercase letters, digits or hyphens"
|
|
461
464
|
|
|
462
465
|
@classmethod
|
|
463
466
|
def _validate_bucket_id(cls, bucket_id: str):
|
|
@@ -466,43 +469,43 @@ class ObjectStorageProtocol(ABC):
|
|
|
466
469
|
https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
|
|
467
470
|
Raises BucketIdValidationError if not valid.
|
|
468
471
|
"""
|
|
469
|
-
if not
|
|
472
|
+
if not cls._re_bucket_id.match(bucket_id):
|
|
470
473
|
raise cls.BucketIdValidationError(
|
|
471
474
|
bucket_id=bucket_id,
|
|
472
|
-
reason=
|
|
473
|
-
)
|
|
474
|
-
if not re.match(r"^[a-z0-9\-]*$", bucket_id):
|
|
475
|
-
raise cls.BucketIdValidationError(
|
|
476
|
-
bucket_id=bucket_id,
|
|
477
|
-
reason="only lowercase letters, digits and hyphens (-) are allowed",
|
|
475
|
+
reason=cls._re_bucket_id_msg,
|
|
478
476
|
)
|
|
479
477
|
if bucket_id.startswith("-") or bucket_id.endswith("-"):
|
|
480
478
|
raise cls.BucketIdValidationError(
|
|
481
479
|
bucket_id=bucket_id,
|
|
482
|
-
reason="may not start or end with a hyphen
|
|
480
|
+
reason="may not start or end with a hyphen",
|
|
483
481
|
)
|
|
484
482
|
|
|
483
|
+
_re_object_id = re.compile(r"^[a-zA-Z0-9\-_.]{3,255}$")
|
|
484
|
+
_re_object_id_msg = (
|
|
485
|
+
"must consist of 3-255 letters, digits, hyphens, underscores or dots"
|
|
486
|
+
)
|
|
487
|
+
|
|
485
488
|
@classmethod
|
|
486
489
|
def _validate_object_id(cls, object_id: str):
|
|
487
|
-
"""Check whether
|
|
490
|
+
"""Check whether an object id follows the recommended naming pattern.
|
|
488
491
|
This is roughly based on (plus some additional restrictions):
|
|
489
492
|
https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html
|
|
490
493
|
Raises ObjectIdValidationError if not valid.
|
|
494
|
+
|
|
495
|
+
Note that the default implementation does not allow object IDs to contain
|
|
496
|
+
forward slashes (/) which are sometimes used to represent directories.
|
|
497
|
+
Providers can override this method or the above constants to allow slashes,
|
|
498
|
+
other characters or longer object names.
|
|
491
499
|
"""
|
|
492
|
-
if not
|
|
493
|
-
raise cls.ObjectIdValidationError(
|
|
494
|
-
object_id=object_id,
|
|
495
|
-
reason="must be between 3 and 63 characters long",
|
|
496
|
-
)
|
|
497
|
-
if not re.match(r"^[a-zA-Z0-9\-\.]*$", object_id):
|
|
500
|
+
if not cls._re_object_id.match(object_id):
|
|
498
501
|
raise cls.ObjectIdValidationError(
|
|
499
502
|
object_id=object_id,
|
|
500
|
-
reason=
|
|
503
|
+
reason=cls._re_object_id_msg,
|
|
501
504
|
)
|
|
502
505
|
if object_id.startswith(("-", ".")) or object_id.endswith(("-", ".")):
|
|
503
506
|
raise cls.ObjectIdValidationError(
|
|
504
507
|
object_id=object_id,
|
|
505
|
-
reason="may not start or end with a hyphen
|
|
508
|
+
reason="object names may not start or end with a hyphen or a dot",
|
|
506
509
|
)
|
|
507
510
|
|
|
508
511
|
# Exceptions that may be used by implementation:
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
from typing import Literal
|
|
20
20
|
|
|
21
|
-
from pydantic import Field, SecretStr
|
|
21
|
+
from pydantic import Field, PositiveInt, SecretStr
|
|
22
22
|
from pydantic_settings import BaseSettings
|
|
23
23
|
|
|
24
24
|
|
|
@@ -76,3 +76,9 @@ class KafkaConfig(BaseSettings):
|
|
|
76
76
|
+ " newly correlation ID will be generated and used in the event header."
|
|
77
77
|
),
|
|
78
78
|
)
|
|
79
|
+
kafka_max_message_size: PositiveInt = Field(
|
|
80
|
+
default=1024 * 1024, # 1 MiB
|
|
81
|
+
description="The largest message size that can be transmitted, in bytes. Only"
|
|
82
|
+
+ " services that have a need to send/receive larger messages should set this.",
|
|
83
|
+
examples=[1024 * 1024, 16 * 1024 * 1024],
|
|
84
|
+
)
|
|
@@ -54,6 +54,7 @@ class KafkaProducerCompatible(Protocol):
|
|
|
54
54
|
client_id: str,
|
|
55
55
|
key_serializer: Callable[[Any], bytes],
|
|
56
56
|
value_serializer: Callable[[Any], bytes],
|
|
57
|
+
max_request_size: int,
|
|
57
58
|
):
|
|
58
59
|
"""
|
|
59
60
|
Initialize the producer with some config params.
|
|
@@ -67,6 +68,8 @@ class KafkaProducerCompatible(Protocol):
|
|
|
67
68
|
Function to serialize the keys into bytes.
|
|
68
69
|
value_serializer:
|
|
69
70
|
Function to serialize the values into bytes.
|
|
71
|
+
max_request_size:
|
|
72
|
+
Maximum sendable message size.
|
|
70
73
|
"""
|
|
71
74
|
...
|
|
72
75
|
|
|
@@ -116,6 +119,7 @@ class KafkaEventPublisher(EventPublisherProtocol):
|
|
|
116
119
|
value_serializer=lambda event_value: json.dumps(event_value).encode(
|
|
117
120
|
"ascii"
|
|
118
121
|
),
|
|
122
|
+
max_request_size=config.kafka_max_message_size,
|
|
119
123
|
)
|
|
120
124
|
|
|
121
125
|
try:
|
|
@@ -90,6 +90,7 @@ class KafkaConsumerCompatible(Protocol):
|
|
|
90
90
|
enable_auto_commit: bool,
|
|
91
91
|
key_deserializer: Callable[[bytes], str],
|
|
92
92
|
value_deserializer: Callable[[bytes], str],
|
|
93
|
+
max_partition_fetch_bytes: int,
|
|
93
94
|
):
|
|
94
95
|
"""
|
|
95
96
|
Initialize the consumer with some config params.
|
|
@@ -108,6 +109,8 @@ class KafkaConsumerCompatible(Protocol):
|
|
|
108
109
|
Function to deserialize the keys into strings.
|
|
109
110
|
value_serializer:
|
|
110
111
|
Function to deserialize the values into strings.
|
|
112
|
+
max_partition_fetch_bytes:
|
|
113
|
+
Maximum receivable message size
|
|
111
114
|
"""
|
|
112
115
|
...
|
|
113
116
|
|
|
@@ -176,6 +179,7 @@ class KafkaEventSubscriber(InboundProviderBase):
|
|
|
176
179
|
value_deserializer=lambda event_value: json.loads(
|
|
177
180
|
event_value.decode("ascii")
|
|
178
181
|
),
|
|
182
|
+
max_partition_fetch_bytes=config.kafka_max_message_size,
|
|
179
183
|
)
|
|
180
184
|
try:
|
|
181
185
|
await consumer.start()
|
|
@@ -31,6 +31,7 @@ from motor.core import AgnosticCollection
|
|
|
31
31
|
from motor.motor_asyncio import AsyncIOMotorClient
|
|
32
32
|
from pydantic import Field, SecretStr
|
|
33
33
|
from pydantic_settings import BaseSettings
|
|
34
|
+
from pymongo.errors import DuplicateKeyError
|
|
34
35
|
|
|
35
36
|
from hexkit.protocols.dao import (
|
|
36
37
|
DaoFactoryProtocol,
|
|
@@ -42,6 +43,7 @@ from hexkit.protocols.dao import (
|
|
|
42
43
|
InvalidFindMappingError,
|
|
43
44
|
MultipleHitsFoundError,
|
|
44
45
|
NoHitsFoundError,
|
|
46
|
+
ResourceAlreadyExistsError,
|
|
45
47
|
ResourceNotFoundError,
|
|
46
48
|
)
|
|
47
49
|
from hexkit.utils import FieldNotInModelError, validate_fields_in_model
|
|
@@ -386,7 +388,10 @@ class MongoDbDaoNaturalId(MongoDbDaoBase[Dto]):
|
|
|
386
388
|
when a resource with the ID specified in the dto does already exist.
|
|
387
389
|
"""
|
|
388
390
|
document = self._dto_to_document(dto)
|
|
389
|
-
|
|
391
|
+
try:
|
|
392
|
+
await self._collection.insert_one(document)
|
|
393
|
+
except DuplicateKeyError as error:
|
|
394
|
+
raise ResourceAlreadyExistsError(id_=document["_id"]) from error
|
|
390
395
|
|
|
391
396
|
async def upsert(self, dto: Dto) -> None:
|
|
392
397
|
"""Update the provided resource if it already exists, create it otherwise.
|
|
@@ -61,24 +61,30 @@ class MongoDbFixture:
|
|
|
61
61
|
|
|
62
62
|
def empty_collections(
|
|
63
63
|
self,
|
|
64
|
+
*,
|
|
65
|
+
collections: Optional[Union[str, list[str]]] = None,
|
|
64
66
|
exclude_collections: Optional[Union[str, list[str]]] = None,
|
|
65
67
|
):
|
|
66
|
-
"""Drop
|
|
68
|
+
"""Drop the given mongodb collection(s) in the database.
|
|
67
69
|
|
|
70
|
+
If no collections are specified, all collections will be dropped.
|
|
68
71
|
You can also specify collection(s) that should be excluded
|
|
69
72
|
from the operation, i.e. collections that should be kept.
|
|
70
73
|
"""
|
|
71
74
|
db_name = self.config.db_name
|
|
72
|
-
if exclude_collections is None:
|
|
73
|
-
exclude_collections = []
|
|
74
|
-
if isinstance(exclude_collections, str):
|
|
75
|
-
exclude_collections = [exclude_collections]
|
|
76
|
-
excluded_collections = set(exclude_collections)
|
|
77
75
|
try:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
if collections is None:
|
|
77
|
+
collections = self.client[db_name].list_collection_names()
|
|
78
|
+
elif isinstance(collections, str):
|
|
79
|
+
collections = [collections]
|
|
80
|
+
if exclude_collections is None:
|
|
81
|
+
exclude_collections = []
|
|
82
|
+
elif isinstance(exclude_collections, str):
|
|
83
|
+
exclude_collections = [exclude_collections]
|
|
84
|
+
excluded_collections = set(exclude_collections)
|
|
85
|
+
for collection in collections:
|
|
86
|
+
if collection not in excluded_collections:
|
|
87
|
+
self.client[db_name].drop_collection(collection)
|
|
82
88
|
except (ExecutionTimeout, OperationFailure) as error:
|
|
83
89
|
raise RuntimeError(
|
|
84
90
|
f"Could not drop collection(s) of Mongo database {db_name}"
|
|
@@ -20,6 +20,7 @@ Require dependencies of the `akafka` and `mongodb` extras.
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
import json
|
|
23
|
+
import logging
|
|
23
24
|
from collections.abc import AsyncIterator, Awaitable, Collection, Mapping
|
|
24
25
|
from contextlib import AbstractAsyncContextManager, asynccontextmanager, contextmanager
|
|
25
26
|
from typing import Any, Callable, Generic, Optional
|
|
@@ -27,6 +28,7 @@ from typing import Any, Callable, Generic, Optional
|
|
|
27
28
|
from aiokafka import AIOKafkaProducer
|
|
28
29
|
from motor.core import AgnosticCollection
|
|
29
30
|
from motor.motor_asyncio import AsyncIOMotorClient
|
|
31
|
+
from pydantic import field_validator
|
|
30
32
|
|
|
31
33
|
from hexkit.correlation import get_correlation_id, set_correlation_id
|
|
32
34
|
from hexkit.custom_types import JsonObject
|
|
@@ -396,6 +398,16 @@ class MongoKafkaDaoPublisher(Generic[Dto]):
|
|
|
396
398
|
class MongoKafkaConfig(MongoDbConfig, KafkaConfig):
|
|
397
399
|
"""Config parameters and their defaults."""
|
|
398
400
|
|
|
401
|
+
@field_validator("kafka_max_message_size", mode="after")
|
|
402
|
+
@classmethod
|
|
403
|
+
def validate_max_message_size(cls, value: int) -> int:
|
|
404
|
+
"""Validate the maximum message size."""
|
|
405
|
+
if value > 2**24: # 16 MiB
|
|
406
|
+
logging.warning(
|
|
407
|
+
f"Max message size ({value}) exceeds the 16 MiB document size limit for MongoDB!"
|
|
408
|
+
)
|
|
409
|
+
return value
|
|
410
|
+
|
|
399
411
|
|
|
400
412
|
class MongoKafkaDaoPublisherFactory(DaoPublisherFactoryProtocol):
|
|
401
413
|
"""A provider implementing the DaoPublisherFactoryProtocol based on MongoDB and
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
"""S3-related test fixtures and support functions."""
|
|
17
17
|
|
|
18
|
-
from ._fixtures import (
|
|
18
|
+
from ._fixtures import (
|
|
19
19
|
LOCALSTACK_IMAGE,
|
|
20
20
|
MEBIBYTE,
|
|
21
21
|
TEST_FILE_DIR,
|
|
@@ -36,8 +36,8 @@ from ._fixtures import ( # noqa: F401
|
|
|
36
36
|
tmp_file,
|
|
37
37
|
upload_file,
|
|
38
38
|
)
|
|
39
|
-
from ._typical_workflow import typical_workflow
|
|
40
|
-
from ._utils import (
|
|
39
|
+
from ._typical_workflow import typical_workflow
|
|
40
|
+
from ._utils import (
|
|
41
41
|
check_part_size,
|
|
42
42
|
download_and_check_test_file,
|
|
43
43
|
multipart_upload_file,
|
|
@@ -45,3 +45,32 @@ from ._utils import ( # noqa: F401
|
|
|
45
45
|
upload_part_of_size,
|
|
46
46
|
upload_part_via_url,
|
|
47
47
|
)
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"LOCALSTACK_IMAGE",
|
|
51
|
+
"MEBIBYTE",
|
|
52
|
+
"TEST_FILE_DIR",
|
|
53
|
+
"TEST_FILE_PATHS",
|
|
54
|
+
"FileObject",
|
|
55
|
+
"S3ContainerFixture",
|
|
56
|
+
"S3Fixture",
|
|
57
|
+
"calc_md5",
|
|
58
|
+
"clean_s3_fixture",
|
|
59
|
+
"get_clean_s3_fixture",
|
|
60
|
+
"get_persistent_s3_fixture",
|
|
61
|
+
"get_s3_container_fixture",
|
|
62
|
+
"persistent_s3_fixture",
|
|
63
|
+
"populate_storage",
|
|
64
|
+
"s3_container_fixture",
|
|
65
|
+
"s3_fixture",
|
|
66
|
+
"temp_file_object",
|
|
67
|
+
"tmp_file",
|
|
68
|
+
"upload_file",
|
|
69
|
+
"typical_workflow",
|
|
70
|
+
"check_part_size",
|
|
71
|
+
"download_and_check_test_file",
|
|
72
|
+
"multipart_upload_file",
|
|
73
|
+
"upload_part",
|
|
74
|
+
"upload_part_of_size",
|
|
75
|
+
"upload_part_via_url",
|
|
76
|
+
]
|
|
@@ -24,7 +24,7 @@ from collections.abc import AsyncGenerator, Generator
|
|
|
24
24
|
from contextlib import contextmanager
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
from tempfile import NamedTemporaryFile
|
|
27
|
-
from typing import Any, NamedTuple, Optional
|
|
27
|
+
from typing import Any, NamedTuple, Optional, Union
|
|
28
28
|
|
|
29
29
|
try:
|
|
30
30
|
from typing import Self
|
|
@@ -93,11 +93,10 @@ class S3Fixture:
|
|
|
93
93
|
self.config = config
|
|
94
94
|
self.storage = storage
|
|
95
95
|
|
|
96
|
-
def get_buckets(self) ->
|
|
96
|
+
def get_buckets(self) -> list[str]:
|
|
97
97
|
"""Return a list of the buckets currently existing in the S3 object storage."""
|
|
98
98
|
response = self.storage._client.list_buckets()
|
|
99
|
-
|
|
100
|
-
return buckets
|
|
99
|
+
return [bucket["Name"] for bucket in response["Buckets"]]
|
|
101
100
|
|
|
102
101
|
async def populate_buckets(self, buckets: list[str]):
|
|
103
102
|
"""Populate the storage with buckets."""
|
|
@@ -111,19 +110,66 @@ class S3Fixture:
|
|
|
111
110
|
self.storage, bucket_fixtures=[], object_fixtures=file_objects
|
|
112
111
|
)
|
|
113
112
|
|
|
114
|
-
async def empty_buckets(
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
async def empty_buckets(
|
|
114
|
+
self,
|
|
115
|
+
*,
|
|
116
|
+
buckets: Optional[Union[str, list[str]]] = None,
|
|
117
|
+
exclude_buckets: Optional[Union[str, list[str]]] = None,
|
|
118
|
+
):
|
|
119
|
+
"""Remove all test objects from the given bucket(s).
|
|
120
|
+
|
|
121
|
+
If no buckets are specified, all existing buckets will be emptied,
|
|
122
|
+
therefore we recommend to always specify a bucket list.
|
|
123
|
+
You can also specify bucket(s) that should be excluded
|
|
124
|
+
from the operation, i.e. buckets that should not be emptied.
|
|
125
|
+
"""
|
|
126
|
+
if buckets is None:
|
|
127
|
+
buckets = self.get_buckets()
|
|
128
|
+
elif isinstance(buckets, str):
|
|
129
|
+
buckets = [buckets]
|
|
130
|
+
if exclude_buckets is None:
|
|
131
|
+
exclude_buckets = []
|
|
132
|
+
elif isinstance(exclude_buckets, str):
|
|
133
|
+
exclude_buckets = [exclude_buckets]
|
|
134
|
+
excluded_buckets = set(exclude_buckets)
|
|
135
|
+
for bucket in buckets:
|
|
136
|
+
if bucket in excluded_buckets:
|
|
137
|
+
continue
|
|
117
138
|
# Get list of all objects in the bucket
|
|
118
139
|
object_ids = await self.storage.list_all_object_ids(bucket_id=bucket)
|
|
119
140
|
# Delete all of these objects
|
|
120
141
|
for object_id in object_ids:
|
|
142
|
+
# Note that id validation errors can be raised here if the bucket
|
|
143
|
+
# was populated by other means than the S3 storage fixture.
|
|
144
|
+
# We intentionally do not catch these errors because they
|
|
145
|
+
# usually mean that you're operating on the wrong buckets.
|
|
121
146
|
await self.storage.delete_object(bucket_id=bucket, object_id=object_id)
|
|
122
147
|
|
|
123
|
-
async def delete_buckets(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
async def delete_buckets(
|
|
149
|
+
self,
|
|
150
|
+
*,
|
|
151
|
+
buckets: Optional[Union[str, list[str]]] = None,
|
|
152
|
+
exclude_buckets: Optional[Union[str, list[str]]] = None,
|
|
153
|
+
):
|
|
154
|
+
"""Delete the given bucket(s).
|
|
155
|
+
|
|
156
|
+
If no buckets are specified, all existing buckets will be deleted,
|
|
157
|
+
therefore we recommend to always specify a bucket list.
|
|
158
|
+
You can also specify bucket(s) that should be excluded
|
|
159
|
+
from the operation, i.e. buckets that should be kept.
|
|
160
|
+
"""
|
|
161
|
+
if buckets is None:
|
|
162
|
+
buckets = self.get_buckets()
|
|
163
|
+
elif isinstance(buckets, str):
|
|
164
|
+
buckets = [buckets]
|
|
165
|
+
if exclude_buckets is None:
|
|
166
|
+
exclude_buckets = []
|
|
167
|
+
elif isinstance(exclude_buckets, str):
|
|
168
|
+
exclude_buckets = [exclude_buckets]
|
|
169
|
+
excluded_buckets = set(exclude_buckets)
|
|
170
|
+
for bucket in buckets:
|
|
171
|
+
if bucket not in excluded_buckets:
|
|
172
|
+
await self.storage.delete_bucket(bucket, delete_content=True)
|
|
127
173
|
|
|
128
174
|
async def get_initialized_upload(self) -> UploadDetails:
|
|
129
175
|
"""Initialize a new empty multipart upload process.
|
|
@@ -26,6 +26,8 @@ from typing import NamedTuple, Optional
|
|
|
26
26
|
from hexkit.custom_types import JsonObject
|
|
27
27
|
from hexkit.protocols.eventpub import EventPublisherProtocol
|
|
28
28
|
|
|
29
|
+
__all__ = ["InMemEventStore", "InMemEventPublisher", "TopicExhaustedError"]
|
|
30
|
+
|
|
29
31
|
|
|
30
32
|
class TopicExhaustedError(RuntimeError):
|
|
31
33
|
"""Thrown when now more event are queued in a topic."""
|
|
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
|