hexkit 1.0.0__tar.gz → 1.2.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.
Files changed (43) hide show
  1. {hexkit-1.0.0/src/hexkit.egg-info → hexkit-1.2.0}/PKG-INFO +4 -4
  2. {hexkit-1.0.0 → hexkit-1.2.0}/pyproject.toml +4 -4
  3. hexkit-1.2.0/src/hexkit/correlation.py +97 -0
  4. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/inject.py +1 -6
  5. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/protocols/dao.py +1 -1
  6. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/protocols/objstorage.py +0 -2
  7. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/provider.py +79 -31
  8. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/testutils.py +4 -3
  9. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/mongodb/provider.py +3 -3
  10. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/s3/provider.py +2 -8
  11. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/s3/testutils.py +0 -3
  12. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/utils.py +14 -1
  13. {hexkit-1.0.0 → hexkit-1.2.0/src/hexkit.egg-info}/PKG-INFO +4 -4
  14. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit.egg-info/SOURCES.txt +1 -0
  15. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit.egg-info/requires.txt +3 -3
  16. {hexkit-1.0.0 → hexkit-1.2.0}/LICENSE +0 -0
  17. {hexkit-1.0.0 → hexkit-1.2.0}/MANIFEST.in +0 -0
  18. {hexkit-1.0.0 → hexkit-1.2.0}/README.md +0 -0
  19. {hexkit-1.0.0 → hexkit-1.2.0}/setup.cfg +0 -0
  20. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/__init__.py +0 -0
  21. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/__main__.py +0 -0
  22. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/base.py +0 -0
  23. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/config.py +0 -0
  24. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/custom_types.py +0 -0
  25. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/protocols/__init__.py +0 -0
  26. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/protocols/eventpub.py +0 -0
  27. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/protocols/eventsub.py +0 -0
  28. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/__init__.py +0 -0
  29. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/__init__.py +0 -0
  30. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
  31. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
  32. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/mongodb/testutils.py +0 -0
  33. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/s3/__init__.py +0 -0
  34. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  35. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  36. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  37. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  38. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  39. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/testing/__init__.py +0 -0
  40. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/testing/eventpub.py +0 -0
  41. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit/providers/testing/utils.py +0 -0
  42. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  43. {hexkit-1.0.0 → hexkit-1.2.0}/src/hexkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hexkit
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: A Toolkit for Building Microservices using the Hexagonal Architecture
5
5
  Author-email: "German Human Genome Phenome Archive (GHGA)" <contact@ghga.de>
6
6
  License: Apache 2.0
@@ -31,13 +31,13 @@ Provides-Extra: mongodb
31
31
  Requires-Dist: motor<4,>=3.1.1; extra == "mongodb"
32
32
  Provides-Extra: test-akafka
33
33
  Requires-Dist: hexkit[akafka]; extra == "test-akafka"
34
- Requires-Dist: testcontainers[kafka]<4,>=3.4.1; extra == "test-akafka"
34
+ Requires-Dist: testcontainers[kafka]<4,>=3.7.1; extra == "test-akafka"
35
35
  Provides-Extra: test-s3
36
36
  Requires-Dist: hexkit[s3]; extra == "test-s3"
37
- Requires-Dist: testcontainers<4,>=3.4.1; extra == "test-s3"
37
+ Requires-Dist: testcontainers<4,>=3.7.1; extra == "test-s3"
38
38
  Provides-Extra: test-mongodb
39
39
  Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
40
- Requires-Dist: testcontainers[mongo]<4,>=3.4.1; extra == "test-mongodb"
40
+ Requires-Dist: testcontainers[mongo]<4,>=3.7.1; extra == "test-mongodb"
41
41
  Provides-Extra: test
42
42
  Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
43
43
  Provides-Extra: all
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hexkit"
7
- version = "1.0.0"
7
+ version = "1.2.0"
8
8
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -35,9 +35,9 @@ akafka = ["aiokafka~=0.8.0", "jsonschema >=4.17.3, <5"]
35
35
  s3 = ["boto3 >=1.26.50, <2", "botocore >=1.29.50, <2"]
