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.
- {hexkit-1.1.0/src/hexkit.egg-info → hexkit-1.2.0}/PKG-INFO +1 -1
- {hexkit-1.1.0 → hexkit-1.2.0}/pyproject.toml +1 -1
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/correlation.py +1 -1
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/inject.py +1 -1
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/provider.py +78 -23
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/testutils.py +4 -3
- {hexkit-1.1.0 → hexkit-1.2.0/src/hexkit.egg-info}/PKG-INFO +1 -1
- {hexkit-1.1.0 → hexkit-1.2.0}/LICENSE +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/MANIFEST.in +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/README.md +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/setup.cfg +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/__init__.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/__main__.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/base.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/config.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/custom_types.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/protocols/__init__.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/protocols/dao.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/protocols/eventpub.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/protocols/eventsub.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/protocols/objstorage.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/__init__.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/__init__.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/mongodb/provider.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/mongodb/testutils.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/__init__.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/provider.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/s3/testutils.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/testing/__init__.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/testing/eventpub.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/providers/testing/utils.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit/utils.py +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit.egg-info/requires.txt +0 -0
- {hexkit-1.1.0 → hexkit-1.2.0}/src/hexkit.egg-info/top_level.txt +0 -0
|
@@ -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
|
|
96
|
-
"""Thrown when
|
|
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(
|
|
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
|
-
|
|
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
|
|
244
|
-
"""Extract the
|
|
245
|
-
for
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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_ =
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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_=
|
|
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(
|
|
410
|
+
config = KafkaConfig(
|
|
410
411
|
service_name="test_publisher",
|
|
411
412
|
service_instance_id="001",
|
|
412
413
|
kafka_servers=kafka_servers,
|
|
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
|