hexkit 5.4.1__tar.gz → 6.0.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-5.4.1/src/hexkit.egg-info → hexkit-6.0.0}/PKG-INFO +3 -3
- {hexkit-5.4.1 → hexkit-6.0.0}/README.md +1 -1
- {hexkit-5.4.1 → hexkit-6.0.0}/pyproject.toml +2 -2
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/correlation.py +33 -16
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/log.py +1 -3
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/eventpub.py +17 -2
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/eventsub.py +12 -1
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/config.py +2 -2
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/provider/eventpub.py +29 -15
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/provider/eventsub.py +105 -53
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/testutils.py +28 -10
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/migrations/_manager.py +8 -7
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/migrations/_utils.py +2 -2
- hexkit-6.0.0/src/hexkit/providers/mongodb/migrations/helpers.py +97 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/provider.py +110 -67
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/testutils.py +37 -18
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/provider/daopub.py +40 -52
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +33 -15
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/testing/eventpub.py +3 -1
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/utils.py +25 -0
- {hexkit-5.4.1 → hexkit-6.0.0/src/hexkit.egg-info}/PKG-INFO +3 -3
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit.egg-info/SOURCES.txt +1 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit.egg-info/requires.txt +1 -1
- {hexkit-5.4.1 → hexkit-6.0.0}/LICENSE +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/MANIFEST.in +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/setup.cfg +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/__main__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/base.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/config.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/custom_types.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/opentelemetry.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/dao.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/daopub.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/daosub.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/objstorage.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/migrations/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/provider/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/provider/config.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/provider.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/testutils/_fixtures.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/testing/__init__.py +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/py.typed +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit.egg-info/entry_points.txt +0 -0
- {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexkit
|
|
3
|
-
Version:
|
|
3
|
+
Version: 6.0.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
|
|
@@ -30,7 +30,7 @@ Provides-Extra: s3
|
|
|
30
30
|
Requires-Dist: boto3<2,>=1.37; extra == "s3"
|
|
31
31
|
Requires-Dist: botocore<2,>=1.37; extra == "s3"
|
|
32
32
|
Provides-Extra: mongodb
|
|
33
|
-
Requires-Dist:
|
|
33
|
+
Requires-Dist: pymongo<5,>=4.13; extra == "mongodb"
|
|
34
34
|
Provides-Extra: opentelemetry-base
|
|
35
35
|
Requires-Dist: opentelemetry-sdk<2,>=1.31.1; extra == "opentelemetry-base"
|
|
36
36
|
Requires-Dist: opentelemetry-exporter-otlp<2,>=1.31.1; extra == "opentelemetry-base"
|
|
@@ -177,7 +177,7 @@ Additionally, the following convenience command is available inside the devconta
|
|
|
177
177
|
(type it in the integrated terminal of VS Code):
|
|
178
178
|
- `dev_install` - install the lib with all development dependencies and pre-commit hooks
|
|
179
179
|
(please run that if you are starting the devcontainer for the first time
|
|
180
|
-
or if added any python dependencies to the [`./
|
|
180
|
+
or if added any python dependencies to the [`./pyproject.toml`](./pyproject.toml))
|
|
181
181
|
|
|
182
182
|
If you prefer not to use vscode, you could get a similar setup (without the editor
|
|
183
183
|
specific features) by running the following commands:
|
|
@@ -103,7 +103,7 @@ Additionally, the following convenience command is available inside the devconta
|
|
|
103
103
|
(type it in the integrated terminal of VS Code):
|
|
104
104
|
- `dev_install` - install the lib with all development dependencies and pre-commit hooks
|
|
105
105
|
(please run that if you are starting the devcontainer for the first time
|
|
106
|
-
or if added any python dependencies to the [`./
|
|
106
|
+
or if added any python dependencies to the [`./pyproject.toml`](./pyproject.toml))
|
|
107
107
|
|
|
108
108
|
If you prefer not to use vscode, you could get a similar setup (without the editor
|
|
109
109
|
specific features) by running the following commands:
|
|
@@ -24,7 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Intended Audience :: Developers",
|
|
25
25
|
]
|
|
26
26
|
name = "hexkit"
|
|
27
|
-
version = "
|
|
27
|
+
version = "6.0.0"
|
|
28
28
|
description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
|
|
29
29
|
dependencies = [
|
|
30
30
|
"opentelemetry-api >=1.31.1, <2",
|
|
@@ -46,7 +46,7 @@ s3 = [
|
|
|
46
46
|
"botocore >=1.37, <2",
|
|
47
47
|
]
|
|
48
48
|
mongodb = [
|
|
49
|
-
"
|
|
49
|
+
"pymongo >=4.13, <5",
|
|
50
50
|
]
|
|
51
51
|
opentelemetry-base = [
|
|
52
52
|
"opentelemetry-sdk >=1.31.1, <2",
|
|
@@ -17,19 +17,24 @@
|
|
|
17
17
|
"""Utilities related to correlation IDs"""
|
|
18
18
|
|
|
19
19
|
import logging
|
|
20
|
+
from collections.abc import AsyncGenerator
|
|
20
21
|
from contextlib import asynccontextmanager
|
|
21
22
|
from contextvars import ContextVar
|
|
23
|
+
from typing import Any
|
|
22
24
|
from uuid import UUID, uuid4
|
|
23
25
|
|
|
26
|
+
from pydantic import UUID4
|
|
27
|
+
|
|
24
28
|
from hexkit.utils import set_context_var
|
|
25
29
|
|
|
26
30
|
log = logging.getLogger(__name__)
|
|
27
31
|
|
|
28
|
-
correlation_id_var: ContextVar[
|
|
32
|
+
correlation_id_var: ContextVar[UUID4] = ContextVar("correlation_id")
|
|
29
33
|
|
|
30
34
|
__all__ = [
|
|
31
35
|
"CorrelationIdContextError",
|
|
32
36
|
"InvalidCorrelationIdError",
|
|
37
|
+
"correlation_id_from_str",
|
|
33
38
|
"get_correlation_id",
|
|
34
39
|
"new_correlation_id",
|
|
35
40
|
"set_correlation_id",
|
|
@@ -42,36 +47,46 @@ class CorrelationIdContextError(RuntimeError):
|
|
|
42
47
|
"""Raised when the correlation ID ContextVar is unexpectedly not set."""
|
|
43
48
|
|
|
44
49
|
def __init__(self):
|
|
45
|
-
super().__init__("
|
|
50
|
+
super().__init__("Correlation ID not set")
|
|
46
51
|
|
|
47
52
|
|
|
48
53
|
class InvalidCorrelationIdError(RuntimeError):
|
|
49
54
|
"""Raised when a correlation ID fails validation."""
|
|
50
55
|
|
|
51
|
-
def __init__(self, *, correlation_id:
|
|
52
|
-
message = f"Invalid correlation ID
|
|
56
|
+
def __init__(self, *, correlation_id: Any):
|
|
57
|
+
message = f"Invalid correlation ID: {correlation_id!r}"
|
|
53
58
|
super().__init__(message)
|
|
54
59
|
|
|
55
60
|
|
|
56
|
-
def new_correlation_id() ->
|
|
61
|
+
def new_correlation_id() -> UUID4:
|
|
57
62
|
"""Generates a new correlation ID."""
|
|
58
|
-
return
|
|
63
|
+
return uuid4()
|
|
59
64
|
|
|
60
65
|
|
|
61
|
-
def validate_correlation_id(correlation_id:
|
|
62
|
-
"""
|
|
66
|
+
def validate_correlation_id(correlation_id: Any):
|
|
67
|
+
"""Validate the correlation ID.
|
|
63
68
|
|
|
64
69
|
Raises:
|
|
65
|
-
InvalidCorrelationIdError: If the correlation ID is
|
|
70
|
+
InvalidCorrelationIdError: If the correlation ID is not a UUID.
|
|
71
|
+
"""
|
|
72
|
+
if not isinstance(correlation_id, UUID):
|
|
73
|
+
raise InvalidCorrelationIdError(correlation_id=correlation_id)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def correlation_id_from_str(correlation_id: str) -> UUID4:
|
|
77
|
+
"""Convert a string to a UUID4.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
InvalidCorrelationIdError: If the string is not a valid UUID.
|
|
66
81
|
"""
|
|
67
82
|
try:
|
|
68
|
-
UUID(correlation_id)
|
|
83
|
+
return UUID(correlation_id)
|
|
69
84
|
except ValueError as err:
|
|
70
85
|
raise InvalidCorrelationIdError(correlation_id=correlation_id) from err
|
|
71
86
|
|
|
72
87
|
|
|
73
88
|
@asynccontextmanager
|
|
74
|
-
async def set_correlation_id(correlation_id:
|
|
89
|
+
async def set_correlation_id(correlation_id: UUID4):
|
|
75
90
|
"""Set the given correlation ID for the life of the context.
|
|
76
91
|
|
|
77
92
|
Raises:
|
|
@@ -85,7 +100,7 @@ async def set_correlation_id(correlation_id: str):
|
|
|
85
100
|
|
|
86
101
|
|
|
87
102
|
@asynccontextmanager
|
|
88
|
-
async def set_new_correlation_id():
|
|
103
|
+
async def set_new_correlation_id() -> AsyncGenerator[UUID4, None]:
|
|
89
104
|
"""Set a new correlation ID for the life of the context."""
|
|
90
105
|
correlation_id = new_correlation_id()
|
|
91
106
|
|
|
@@ -94,17 +109,19 @@ async def set_new_correlation_id():
|
|
|
94
109
|
yield correlation_id
|
|
95
110
|
|
|
96
111
|
|
|
97
|
-
def get_correlation_id() ->
|
|
112
|
+
def get_correlation_id() -> UUID4:
|
|
98
113
|
"""Get the correlation ID.
|
|
99
114
|
|
|
100
115
|
This should only be called when the correlation ID ContextVar is expected to be set.
|
|
101
116
|
|
|
102
117
|
Raises:
|
|
103
118
|
CorrelationIdContextError: when the correlation ID ContextVar is not set.
|
|
104
|
-
InvalidCorrelationIdError: when the correlation ID is invalid.
|
|
119
|
+
InvalidCorrelationIdError: when the correlation ID is set but invalid.
|
|
105
120
|
"""
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
try:
|
|
122
|
+
correlation_id = correlation_id_var.get()
|
|
123
|
+
except LookupError as err:
|
|
124
|
+
raise CorrelationIdContextError() from err
|
|
108
125
|
|
|
109
126
|
validate_correlation_id(correlation_id)
|
|
110
127
|
|
|
@@ -24,9 +24,7 @@ from typing import Any, Literal, Optional
|
|
|
24
24
|
from pydantic import Field
|
|
25
25
|
from pydantic_settings import BaseSettings
|
|
26
26
|
|
|
27
|
-
from hexkit.correlation import
|
|
28
|
-
correlation_id_var,
|
|
29
|
-
)
|
|
27
|
+
from hexkit.correlation import correlation_id_var
|
|
30
28
|
|
|
31
29
|
__all__ = [
|
|
32
30
|
"JsonFormatter",
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
from abc import ABC, abstractmethod
|
|
20
20
|
from collections.abc import Mapping
|
|
21
21
|
from typing import Optional
|
|
22
|
+
from uuid import UUID, uuid4
|
|
23
|
+
|
|
24
|
+
from pydantic import UUID4
|
|
22
25
|
|
|
23
26
|
from hexkit.custom_types import Ascii, JsonObject
|
|
24
27
|
from hexkit.utils import check_ascii
|
|
@@ -27,13 +30,14 @@ from hexkit.utils import check_ascii
|
|
|
27
30
|
class EventPublisherProtocol(ABC):
|
|
28
31
|
"""A protocol for publishing events to an event broker."""
|
|
29
32
|
|
|
30
|
-
async def publish(
|
|
33
|
+
async def publish( # noqa: PLR0913
|
|
31
34
|
self,
|
|
32
35
|
*,
|
|
33
36
|
payload: JsonObject,
|
|
34
37
|
type_: Ascii,
|
|
35
38
|
key: Ascii,
|
|
36
39
|
topic: Ascii,
|
|
40
|
+
event_id: Optional[UUID4] = None,
|
|
37
41
|
headers: Optional[Mapping[str, str]] = None,
|
|
38
42
|
) -> None:
|
|
39
43
|
"""Publish an event.
|
|
@@ -43,8 +47,16 @@ class EventPublisherProtocol(ABC):
|
|
|
43
47
|
- `type_` (str): The event type. ASCII characters only.
|
|
44
48
|
- `key` (str): The event type. ASCII characters only.
|
|
45
49
|
- `topic` (str): The event type. ASCII characters only.
|
|
50
|
+
- `event_id` (UUID4, optional): An optional event ID. If not provided, a new
|
|
51
|
+
one will be generated.
|
|
46
52
|
- `headers`: Additional headers to attach to the event.
|
|
47
53
|
"""
|
|
54
|
+
if event_id:
|
|
55
|
+
if not isinstance(event_id, UUID):
|
|
56
|
+
raise TypeError(f"event_id must be a UUID, got {type(event_id)}")
|
|
57
|
+
else:
|
|
58
|
+
event_id = uuid4()
|
|
59
|
+
|
|
48
60
|
check_ascii(type_, key, topic)
|
|
49
61
|
if headers is None:
|
|
50
62
|
headers = {}
|
|
@@ -54,17 +66,19 @@ class EventPublisherProtocol(ABC):
|
|
|
54
66
|
type_=type_,
|
|
55
67
|
key=key,
|
|
56
68
|
topic=topic,
|
|
69
|
+
event_id=event_id,
|
|
57
70
|
headers=headers,
|
|
58
71
|
)
|
|
59
72
|
|
|
60
73
|
@abstractmethod
|
|
61
|
-
async def _publish_validated(
|
|
74
|
+
async def _publish_validated( # noqa: PLR0913
|
|
62
75
|
self,
|
|
63
76
|
*,
|
|
64
77
|
payload: JsonObject,
|
|
65
78
|
type_: Ascii,
|
|
66
79
|
key: Ascii,
|
|
67
80
|
topic: Ascii,
|
|
81
|
+
event_id: UUID4,
|
|
68
82
|
headers: Mapping[str, str],
|
|
69
83
|
) -> None:
|
|
70
84
|
"""Publish an event with already validated topic and type.
|
|
@@ -74,6 +88,7 @@ class EventPublisherProtocol(ABC):
|
|
|
74
88
|
- `type_` (str): The event type. ASCII characters only.
|
|
75
89
|
- `key` (str): The event type. ASCII characters only.
|
|
76
90
|
- `topic` (str): The event type. ASCII characters only.
|
|
91
|
+
- `event_id` (UUID): The event ID.
|
|
77
92
|
- `headers`: Additional headers to attach to the event.
|
|
78
93
|
"""
|
|
79
94
|
...
|
|
@@ -20,6 +20,8 @@ from abc import ABC, abstractmethod
|
|
|
20
20
|
from collections.abc import Mapping
|
|
21
21
|
from datetime import datetime
|
|
22
22
|
|
|
23
|
+
from pydantic import UUID4
|
|
24
|
+
|
|
23
25
|
from hexkit.custom_types import Ascii, JsonObject
|
|
24
26
|
from hexkit.utils import check_ascii
|
|
25
27
|
|
|
@@ -54,6 +56,7 @@ class EventSubscriberProtocol(ABC):
|
|
|
54
56
|
type_: Ascii,
|
|
55
57
|
topic: Ascii,
|
|
56
58
|
key: Ascii,
|
|
59
|
+
event_id: UUID4,
|
|
57
60
|
) -> None:
|
|
58
61
|
"""Receive an event of interest and process it according to its type.
|
|
59
62
|
|
|
@@ -62,6 +65,7 @@ class EventSubscriberProtocol(ABC):
|
|
|
62
65
|
type_: The type of the event.
|
|
63
66
|
topic: Name of the topic the event was published to.
|
|
64
67
|
key: A key used for routing the event.
|
|
68
|
+
event_id: The unique identifier of the event.
|
|
65
69
|
"""
|
|
66
70
|
check_ascii(type_, topic)
|
|
67
71
|
|
|
@@ -69,7 +73,7 @@ class EventSubscriberProtocol(ABC):
|
|
|
69
73
|
check_ascii(key)
|
|
70
74
|
|
|
71
75
|
await self._consume_validated(
|
|
72
|
-
payload=payload, type_=type_, topic=topic, key=key
|
|
76
|
+
payload=payload, type_=type_, topic=topic, key=key, event_id=event_id
|
|
73
77
|
)
|
|
74
78
|
|
|
75
79
|
@abstractmethod
|
|
@@ -80,6 +84,7 @@ class EventSubscriberProtocol(ABC):
|
|
|
80
84
|
type_: Ascii,
|
|
81
85
|
topic: Ascii,
|
|
82
86
|
key: Ascii,
|
|
87
|
+
event_id: UUID4,
|
|
83
88
|
) -> None:
|
|
84
89
|
"""
|
|
85
90
|
Receive and process an event with already validated topic, type, and key.
|
|
@@ -89,6 +94,7 @@ class EventSubscriberProtocol(ABC):
|
|
|
89
94
|
type_: The type of the event.
|
|
90
95
|
topic: Name of the topic the event was published to.
|
|
91
96
|
key: A key used for routing the event.
|
|
97
|
+
event_id: The unique identifier of the event.
|
|
92
98
|
"""
|
|
93
99
|
...
|
|
94
100
|
|
|
@@ -114,6 +120,7 @@ class DLQSubscriberProtocol(ABC):
|
|
|
114
120
|
type_: Ascii,
|
|
115
121
|
topic: Ascii,
|
|
116
122
|
key: Ascii,
|
|
123
|
+
event_id: UUID4,
|
|
117
124
|
timestamp: datetime,
|
|
118
125
|
headers: Mapping[str, str],
|
|
119
126
|
) -> None:
|
|
@@ -124,6 +131,7 @@ class DLQSubscriberProtocol(ABC):
|
|
|
124
131
|
type_: The type of the event.
|
|
125
132
|
topic: Name of the topic the event was published to.
|
|
126
133
|
key: A key used for routing the event.
|
|
134
|
+
event_id: The unique identifier of the event.
|
|
127
135
|
timestamp: The timestamp assigned by either the publisher or the broker.
|
|
128
136
|
headers: The event headers containing important metadata.
|
|
129
137
|
"""
|
|
@@ -137,6 +145,7 @@ class DLQSubscriberProtocol(ABC):
|
|
|
137
145
|
type_=type_,
|
|
138
146
|
topic=topic,
|
|
139
147
|
key=key,
|
|
148
|
+
event_id=event_id,
|
|
140
149
|
timestamp=timestamp,
|
|
141
150
|
headers=headers,
|
|
142
151
|
)
|
|
@@ -149,6 +158,7 @@ class DLQSubscriberProtocol(ABC):
|
|
|
149
158
|
type_: Ascii,
|
|
150
159
|
topic: Ascii,
|
|
151
160
|
key: Ascii,
|
|
161
|
+
event_id: UUID4,
|
|
152
162
|
timestamp: datetime,
|
|
153
163
|
headers: Mapping[str, str],
|
|
154
164
|
) -> None:
|
|
@@ -160,6 +170,7 @@ class DLQSubscriberProtocol(ABC):
|
|
|
160
170
|
type_: The type of the event.
|
|
161
171
|
topic: Name of the topic the event was published to.
|
|
162
172
|
key: A key used for routing the event.
|
|
173
|
+
event_id: The unique identifier of the event.
|
|
163
174
|
timestamp: The timestamp assigned by either the publisher or the broker.
|
|
164
175
|
headers: The event headers containing important metadata.
|
|
165
176
|
"""
|
|
@@ -74,8 +74,8 @@ class KafkaConfig(BaseSettings):
|
|
|
74
74
|
examples=[True, False],
|
|
75
75
|
description=(
|
|
76
76
|
"A flag, which, if False, will result in an error when trying to publish an"
|
|
77
|
-
+ " event without a valid correlation ID set for the context. If True,
|
|
78
|
-
+ "
|
|
77
|
+
+ " event without a valid correlation ID set for the context. If True, a"
|
|
78
|
+
+ " new correlation ID will be generated and used in the event header."
|
|
79
79
|
),
|
|
80
80
|
)
|
|
81
81
|
kafka_max_message_size: PositiveInt = Field(
|
|
@@ -30,12 +30,12 @@ from typing import Any, Callable, Optional, Protocol
|
|
|
30
30
|
from uuid import UUID
|
|
31
31
|
|
|
32
32
|
from aiokafka import AIOKafkaProducer
|
|
33
|
+
from pydantic import UUID4
|
|
33
34
|
|
|
34
35
|
from hexkit.correlation import (
|
|
35
36
|
CorrelationIdContextError,
|
|
36
37
|
get_correlation_id,
|
|
37
38
|
new_correlation_id,
|
|
38
|
-
validate_correlation_id,
|
|
39
39
|
)
|
|
40
40
|
from hexkit.custom_types import Ascii, JsonObject, KafkaCompressionType
|
|
41
41
|
from hexkit.protocols.eventpub import EventPublisherProtocol
|
|
@@ -45,7 +45,7 @@ from hexkit.providers.akafka.provider.utils import (
|
|
|
45
45
|
generate_ssl_context,
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
-
RESERVED_HEADERS = ["type", "correlation_id"]
|
|
48
|
+
RESERVED_HEADERS = ["type", "correlation_id", "event_id"]
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class KafkaProducerCompatible(Protocol):
|
|
@@ -171,13 +171,14 @@ class KafkaEventPublisher(EventPublisherProtocol):
|
|
|
171
171
|
return value.isoformat()
|
|
172
172
|
raise TypeError(f"Object of type {type(value)} is not JSON serializable")
|
|
173
173
|
|
|
174
|
-
async def _publish_validated(
|
|
174
|
+
async def _publish_validated( # noqa: PLR0913
|
|
175
175
|
self,
|
|
176
176
|
*,
|
|
177
177
|
payload: JsonObject,
|
|
178
178
|
type_: Ascii,
|
|
179
179
|
key: Ascii,
|
|
180
180
|
topic: Ascii,
|
|
181
|
+
event_id: UUID4,
|
|
181
182
|
headers: Mapping[str, str],
|
|
182
183
|
) -> None:
|
|
183
184
|
"""Publish an event with already validated topic and type.
|
|
@@ -187,6 +188,7 @@ class KafkaEventPublisher(EventPublisherProtocol):
|
|
|
187
188
|
- `type_` (str): The event type. ASCII characters only.
|
|
188
189
|
- `key` (str): The event key. ASCII characters only.
|
|
189
190
|
- `topic` (str): The event topic. ASCII characters only.
|
|
191
|
+
- `event_id` (UUID): The event ID.
|
|
190
192
|
- `headers`: Additional headers to attach to the event.
|
|
191
193
|
"""
|
|
192
194
|
try:
|
|
@@ -196,25 +198,37 @@ class KafkaEventPublisher(EventPublisherProtocol):
|
|
|
196
198
|
raise
|
|
197
199
|
|
|
198
200
|
correlation_id = new_correlation_id()
|
|
199
|
-
logging.info("Generated new correlation ID: %s", correlation_id)
|
|
200
|
-
|
|
201
|
-
validate_correlation_id(correlation_id)
|
|
201
|
+
logging.info("Generated new correlation ID: %s.", correlation_id)
|
|
202
202
|
|
|
203
203
|
# Create a shallow copy of the headers
|
|
204
|
-
headers_copy = dict(headers)
|
|
204
|
+
headers_copy = dict(headers) # used for validation and logging only
|
|
205
205
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if header in headers_copy:
|
|
212
|
-
logging.warning(log_msg, extra={header: headers_copy[header]})
|
|
206
|
+
overrides = {
|
|
207
|
+
header: headers_copy[header]
|
|
208
|
+
for header in RESERVED_HEADERS
|
|
209
|
+
if header in headers_copy
|
|
210
|
+
}
|
|
213
211
|
|
|
214
212
|
headers_copy["type"] = type_
|
|
215
|
-
headers_copy["
|
|
213
|
+
headers_copy["event_id"] = str(event_id)
|
|
214
|
+
headers_copy["correlation_id"] = str(correlation_id)
|
|
216
215
|
encoded_headers_list = [(k, v.encode("ascii")) for k, v in headers_copy.items()]
|
|
217
216
|
|
|
217
|
+
# Log warnings for any reserved headers that were overridden
|
|
218
|
+
for header, old_value in overrides.items():
|
|
219
|
+
log_msg = (
|
|
220
|
+
f"The '{header}' header shouldn't be supplied, but was."
|
|
221
|
+
f" Overwriting old value ({old_value}) with {headers_copy[header]}."
|
|
222
|
+
)
|
|
223
|
+
logging.warning(log_msg, extra={header: old_value})
|
|
224
|
+
|
|
218
225
|
await self._producer.send_and_wait(
|
|
219
226
|
topic, key=key, value=payload, headers=encoded_headers_list
|
|
220
227
|
)
|
|
228
|
+
logging.info(
|
|
229
|
+
"Published event: topic=%s, type=%s, key=%s, event_id=%s.",
|
|
230
|
+
topic,
|
|
231
|
+
type_,
|
|
232
|
+
key,
|
|
233
|
+
event_id,
|
|
234
|
+
)
|