36
36
  mongodb = ["motor >=3.1.1, <4"]
37
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"]
38
+ test-akafka = ["hexkit[akafka]", "testcontainers[kafka] >=3.7.1, <4"]
39
+ test-s3 = ["hexkit[s3]", "testcontainers >=3.7.1, <4"]
40
+ test-mongodb = ["hexkit[mongodb]", "testcontainers[mongo] >=3.7.1, <4"]
41
41
  test = ["hexkit[test-akafka,test-s3,test-mongodb]"]
42
42
 
43
43
  all = ["hexkit[test]"]
@@ -0,0 +1,97 @@
1
+ # Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Utilities related to correlation IDs"""
18
+
19
+ import logging
20
+ from contextlib import asynccontextmanager
21
+ from contextvars import ContextVar
22
+ from uuid import UUID, uuid4
23
+
24
+ from hexkit.utils import set_context_var
25
+
26
+ log = logging.getLogger(__name__)
27
+
28
+ correlation_id_var: ContextVar[str] = ContextVar("correlation_id", default="")
29
+
30
+ __all__ = [
31
+ "set_correlation_id",
32
+ "get_correlation_id",
33
+ "new_correlation_id",
34
+ "validate_correlation_id",
35
+ "CorrelationIdContextError",
36
+ "InvalidCorrelationIdError",
37
+ ]
38
+
39
+
40
+ class CorrelationIdContextError(RuntimeError):
41
+ """Raised when the correlation ID ContextVar is unexpectedly not set."""
42
+
43
+
44
+ class InvalidCorrelationIdError(RuntimeError):
45
+ """Raised when a correlation ID fails validation."""
46
+
47
+ def __init__(self, *, correlation_id: str):
48
+ message = f"Invalid correlation ID found: '{correlation_id}'"
49
+ super().__init__(message)
50
+
51
+
52
+ def new_correlation_id() -> str:
53
+ """Generates a new correlation ID."""
54
+ return str(uuid4())
55
+
56
+
57
+ def validate_correlation_id(correlation_id: str):
58
+ """Raises an error if the correlation ID is invalid.
59
+
60
+ Raises:
61
+ InvalidCorrelationIdError: If the correlation ID is empty or invalid.
62
+ """
63
+ try:
64
+ UUID(correlation_id)
65
+ except ValueError as err:
66
+ raise InvalidCorrelationIdError(correlation_id=correlation_id) from err
67
+
68
+
69
+ @asynccontextmanager
70
+ async def set_correlation_id(correlation_id: str):
71
+ """Set the correlation ID for the life of the context.
72
+
73
+ Raises:
74
+ InvalidCorrelationIdError: when the correlation ID is empty or invalid.
75
+ """
76
+ validate_correlation_id(correlation_id)
77
+
78
+ async with set_context_var(correlation_id_var, correlation_id):
79
+ log.info("Set context correlation ID to %s", correlation_id)
80
+ yield
81
+
82
+
83
+ def get_correlation_id() -> str:
84
+ """Get the correlation ID.
85
+
86
+ This should only be called when the correlation ID ContextVar is expected to be set.
87
+
88
+ Raises:
89
+ CorrelationIdContextError: when the correlation ID ContextVar is not set.
90
+ InvalidCorrelationIdError: when the correlation ID is invalid.
91
+ """
92
+ if not (correlation_id := correlation_id_var.get()):
93
+ raise CorrelationIdContextError()
94
+
95
+ validate_correlation_id(correlation_id)
96
+
97
+ return correlation_id
@@ -46,8 +46,6 @@ __all__ = [
46
46
  "Configurator",
47
47
  ]
48
48
 
49
- # pylint: disable=c-extension-no-member
50
-
51
49
 
52
50
  class NotConstructableError(TypeError):
53
51
  """Thrown when an AsyncContextConstructable was expected but not obtained."""
@@ -60,7 +58,7 @@ class AsyncInitShutdownError(TypeError):
60
58
 
