hexkit 4.0.0__tar.gz → 4.1.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-4.0.0/src/hexkit.egg-info → hexkit-4.1.0}/PKG-INFO +1 -1
- {hexkit-4.0.0 → hexkit-4.1.0}/pyproject.toml +1 -1
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/akafka/__init__.py +2 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/akafka/provider/__init__.py +2 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/akafka/provider/daosub.py +10 -66
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/akafka/provider/eventsub.py +105 -0
- {hexkit-4.0.0 → hexkit-4.1.0/src/hexkit.egg-info}/PKG-INFO +1 -1
- {hexkit-4.0.0 → hexkit-4.1.0}/LICENSE +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/MANIFEST.in +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/README.md +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/setup.cfg +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/__init__.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/__main__.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/base.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/config.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/correlation.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/custom_types.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/log.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/protocols/__init__.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/protocols/dao.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/protocols/daopub.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/protocols/daosub.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/protocols/eventpub.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/protocols/eventsub.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/protocols/objstorage.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/__init__.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/akafka/config.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/akafka/testutils.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/mongodb/provider.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/mongodb/testutils.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/mongokafka/provider.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/s3/__init__.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/s3/provider.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/s3/testutils/_fixtures.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/testing/__init__.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/providers/testing/eventpub.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/py.typed +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit/utils.py +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit.egg-info/requires.txt +0 -0
- {hexkit-4.0.0 → hexkit-4.1.0}/src/hexkit.egg-info/top_level.txt +0 -0
|
@@ -21,12 +21,14 @@ respectively.
|
|
|
21
21
|
|
|
22
22
|
from .config import KafkaConfig
|
|
23
23
|
from .provider import (
|
|
24
|
+
ComboTranslator,
|
|
24
25
|
KafkaEventPublisher,
|
|
25
26
|
KafkaEventSubscriber,
|
|
26
27
|
KafkaOutboxSubscriber,
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
__all__ = [
|
|
31
|
+
"ComboTranslator",
|
|
30
32
|
"KafkaConfig",
|
|
31
33
|
"KafkaEventPublisher",
|
|
32
34
|
"KafkaEventSubscriber",
|
|
@@ -22,6 +22,7 @@ Require dependencies of the `akafka` extra.
|
|
|
22
22
|
from .daosub import KafkaOutboxSubscriber
|
|
23
23
|
from .eventpub import KafkaEventPublisher
|
|
24
24
|
from .eventsub import (
|
|
25
|
+
ComboTranslator,
|
|
25
26
|
ConsumerEvent,
|
|
26
27
|
ExtractedEventInfo,
|
|
27
28
|
KafkaEventSubscriber,
|
|
@@ -29,6 +30,7 @@ from .eventsub import (
|
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
__all__ = [
|
|
33
|
+
"ComboTranslator",
|
|
32
34
|
"ConsumerEvent",
|
|
33
35
|
"ExtractedEventInfo",
|
|
34
36
|
"KafkaEventPublisher",
|
|
@@ -24,85 +24,29 @@ from contextlib import asynccontextmanager
|
|
|
24
24
|
from typing import Optional
|
|
25
25
|
|
|
26
26
|
from aiokafka import AIOKafkaConsumer
|
|
27
|
-
from pydantic import ValidationError
|
|
28
27
|
|
|
29
28
|
from hexkit.base import InboundProviderBase
|
|
30
|
-
from hexkit.
|
|
31
|
-
from hexkit.protocols.daosub import (
|
|
32
|
-
DaoSubscriberProtocol,
|
|
33
|
-
DtoValidationError,
|
|
34
|
-
)
|
|
29
|
+
from hexkit.protocols.daosub import DaoSubscriberProtocol
|
|
35
30
|
from hexkit.protocols.eventpub import EventPublisherProtocol
|
|
36
|
-
from hexkit.protocols.eventsub import EventSubscriberProtocol
|
|
37
31
|
from hexkit.providers.akafka.config import KafkaConfig
|
|
38
32
|
from hexkit.providers.akafka.provider.eventsub import (
|
|
33
|
+
CHANGE_EVENT_TYPE, # noqa: F401 (export for backwards compatibility until v5)
|
|
34
|
+
DELETE_EVENT_TYPE, # noqa: F401 (export for backwards compatibility until v5)
|
|
39
35
|
KafkaConsumerCompatible,
|
|
40
36
|
KafkaEventSubscriber,
|
|
41
37
|
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class TranslatorConverter(EventSubscriberProtocol):
|
|
48
|
-
"""Takes a list of translators implementing the `DaoSubscriberProtocol` to
|
|
49
|
-
create a single translator implementing the `EventSubscriberProtocol`.
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
types_of_interest = [CHANGE_EVENT_TYPE, DELETE_EVENT_TYPE]
|
|
53
|
-
|
|
54
|
-
def __init__(self, *, translators: Sequence[DaoSubscriberProtocol]):
|
|
55
|
-
self.topics_of_interest = [translator.event_topic for translator in translators]
|
|
56
|
-
|
|
57
|
-
if len(set(self.topics_of_interest)) != len(self.topics_of_interest):
|
|
58
|
-
raise ValueError(
|
|
59
|
-
"Got multiple DaoSubscriberProtocol-compliant translators trying to"
|
|
60
|
-
+ " consume from the same event topic."
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
self._translator_by_topic = {
|
|
64
|
-
translator.event_topic: translator for translator in translators
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async def _consume_validated(
|
|
68
|
-
self, *, payload: JsonObject, type_: Ascii, topic: Ascii, key: Ascii
|
|
69
|
-
) -> None:
|
|
70
|
-
"""
|
|
71
|
-
Receive and process an event with already validated topic, type, and key.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
payload: The data/payload to send with the event.
|
|
75
|
-
type_: The type of the event.
|
|
76
|
-
topic: Name of the topic the event was published to.
|
|
77
|
-
key: A key used for routing the event.
|
|
78
|
-
"""
|
|
79
|
-
translator = self._translator_by_topic.get(topic)
|
|
80
|
-
|
|
81
|
-
if translator is None:
|
|
82
|
-
# This should never happen, as the topic should have been filtered out:
|
|
83
|
-
raise RuntimeError
|
|
84
|
-
|
|
85
|
-
if type_ == CHANGE_EVENT_TYPE:
|
|
86
|
-
try:
|
|
87
|
-
dto = translator.dto_model.model_validate(payload)
|
|
88
|
-
except ValidationError as error:
|
|
89
|
-
message = (
|
|
90
|
-
f"The event of type {type_} on topic {topic}"
|
|
91
|
-
+ " was not valid wrt. the DTO model."
|
|
92
|
-
)
|
|
93
|
-
logging.error(message)
|
|
94
|
-
raise DtoValidationError(message) from error
|
|
95
|
-
|
|
96
|
-
await translator.changed(resource_id=key, update=dto)
|
|
97
|
-
|
|
98
|
-
else:
|
|
99
|
-
# a deletion event:
|
|
100
|
-
await translator.deleted(resource_id=key)
|
|
38
|
+
from hexkit.providers.akafka.provider.eventsub import (
|
|
39
|
+
ComboTranslator as TranslatorConverter, # for backwards compatibility
|
|
40
|
+
)
|
|
101
41
|
|
|
102
42
|
|
|
103
43
|
class KafkaOutboxSubscriber(InboundProviderBase):
|
|
104
44
|
"""Apache Kafka-specific provider using translators that implement the
|
|
105
45
|
`DaoSubscriberProtocol`.
|
|
46
|
+
|
|
47
|
+
Note: this class can be replaced by using the ComboTranslator in conjunction
|
|
48
|
+
with the normal KafkaEventSubscriber class. We may therefore deprecate this
|
|
49
|
+
class in the near future.
|
|
106
50
|
"""
|
|
107
51
|
|
|
108
52
|
@classmethod
|
|
@@ -25,16 +25,20 @@ import asyncio
|
|
|
25
25
|
import json
|
|
26
26
|
import logging
|
|
27
27
|
import ssl
|
|
28
|
+
from collections import defaultdict
|
|
29
|
+
from collections.abc import Sequence
|
|
28
30
|
from contextlib import asynccontextmanager
|
|
29
31
|
from dataclasses import asdict, dataclass
|
|
30
32
|
from datetime import datetime, timezone
|
|
31
33
|
from typing import Any, Callable, Literal, Optional, Protocol, TypeVar, Union, cast
|
|
32
34
|
|
|
33
35
|
from aiokafka import AIOKafkaConsumer
|
|
36
|
+
from pydantic import ValidationError
|
|
34
37
|
|
|
35
38
|
from hexkit.base import InboundProviderBase
|
|
36
39
|
from hexkit.correlation import set_correlation_id
|
|
37
40
|
from hexkit.custom_types import Ascii, JsonObject
|
|
41
|
+
from hexkit.protocols.daosub import DaoSubscriberProtocol, DtoValidationError
|
|
38
42
|
from hexkit.protocols.eventpub import EventPublisherProtocol
|
|
39
43
|
from hexkit.protocols.eventsub import DLQSubscriberProtocol, EventSubscriberProtocol
|
|
40
44
|
from hexkit.providers.akafka.config import KafkaConfig
|
|
@@ -43,6 +47,107 @@ from hexkit.providers.akafka.provider.utils import (
|
|
|
43
47
|
generate_ssl_context,
|
|
44
48
|
)
|
|
45
49
|
|
|
50
|
+
CHANGE_EVENT_TYPE = "upserted"
|
|
51
|
+
DELETE_EVENT_TYPE = "deleted"
|
|
52
|
+
EventOrDaoSubProtocol = Union[DaoSubscriberProtocol, EventSubscriberProtocol]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ComboTranslator(EventSubscriberProtocol):
|
|
56
|
+
"""Takes a list of translators implementing either the `DaoSubscriberProtocol`,
|
|
57
|
+
`EventSubscriberProtocol`, or both, and create a single translator
|
|
58
|
+
implementing the `EventSubscriberProtocol`. This basically bundles the
|
|
59
|
+
passed-in translators, with the resulting combo translator acting as a router
|
|
60
|
+
that directs the event information to the appropriate translator based on
|
|
61
|
+
its `topic` and `type`.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
topics_of_interest: list[str]
|
|
65
|
+
types_of_interest: list[str]
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
*,
|
|
70
|
+
translators: Sequence[EventOrDaoSubProtocol],
|
|
71
|
+
):
|
|
72
|
+
self.translators: dict[str, dict[str, EventOrDaoSubProtocol]] = defaultdict(
|
|
73
|
+
dict
|
|
74
|
+
)
|
|
75
|
+
self.topics_of_interest = []
|
|
76
|
+
self.types_of_interest = []
|
|
77
|
+
uses_outbox = False
|
|
78
|
+
|
|
79
|
+
outbox_topics: list[str] = []
|
|
80
|
+
non_outbox_topics: list[str] = []
|
|
81
|
+
for translator in translators:
|
|
82
|
+
if isinstance(translator, DaoSubscriberProtocol):
|
|
83
|
+
outbox_topics.append(translator.event_topic)
|
|
84
|
+
self.translators[translator.event_topic][CHANGE_EVENT_TYPE] = translator
|
|
85
|
+
self.translators[translator.event_topic][DELETE_EVENT_TYPE] = translator
|
|
86
|
+
uses_outbox = True
|
|
87
|
+
elif isinstance(translator, EventSubscriberProtocol):
|
|
88
|
+
# This looks like a looping no-no, but in reality is only a handful of
|
|
89
|
+
# iterations and it ensures we route the event to the right translator
|
|
90
|
+
self.types_of_interest.extend(translator.types_of_interest)
|
|
91
|
+
non_outbox_topics.extend(dict.fromkeys(translator.topics_of_interest))
|
|
92
|
+
for topic in translator.topics_of_interest:
|
|
93
|
+
for type_ in translator.types_of_interest:
|
|
94
|
+
if type_ in self.translators[topic]:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"Got multiple EventSubscriberProtocol-compliant"
|
|
97
|
+
+ " translators trying to consume from the same topic"
|
|
98
|
+
+ " and type."
|
|
99
|
+
)
|
|
100
|
+
self.translators[topic][type_] = translator
|
|
101
|
+
if uses_outbox:
|
|
102
|
+
self.types_of_interest.append(CHANGE_EVENT_TYPE)
|
|
103
|
+
self.types_of_interest.append(DELETE_EVENT_TYPE)
|
|
104
|
+
|
|
105
|
+
if len(set(outbox_topics)) != len(outbox_topics):
|
|
106
|
+
raise ValueError(
|
|
107
|
+
"Got multiple DaoSubscriberProtocol-compliant translators trying to"
|
|
108
|
+
+ " consume from the same event topic."
|
|
109
|
+
)
|
|
110
|
+
self.topics_of_interest.extend(outbox_topics)
|
|
111
|
+
self.topics_of_interest.extend(non_outbox_topics)
|
|
112
|
+
|
|
113
|
+
async def _consume_validated(
|
|
114
|
+
self, *, payload: JsonObject, type_: Ascii, topic: Ascii, key: Ascii
|
|
115
|
+
) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Receive and process an event with already validated topic, type, and key.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
payload: The data/payload to send with the event.
|
|
121
|
+
type_: The type of the event.
|
|
122
|
+
topic: Name of the topic the event was published to.
|
|
123
|
+
key: A key used for routing the event.
|
|
124
|
+
"""
|
|
125
|
+
translator = self.translators[topic][type_]
|
|
126
|
+
|
|
127
|
+
if translator is None:
|
|
128
|
+
# This should never happen, as the topic should have been filtered out:
|
|
129
|
+
raise RuntimeError
|
|
130
|
+
|
|
131
|
+
if isinstance(translator, DaoSubscriberProtocol):
|
|
132
|
+
if type_ == CHANGE_EVENT_TYPE:
|
|
133
|
+
try:
|
|
134
|
+
dto = translator.dto_model.model_validate(payload)
|
|
135
|
+
except ValidationError as error:
|
|
136
|
+
message = (
|
|
137
|
+
f"The event of type {type_} on topic {topic}"
|
|
138
|
+
+ " was not valid wrt. the DTO model."
|
|
139
|
+
)
|
|
140
|
+
logging.error(message)
|
|
141
|
+
raise DtoValidationError(message) from error
|
|
142
|
+
|
|
143
|
+
await translator.changed(resource_id=key, update=dto)
|
|
144
|
+
|
|
145
|
+
else:
|
|
146
|
+
# a deletion event:
|
|
147
|
+
await translator.deleted(resource_id=key)
|
|
148
|
+
else:
|
|
149
|
+
await translator.consume(payload=payload, type_=type_, topic=topic, key=key)
|
|
150
|
+
|
|
46
151
|
|
|
47
152
|
class HeaderNames:
|
|
48
153
|
"""Encapsulated constants for event header values"""
|
|
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
|
|
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
|