hexkit 1.1.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.1.0/src/hexkit.egg-info → hexkit-1.2.0}/PKG-INFO +1 -1
  2. {hexkit-1.1.0 → hexkit-1.2.0}/pyproject.toml +1 -1
  3. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/correlation.py +1 -1
  4. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/inject.py +1 -1
  5. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/provider.py +78 -23
  6. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/testutils.py +4 -3
  7. {hexkit-1.1.0 → hexkit-1.2.0/src/hexkit.egg-info}/PKG-INFO +1 -1
  8. {hexkit-1.1.0 → hexkit-1.2.0}/LICENSE +0 -0
  9. {hexkit-1.1.0 → hexkit-1.2.0}/MANIFEST.in +0 -0
  10. {hexkit-1.1.0 → hexkit-1.2.0}/README.md +0 -0
  11. {hexkit-1.1.0 → hexkit-1.2.0}/setup.cfg +0 -0
  12. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/__init__.py +0 -0
  13. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/__main__.py +0 -0
  14. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/base.py +0 -0
  15. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/config.py +0 -0
  16. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/custom_types.py +0 -0
  17. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/protocols/__init__.py +0 -0
  18. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/protocols/dao.py +0 -0
  19. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/protocols/eventpub.py +0 -0
  20. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/protocols/eventsub.py +0 -0
  21. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/protocols/objstorage.py +0 -0
  22. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/__init__.py +0 -0
  23. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/__init__.py +0 -0
  24. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
  25. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
  26. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/mongodb/provider.py +0 -0
  27. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/mongodb/testutils.py +0 -0
  28. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/__init__.py +0 -0
  29. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/provider.py +0 -0
  30. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  31. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  32. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  33. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  34. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  35. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/testutils.py +0 -0
  36. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/testing/__init__.py +0 -0
  37. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/testing/eventpub.py +0 -0
  38. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/testing/utils.py +0 -0
  39. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/utils.py +0 -0
  40. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
  41. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  42. {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit.egg-info/requires.txt +0 -0
  43. {hexkit-1.1.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.1.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hexkit"
7
- version = "1.1.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 = [
@@ -23,7 +23,7 @@ from uuid import UUID, uuid4
23
23
 
24
24
  from hexkit.utils import set_context_var
25
25
 
26
- log = logging.getLogger()
26
+ log = logging.getLogger(__name__)
27
27
 
28
28
  correlation_id_var: ContextVar[str] = ContextVar("correlation_id", default="")
29
29
 
@@ -58,7 +58,7 @@ class AsyncInitShutdownError(TypeError):
58
58
 
59
59
 
60
60
  def assert_async_constructable(
61
- constructable: Union[type[AsyncContextConstructable], type[AsyncConstructable]]
61
+ constructable: Union[type[AsyncContextConstructable], type[AsyncConstructable]],
62
62
  ):
63
63
  """
64
64
  Make sure that the provided object has a callable attribute `construct`.
@@ -33,6 +33,13 @@ 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
@@ -68,32 +75,45 @@ class KafkaConfig(BaseSettings):
68
75
  description="A list of connection strings to connect to Kafka bootstrap servers.",
69
76
  )
70
77
  kafka_security_protocol: Literal["PLAINTEXT", "SSL"] = Field(
71
- "PLAINTEXT",
78
+ default="PLAINTEXT",
72
79
  description="Protocol used to communicate with brokers. "
73
80
  + "Valid values are: PLAINTEXT, SSL.",
74
81
  )
75
82
  kafka_ssl_cafile: str = Field(
76
- "",
83
+ default="",
77
84
  description="Certificate Authority file path containing certificates"
78
- + " 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"
79
86
  + " system CA will be used if found by OpenSSL.",
80
87
  )
81
88
  kafka_ssl_certfile: str = Field(
82
- "",
89
+ default="",
83
90
  description="Optional filename of client certificate, as well as any"
84
91
  + " CA certificates needed to establish the certificate's authenticity.",
85
92
  )
86
93
  kafka_ssl_keyfile: str = Field(
87
- "", description="Optional filename containing the client private key."
94
+ default="", description="Optional filename containing the client private key."
88
95
  )
89
96
  kafka_ssl_password: str = Field(
90
- "",
97
+ default="",
91
98
  description="Optional password to be used for the client private key.",
92
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
+ )
93
109
 
94
110
 
95
- class EventTypeNotFoundError(RuntimeError):
96
- """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)
97
117
 
98
118
 
99
119
  def generate_client_id(*, service_name: str, instance_id: str) -> str:
@@ -193,9 +213,13 @@ class KafkaEventPublisher(EventPublisherProtocol):
193
213
  "ascii"
194
214
  ),
195
215
  )
216
+
196
217
  try:
197
218
  await producer.start()
198
- yield cls(producer=producer)
219
+ yield cls(
220
+ producer=producer,
221
+ generate_correlation_id=config.generate_correlation_id,
222
+ )
199
223
  finally:
200
224
  await producer.stop()
201
225
 
@@ -203,13 +227,19 @@ class KafkaEventPublisher(EventPublisherProtocol):
203
227
  self,
204
228
  *,
205
229
  producer: KafkaProducerCompatible,
230
+ generate_correlation_id: bool,
206
231
  ):
207
232
  """Please do not call directly! Should be called by the `construct` method.
208
233
  Args:
209
234
  producer:
210
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.
211
240
  """
212
241
  self._producer = producer
242
+ self._generate_correlation_id = generate_correlation_id
213
243
 
214
244
  async def _publish_validated(
215
245
  self, *, payload: JsonObject, type_: Ascii, key: Ascii, topic: Ascii
@@ -222,8 +252,21 @@ class KafkaEventPublisher(EventPublisherProtocol):
222
252
  key (str): The event type. ASCII characters only.
223
253
  topic (str): The event type. ASCII characters only.
224
254
  """
225
- 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)
226
265
 
266
+ event_headers = [
267
+ ("type", type_.encode("ascii")),
268
+ ("correlation_id", correlation_id.encode("ascii")),
269
+ ]
227
270
  await self._producer.send_and_wait(
228
271
  topic, key=key, value=payload, headers=event_headers
229
272
  )
@@ -240,12 +283,17 @@ class ConsumerEvent(Protocol):
240
283
  offset: int
241
284
 
242
285
 
243
- def get_event_type(event: ConsumerEvent) -> str:
244
- """Extract the event type out of an ConsumerEvent."""
245
- for header in event.headers:
246
- if header[0] == "type":
247
- return header[1].decode("ascii")
248
- 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
249
297
 
250
298
 
251
299
  KCC = TypeVar("KCC")
@@ -380,21 +428,28 @@ class KafkaEventSubscriber(InboundProviderBase):
380
428
  async def _consume_event(self, event: ConsumerEvent) -> None:
381
429
  """Consume an event by passing it down to the translator via the protocol."""
382
430
  event_label = self._get_event_label(event)
431
+ headers = headers_as_dict(event)
383
432
 
384
433
  try:
385
- type_ = get_event_type(event)
386
- except EventTypeNotFoundError:
387
- 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])
388
440
  return
389
441
 
390
442
  if type_ in self._types_whitelist:
391
443
  logging.info('Consuming event of type "%s": %s', type_, event_label)
392
444
 
393
445
  try:
394
- # blocks until event processing is completed:
395
- await self._translator.consume(
396
- payload=event.value, type_=type_, topic=event.topic
397
- )
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
+ )
398
453
  except Exception:
399
454
  logging.error(
400
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hexkit
3
- Version: 1.1.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
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