61
59
 
62
60
  def assert_async_constructable(
63
- constructable: Union[type[AsyncContextConstructable], type[AsyncConstructable]]
61
+ constructable: Union[type[AsyncContextConstructable], type[AsyncConstructable]],
64
62
  ):
65
63
  """
66
64
  Make sure that the provided object has a callable attribute `construct`.
@@ -108,9 +106,6 @@ class AsyncConstructor(dependency_injector.providers.Resource):
108
106
 
109
107
  return resource
110
108
 
111
- # This pylint error is inherited from the dependency_injector framework, other
112
- # DI providers use the same basic signature:
113
- # pylint: disable=keyword-arg-before-vararg
114
109
  def __init__(
115
110
  self,
116
111
  provides: Optional[
@@ -450,7 +450,7 @@ class DaoFactoryProtocol(ABC):
450
450
 
451
451
  @overload
452
452
  @abstractmethod
453
- async def _get_dao( # pylint: disable=arguments-differ
453
+ async def _get_dao(
454
454
  self,
455
455
  *,
456
456
  name: str,
@@ -155,7 +155,6 @@ class ObjectStorageProtocol(ABC):
155
155
  upload_id=upload_id, bucket_id=bucket_id, object_id=object_id
156
156
  )
157
157
 
158
- # pylint: disable=too-many-arguments
159
158
  async def complete_multipart_upload(
160
159
  self,
161
160
  *,
@@ -367,7 +366,6 @@ class ObjectStorageProtocol(ABC):
367
366
  """
368
367
  ...
369
368
 
370
- # pylint: disable=too-many-arguments
371
369
  @abstractmethod
372
370
  async def _complete_multipart_upload(
373
371
  self,
@@ -25,7 +25,7 @@ import json
25
25
  import logging
26
26
  import ssl
27
27
  from contextlib import asynccontextmanager
28
- from typing import Any, Callable, Optional, Protocol, TypeVar
28
+ from typing import Any, Callable, Literal, Optional, Protocol, TypeVar
29
29
 
30
30
  from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
31
31
  from aiokafka.helpers import create_ssl_context
@@ -33,15 +33,17 @@ from pydantic import Field
33
33
  from pydantic_settings import BaseSettings
34
34
 
35
35
  from hexkit.base import InboundProviderBase
36
+ from hexkit.correlation import (
37
+ CorrelationIdContextError,
38
+ get_correlation_id,
39
+ new_correlation_id,
40
+ set_correlation_id,
41
+ validate_correlation_id,
42
+ )
36
43
  from hexkit.custom_types import Ascii, JsonObject
37
44
  from hexkit.protocols.eventpub import EventPublisherProtocol
38
45
  from hexkit.protocols.eventsub import EventSubscriberProtocol
39
46
 
40
- try: # workaround for https://github.com/pydantic/pydantic/issues/5821
41
- from typing_extensions import Literal
42
- except ImportError:
43
- from typing import Literal # type: ignore
44
-
45
47
  __all__ = [
46
48
  "KafkaConfig",
47
49
  "KafkaEventPublisher",
@@ -73,32 +75,45 @@ class KafkaConfig(BaseSettings):
73
75
  description="A list of connection strings to connect to Kafka bootstrap servers.",
74
76
  )
75
77
  kafka_security_protocol: Literal["PLAINTEXT", "SSL"] = Field(
76
- "PLAINTEXT",
78
+ default="PLAINTEXT",
77
79
  description="Protocol used to communicate with brokers. "
78
80
  + "Valid values are: PLAINTEXT, SSL.",
79
81
  )
80
82
  kafka_ssl_cafile: str = Field(
81
- "",
83
+ default="",
82
84
  description="Certificate Authority file path containing certificates"
83
- + " used to sign broker certificates. If a CA not specified, the default"
85
+ + " used to sign broker certificates. If a CA is not specified, the default"
84
86
  + " system CA will be used if found by OpenSSL.",
85
87
  )
86
88
  kafka_ssl_certfile: str = Field(
87
- "",
89
+ default="",
88
90
  description="Optional filename of client certificate, as well as any"
89
91
  + " CA certificates needed to establish the certificate's authenticity.",
90
92
  )
91
93
  kafka_ssl_keyfile: str = Field(
92
- "", description="Optional filename containing the client private key."
94
+ default="", description="Optional filename containing the client private key."
93
95
  )
94
96
  kafka_ssl_password: str = Field(
95
- "",
97
+ default="",
96
98
  description="Optional password to be used for the client private key.",
97
99
  )
100
+ generate_correlation_id: bool = Field(
101
+ default=True,
102
+ examples=[True, False],
103
+ description=(
104
+ "A flag, which, if False, will result in an error when trying to publish an"
105
+ + " event without a valid correlation ID set for the context. If True, the a"
106
+ + " newly correlation ID will be generated and used in the event header."
107
+ ),
108
+ )
98
109
 
99
110
 
100
- class EventTypeNotFoundError(RuntimeError):
101
- """Thrown when no `type` was set in the headers of an event."""
111
+ class EventHeaderNotFoundError(RuntimeError):
112
+ """Thrown when a given detail was not set in the headers of an event."""
113
+
114
+ def __init__(self, *, header_name):
115
+ message = f"No '{header_name}' was set in event header."
116
+ super().__init__(message)
102
117
 
103
118
 
104
119
  def generate_client_id(*, service_name: str, instance_id: str) -> str:
@@ -198,9 +213,13 @@ class KafkaEventPublisher(EventPublisherProtocol):
198
213
  "ascii"
199
214
  ),
