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.
Files changed (65) hide show
  1. {hexkit-5.4.1/src/hexkit.egg-info → hexkit-6.0.0}/PKG-INFO +3 -3
  2. {hexkit-5.4.1 → hexkit-6.0.0}/README.md +1 -1
  3. {hexkit-5.4.1 → hexkit-6.0.0}/pyproject.toml +2 -2
  4. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/correlation.py +33 -16
  5. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/log.py +1 -3
  6. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/eventpub.py +17 -2
  7. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/eventsub.py +12 -1
  8. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/config.py +2 -2
  9. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/provider/eventpub.py +29 -15
  10. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/provider/eventsub.py +105 -53
  11. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/testutils.py +28 -10
  12. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/migrations/_manager.py +8 -7
  13. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/migrations/_utils.py +2 -2
  14. hexkit-6.0.0/src/hexkit/providers/mongodb/migrations/helpers.py +97 -0
  15. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/provider.py +110 -67
  16. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/testutils.py +37 -18
  17. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/provider/daopub.py +40 -52
  18. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +33 -15
  19. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/testing/eventpub.py +3 -1
  20. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/utils.py +25 -0
  21. {hexkit-5.4.1 → hexkit-6.0.0/src/hexkit.egg-info}/PKG-INFO +3 -3
  22. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit.egg-info/SOURCES.txt +1 -0
  23. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit.egg-info/requires.txt +1 -1
  24. {hexkit-5.4.1 → hexkit-6.0.0}/LICENSE +0 -0
  25. {hexkit-5.4.1 → hexkit-6.0.0}/MANIFEST.in +0 -0
  26. {hexkit-5.4.1 → hexkit-6.0.0}/setup.cfg +0 -0
  27. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/__init__.py +0 -0
  28. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/__main__.py +0 -0
  29. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/base.py +0 -0
  30. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/config.py +0 -0
  31. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/custom_types.py +0 -0
  32. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/opentelemetry.py +0 -0
  33. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/__init__.py +0 -0
  34. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/dao.py +0 -0
  35. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/daopub.py +0 -0
  36. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/daosub.py +0 -0
  37. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/protocols/objstorage.py +0 -0
  38. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/__init__.py +0 -0
  39. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/__init__.py +0 -0
  40. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
  41. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
  42. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
  43. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
  44. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
  45. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongodb/migrations/__init__.py +0 -0
  46. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
  47. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/provider/__init__.py +0 -0
  48. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/provider/config.py +0 -0
  49. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
  50. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/__init__.py +0 -0
  51. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/provider.py +0 -0
  52. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  53. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  54. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  55. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  56. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  57. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
  58. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/testutils/_fixtures.py +0 -0
  59. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
  60. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
  61. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/providers/testing/__init__.py +0 -0
  62. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit/py.typed +0 -0
  63. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  64. {hexkit-5.4.1 → hexkit-6.0.0}/src/hexkit.egg-info/entry_points.txt +0 -0
  65. {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: 5.4.1
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: motor<4,>=3.7; extra == "mongodb"
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 [`./setup.cfg`](./setup.cfg))
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 [`./setup.cfg`](./setup.cfg))
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 = "5.4.1"
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
- "motor >=3.7, <4",
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[str] = ContextVar("correlation_id", default="")
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__("No correlation ID has been set.")
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: str):
52
- message = f"Invalid correlation ID found: '{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() -> str:
61
+ def new_correlation_id() -> UUID4:
57
62
  """Generates a new correlation ID."""
58
- return str(uuid4())
63
+ return uuid4()
59
64
 
60
65
 
61
- def validate_correlation_id(correlation_id: str):
62
- """Raises an error if the correlation ID is invalid.
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 empty or invalid.
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: str):
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() -> str:
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
- if not (correlation_id := correlation_id_var.get()):
107
- raise CorrelationIdContextError()
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, the a"
78
- + " newly correlation ID will be generated and used in the event header."
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
- # Check and log warnings for reserved headers
207
- for header in RESERVED_HEADERS:
208
- log_msg = (
209
- f"The '{header}' header shouldn't be supplied, but was. Overwriting."
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["correlation_id"] = correlation_id
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
+ )