200
215
  )
216
+
201
217
  try:
202
218
  await producer.start()
203
- yield cls(producer=producer)
219
+ yield cls(
220
+ producer=producer,
221
+ generate_correlation_id=config.generate_correlation_id,
222
+ )
204
223
  finally:
205
224
  await producer.stop()
206
225
 
@@ -208,13 +227,19 @@ class KafkaEventPublisher(EventPublisherProtocol):
208
227
  self,
209
228
  *,
210
229
  producer: KafkaProducerCompatible,
230
+ generate_correlation_id: bool,
211
231
  ):
212
232
  """Please do not call directly! Should be called by the `construct` method.
213
233
  Args:
214
234
  producer:
215
235
  hands over a started AIOKafkaProducer.
236
+ generate_correlation_id:
237
+ whether to throw an error or generate a new ID if no correlation ID is
238
+ set for the context when trying to publish an event. An invalid ID will
239
+ result in an error in either case.
216
240
  """
217
241
  self._producer = producer
242
+ self._generate_correlation_id = generate_correlation_id
218
243
 
219
244
  async def _publish_validated(
220
245
  self, *, payload: JsonObject, type_: Ascii, key: Ascii, topic: Ascii
@@ -227,8 +252,21 @@ class KafkaEventPublisher(EventPublisherProtocol):
227
252
  key (str): The event type. ASCII characters only.
228
253
  topic (str): The event type. ASCII characters only.
229
254
  """
230
- event_headers = [("type", type_.encode("ascii"))]
255
+ try:
256
+ correlation_id = get_correlation_id()
257
+ except CorrelationIdContextError:
258
+ if not self._generate_correlation_id:
259
+ raise
260
+
261
+ correlation_id = new_correlation_id()
262
+ logging.info("Generated new correlation id: %s", correlation_id)
263
+
264
+ validate_correlation_id(correlation_id)
231
265
 
266
+ event_headers = [
267
+ ("type", type_.encode("ascii")),
268
+ ("correlation_id", correlation_id.encode("ascii")),
269
+ ]
232
270
  await self._producer.send_and_wait(
233
271
  topic, key=key, value=payload, headers=event_headers
234
272
  )
@@ -245,12 +283,17 @@ class ConsumerEvent(Protocol):
245
283
  offset: int
246
284
 
247
285
 
248
- def get_event_type(event: ConsumerEvent) -> str:
249
- """Extract the event type out of an ConsumerEvent."""
250
- for header in event.headers:
251
- if header[0] == "type":
252
- return header[1].decode("ascii")
253
- raise EventTypeNotFoundError()
286
+ def headers_as_dict(event: ConsumerEvent) -> dict[str, str]:
287
+ """Extract the headers from a ConsumerEvent object and return them as a dict."""
288
+ return {name: value.decode("ascii") for name, value in event.headers}
289
+
290
+
291
+ def get_header_value(header_name: str, headers: dict[str, str]) -> str:
292
+ """Extract the given value from the dict headers and raise an error if not found."""
293
+ try:
294
+ return headers[header_name]
295
+ except KeyError as err:
296
+ raise EventHeaderNotFoundError(header_name=header_name) from err
254
297
 
255
298
 
256
299
  KCC = TypeVar("KCC")
@@ -358,8 +401,6 @@ class KafkaEventSubscriber(InboundProviderBase):
358
401
  finally:
359
402
  await consumer.stop()
360
403
 
361
- # pylint: disable=too-many-arguments
362
- # (some arguments are only used for testing)
363
404
  def __init__(
364
405
  self, *, consumer: KafkaConsumerCompatible, translator: EventSubscriberProtocol
365
406
  ):
@@ -387,21 +428,28 @@ class KafkaEventSubscriber(InboundProviderBase):
387
428
  async def _consume_event(self, event: ConsumerEvent) -> None:
388
429
  """Consume an event by passing it down to the translator via the protocol."""
389
430
  event_label = self._get_event_label(event)
431
+ headers = headers_as_dict(event)
390
432
 
391
433
  try:
392
- type_ = get_event_type(event)
393
- except EventTypeNotFoundError:
394
- logging.warning("Ignored an event without type: %s", event_label)
434
+ type_ = get_header_value(header_name="type", headers=headers)
435
+ correlation_id = get_header_value(
436
+ header_name="correlation_id", headers=headers
437
+ )
438
+ except EventHeaderNotFoundError as err:
439
+ logging.warning("Ignored an event: %s. %s", event_label, err.args[0])
395
440
  return
396
441
 
397
442
  if type_ in self._types_whitelist:
398
443
  logging.info('Consuming event of type "%s": %s', type_, event_label)
399
444
 
400
445
  try:
401
- # blocks until event processing is completed:
402
- await self._translator.consume(
403
- payload=event.value, type_=type_, topic=event.topic
404
- )
446
+ async with set_correlation_id(correlation_id):
447
+ # blocks until event processing is completed:
448
+ await self._translator.consume(
449
+ payload=event.value,
450
+ type_=type_,
451
+ topic=event.topic,
452
+ )
405
453
  except Exception:
406
454
  logging.error(
407
455
  "A fatal error occurred while processing the event: %s",
@@ -36,7 +36,8 @@ from hexkit.providers.akafka.provider import (
36
36
  ConsumerEvent,
37
37
  KafkaConfig,
38
38
  KafkaEventPublisher,
39
- get_event_type,
39
+ get_header_value,
40
+ headers_as_dict,
40
41
  )
41
42
  from hexkit.providers.akafka.testcontainer import DEFAULT_IMAGE as KAFKA_IMAGE
42
43
 
@@ -282,7 +283,7 @@ class EventRecorder:
282
283
  return [
283
284
  RecordedEvent(
284
285
  payload=raw_event.value,
285
- type_=get_event_type(raw_event),
286
+ type_=get_header_value("type", headers=headers_as_dict(raw_event)),
286
287
  key=raw_event.key,
287
288
  )
288
289
  for raw_event in raw_events
@@ -406,7 +407,7 @@ async def kafka_fixture_function() -> AsyncGenerator[KafkaFixture, None]:
406
407
  """
407
408
  with KafkaContainer(image=KAFKA_IMAGE) as kafka:
408
409
  kafka_servers = [kafka.get_bootstrap_server()]
409
- config = KafkaConfig( # type: ignore
410
+ config = KafkaConfig(
410
411
  service_name="test_publisher",
411
412
  service_instance_id="001",
412
413
  kafka_servers=kafka_servers,
@@ -111,7 +111,7 @@ class MongoDbDaoBase(ABC, Generic[Dto]):
111
111
  Raises:
112
112
  ResourceNotFoundError: when resource with the specified id_ was not found
113
113
  """
114
- document = await self._collection.find_one( # type: ignore
114
+ document = await self._collection.find_one(
115
115
  {"_id": id_},
116
116
  session=self._session,
117
117
  )
@@ -406,7 +406,7 @@ class MongoDbDaoFactory(DaoFactoryProtocol):
406
406
  self._config = config
407
407
 
408
408
  # get a database-specific client:
409
- self._client = AsyncIOMotorClient(
409
+ self._client = AsyncIOMotorClient( # type: ignore
410
410
  self._config.db_connection_str.get_secret_value()
411
411
  )
412
412
  self._db = self._client[self._config.db_name]
@@ -428,7 +428,7 @@ class MongoDbDaoFactory(DaoFactoryProtocol):
428
428
  ...
429
429
 
430
430
  @overload
431
- async def _get_dao( # pylint: disable=arguments-differ
431
+ async def _get_dao(
432
432
  self,
433
433
  *,
434
434
  name: str,
@@ -97,7 +97,6 @@ class S3Config(BaseSettings):
97
97
  description=(
98
98
  "Path to a config file for specifying more advanced S3 parameters."
99
99
  + " This should follow the format described here:"
100
- # pylint: disable=line-too-long
101
100
  + " https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#using-a-configuration-file"
102
101
  ),
103
102
  )
@@ -114,12 +113,10 @@ def read_aws_config_ini(aws_config_ini: Path) -> botocore.config.Config:
114
113
  return botocore.config.Config(**config_profile)
115
114
 
116
115
 
117
- class S3ObjectStorage(
118
- ObjectStorageProtocol
119
- ): # pylint: disable=too-many-instance-attributes
116
+ class S3ObjectStorage(ObjectStorageProtocol):
120
117
  """S3-based provider implementing the ObjectStorageProtocol."""
121
118
 
122
- def __init__( # pylint: disable=too-many-arguments
119
+ def __init__(
123
120
  self,
124
121
  *,
125
122
  config: S3Config,
@@ -548,7 +545,6 @@ class S3ObjectStorage(
548
545
  object_id=object_id,
549
546
  ) from error
550
547
 
551
- # pylint: disable=too-many-arguments
552
548
  async def _check_uploaded_parts(
553
549
  self,
554
550
  *,
@@ -629,7 +625,6 @@ class S3ObjectStorage(
629
625
  + f" first part which had only {first_part_size}.",
630
626
  )
631
627
 
632
- # pylint: disable=too-many-arguments
633
628
  async def _abort_multipart_upload(
634
629
  self,
635
630
  *,
@@ -686,7 +681,6 @@ class S3ObjectStorage(
686
681
  upload_id=upload_id, bucket_id=bucket_id, object_id=object_id
687
682
  )
688
683
 
689
- # pylint: disable=too-many-arguments
690
684
  async def _complete_multipart_upload(
691
685
  self,
692
686
  *,
@@ -200,7 +200,6 @@ def check_part_size(file_path: Path, anticipated_size: int) -> None:
200
200
  )
201
201
 
202
202
 
203
- # pylint: disable=too-many-arguments
204
203
  async def upload_part(
205
204
  storage_dao: ObjectStorageProtocol,
206
205
  upload_id: str,
@@ -220,7 +219,6 @@ async def upload_part(
220
219
  response.raise_for_status()
221
220
 
222
221
 
223
- # pylint: disable=too-many-arguments
224
222
  async def upload_part_of_size(
225
223
  storage_dao: ObjectStorageProtocol,
226
224
  upload_id: str,
@@ -372,7 +370,6 @@ async def prepare_non_completed_upload(s3_fixture_: S3Fixture):
372
370
  # This workflow is defined as a seperate function so that it can also be used
373
371
  # outside of the `tests` package e.g. to test the compliance of an S3-compatible
374
372
  # object storage implementation:
375
- # pylint: disable=too-many-arguments
376
373
  async def typical_workflow(
377
374
  storage_client: ObjectStorageProtocol,
378
375
  test_file_path: Path,
@@ -16,7 +16,9 @@
16
16
  """General utilities that don't require heavy dependencies."""
17
17
 
18
18
  from collections.abc import Collection
19
- from typing import Optional
19
+ from contextlib import asynccontextmanager
20
+ from contextvars import ContextVar
21
+ from typing import Any, Optional
20
22
 
21
23
  from pydantic import BaseModel
22
24
 
@@ -111,3 +113,14 @@ def validate_fields_in_model(
111
113
  if not fields_set.issubset(existing_fields_set):
112
114
  unexpected_fields = fields_set.difference(existing_fields_set)
113
115
  raise FieldNotInModelError(model=model, unexpected_field=unexpected_fields)
116
+
117
+
118
+ @asynccontextmanager
119
+ async def set_context_var(context_var: ContextVar, value: Any):
120
+ """An async context manager to simplify the use of ContextVars.
121
+
122
+ The value will be reset upon exiting the context.
123
+ """
124
+ token = context_var.set(value)
125
+ yield
126
+ context_var.reset(token)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hexkit
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: A Toolkit for Building Microservices using the Hexagonal Architecture
5
5
  Author-email: "German Human Genome Phenome Archive (GHGA)" <contact@ghga.de>
6
6
  License: Apache 2.0
@@ -31,13 +31,13 @@ Provides-Extra: mongodb
31
31
  Requires-Dist: motor<4,>=3.1.1; extra == "mongodb"
32
32
  Provides-Extra: test-akafka
33
33
  Requires-Dist: hexkit[akafka]; extra == "test-akafka"
34
- Requires-Dist: testcontainers[kafka]<4,>=3.4.1; extra == "test-akafka"
34
+ Requires-Dist: testcontainers[kafka]<4,>=3.7.1; extra == "test-akafka"
35
35
  Provides-Extra: test-s3
36
36
  Requires-Dist: hexkit[s3]; extra == "test-s3"
37
- Requires-Dist: testcontainers<4,>=3.4.1; extra == "test-s3"
37
+ Requires-Dist: testcontainers<4,>=3.7.1; extra == "test-s3"
38
38
  Provides-Extra: test-mongodb
39
39
  Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
40
- Requires-Dist: testcontainers[mongo]<4,>=3.4.1; extra == "test-mongodb"
40
+ Requires-Dist: testcontainers[mongo]<4,>=3.7.1; extra == "test-mongodb"
41
41
  Provides-Extra: test
42
42
  Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
43
43
  Provides-Extra: all
@@ -6,6 +6,7 @@ src/hexkit/__init__.py
6
6
  src/hexkit/__main__.py
7
7
  src/hexkit/base.py
8
8
  src/hexkit/config.py
9
+ src/hexkit/correlation.py
9
10
  src/hexkit/custom_types.py
10
11
  src/hexkit/inject.py
11
12
  src/hexkit/utils.py
@@ -22,12 +22,12 @@ hexkit[test-akafka,test-mongodb,test-s3]
22
22
 
23
23
  [test-akafka]
24
24
  hexkit[akafka]
25
- testcontainers[kafka]<4,>=3.4.1
25
+ testcontainers[kafka]<4,>=3.7.1
26
26
 
27
27
  [test-mongodb]
28
28
  hexkit[mongodb]
29
- testcontainers[mongo]<4,>=3.4.1
29
+ testcontainers[mongo]<4,>=3.7.1
30
30
 
31
31
  [test-s3]
32
32
  hexkit[s3]
33
- testcontainers<4,>=3.4.1
33
+ testcontainers<4,>=3.7.1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes