hexkit 2.1.0__tar.gz → 2.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-2.1.0/src/hexkit.egg-info → hexkit-2.2.0}/PKG-INFO +4 -4
- {hexkit-2.1.0 → hexkit-2.2.0}/pyproject.toml +22 -20
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/protocols/dao.py +35 -21
- hexkit-2.2.0/src/hexkit/protocols/dao_outbox.py +130 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/akafka/provider.py +16 -4
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/akafka/testutils.py +7 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/mongodb/provider.py +123 -82
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/mongodb/testutils.py +1 -0
- hexkit-2.2.0/src/hexkit/providers/mongokafka/__init__.py +19 -0
- hexkit-2.2.0/src/hexkit/providers/mongokafka/provider.py +461 -0
- {hexkit-2.1.0 → hexkit-2.2.0/src/hexkit.egg-info}/PKG-INFO +4 -4
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit.egg-info/SOURCES.txt +3 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit.egg-info/requires.txt +3 -3
- {hexkit-2.1.0 → hexkit-2.2.0}/LICENSE +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/MANIFEST.in +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/README.md +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/setup.cfg +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/__init__.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/__main__.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/base.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/config.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/correlation.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/custom_types.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/log.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/protocols/__init__.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/protocols/eventpub.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/protocols/eventsub.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/protocols/objstorage.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/__init__.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/akafka/__init__.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/s3/__init__.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/s3/provider.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/s3/testutils.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/testing/__init__.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/providers/testing/eventpub.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit/utils.py +0 -0
- {hexkit-2.1.0 → hexkit-2.2.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-2.1.0 → hexkit-2.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: 2.
|
|
3
|
+
Version: 2.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
|
|
@@ -30,13 +30,13 @@ Provides-Extra: mongodb
|
|
|
30
30
|
Requires-Dist: motor<4,>=3.1.1; extra == "mongodb"
|
|
31
31
|
Provides-Extra: test-akafka
|
|
32
32
|
Requires-Dist: hexkit[akafka]; extra == "test-akafka"
|
|
33
|
-
Requires-Dist: testcontainers[kafka]<4
|
|
33
|
+
Requires-Dist: testcontainers[kafka]<5,>=4.3.3; extra == "test-akafka"
|
|
34
34
|
Provides-Extra: test-s3
|
|
35
35
|
Requires-Dist: hexkit[s3]; extra == "test-s3"
|
|
36
|
-
Requires-Dist: testcontainers<4
|
|
36
|
+
Requires-Dist: testcontainers<5,>=4.3.3; extra == "test-s3"
|
|
37
37
|
Provides-Extra: test-mongodb
|
|
38
38
|
Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
|
|
39
|
-
Requires-Dist: testcontainers[mongo]<4
|
|
39
|
+
Requires-Dist: testcontainers[mongo]<5,>=4.3.3; extra == "test-mongodb"
|
|
40
40
|
Provides-Extra: test
|
|
41
41
|
Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
|
|
42
42
|
Provides-Extra: all
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Intended Audience :: Developers",
|
|
23
23
|
]
|
|
24
24
|
name = "hexkit"
|
|
25
|
-
version = "2.
|
|
25
|
+
version = "2.2.0"
|
|
26
26
|
description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
|
|
27
27
|
dependencies = [
|
|
28
28
|
"pydantic >=2, <3",
|
|
@@ -47,15 +47,15 @@ mongodb = [
|
|
|
47
47
|
]
|
|
48
48
|
test-akafka = [
|
|
49
49
|
"hexkit[akafka]",
|
|
50
|
-
"testcontainers[kafka] >=3.
|
|
50
|
+
"testcontainers[kafka] >=4.3.3, <5",
|
|
51
51
|
]
|
|
52
52
|
test-s3 = [
|
|
53
53
|
"hexkit[s3]",
|
|
54
|
-
"testcontainers >=3.
|
|
54
|
+
"testcontainers >=4.3.3, <5",
|
|
55
55
|
]
|
|
56
56
|
test-mongodb = [
|
|
57
57
|
"hexkit[mongodb]",
|
|
58
|
-
"testcontainers[mongo] >=3.
|
|
58
|
+
"testcontainers[mongo] >=4.3.3, <5",
|
|
59
59
|
]
|
|
60
60
|
test = [
|
|
61
61
|
"hexkit[test-akafka,test-s3,test-mongodb]",
|
|
@@ -80,6 +80,21 @@ exclude = [
|
|
|
80
80
|
"build",
|
|
81
81
|
"dist",
|
|
82
82
|
]
|
|
83
|
+
line-length = 88
|
|
84
|
+
src = [
|
|
85
|
+
"src",
|
|
86
|
+
"tests",
|
|
87
|
+
"examples",
|
|
88
|
+
"scripts",
|
|
89
|
+
]
|
|
90
|
+
target-version = "py39"
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint]
|
|
93
|
+
fixable = [
|
|
94
|
+
"UP",
|
|
95
|
+
"I",
|
|
96
|
+
"D",
|
|
97
|
+
]
|
|
83
98
|
ignore = [
|
|
84
99
|
"E",
|
|
85
100
|
"W",
|
|
@@ -97,7 +112,6 @@ ignore = [
|
|
|
97
112
|
"D206",
|
|
98
113
|
"D300",
|
|
99
114
|
]
|
|
100
|
-
line-length = 88
|
|
101
115
|
select = [
|
|
102
116
|
"C90",
|
|
103
117
|
"F",
|
|
@@ -111,23 +125,11 @@ select = [
|
|
|
111
125
|
"SIM",
|
|
112
126
|
"D",
|
|
113
127
|
]
|
|
114
|
-
fixable = [
|
|
115
|
-
"UP",
|
|
116
|
-
"I",
|
|
117
|
-
"D",
|
|
118
|
-
]
|
|
119
|
-
src = [
|
|
120
|
-
"src",
|
|
121
|
-
"tests",
|
|
122
|
-
"examples",
|
|
123
|
-
"scripts",
|
|
124
|
-
]
|
|
125
|
-
target-version = "py39"
|
|
126
128
|
|
|
127
|
-
[tool.ruff.mccabe]
|
|
129
|
+
[tool.ruff.lint.mccabe]
|
|
128
130
|
max-complexity = 10
|
|
129
131
|
|
|
130
|
-
[tool.ruff.per-file-ignores]
|
|
132
|
+
[tool.ruff.lint.per-file-ignores]
|
|
131
133
|
"scripts/*" = [
|
|
132
134
|
"PL",
|
|
133
135
|
"S",
|
|
@@ -153,7 +155,7 @@ max-complexity = 10
|
|
|
153
155
|
"D",
|
|
154
156
|
]
|
|
155
157
|
|
|
156
|
-
[tool.ruff.pydocstyle]
|
|
158
|
+
[tool.ruff.lint.pydocstyle]
|
|
157
159
|
convention = "pep257"
|
|
158
160
|
|
|
159
161
|
[tool.mypy]
|
|
@@ -268,10 +268,8 @@ async def uuid4_id_generator() -> AsyncGenerator[str, None]:
|
|
|
268
268
|
yield str(uuid4())
|
|
269
269
|
|
|
270
270
|
|
|
271
|
-
class
|
|
272
|
-
"""A
|
|
273
|
-
that are enclosed in transactional scopes.
|
|
274
|
-
"""
|
|
271
|
+
class DaoFactoryBase:
|
|
272
|
+
"""A base for Data Access Objects (DAO) Factory protocols."""
|
|
275
273
|
|
|
276
274
|
class IdFieldNotFoundError(ValueError):
|
|
277
275
|
"""Raised when the dto_model did not contain the expected id_field."""
|
|
@@ -335,6 +333,32 @@ class DaoFactoryProtocol(ABC):
|
|
|
335
333
|
f"Provided index fields are invalid: {error}"
|
|
336
334
|
) from error
|
|
337
335
|
|
|
336
|
+
@classmethod
|
|
337
|
+
def _validate(
|
|
338
|
+
cls,
|
|
339
|
+
*,
|
|
340
|
+
dto_model: type[Dto],
|
|
341
|
+
id_field: str,
|
|
342
|
+
dto_creation_model: Optional[type[DtoCreation]],
|
|
343
|
+
fields_to_index: Optional[Collection[str]],
|
|
344
|
+
) -> None:
|
|
345
|
+
"""Validates the input parameters of the get_dao method."""
|
|
346
|
+
cls._validate_dto_model_id(dto_model=dto_model, id_field=id_field)
|
|
347
|
+
cls._validate_dto_creation_model(
|
|
348
|
+
dto_model=dto_model,
|
|
349
|
+
dto_creation_model=dto_creation_model,
|
|
350
|
+
id_field=id_field,
|
|
351
|
+
)
|
|
352
|
+
cls._validate_fields_to_index(
|
|
353
|
+
dto_model=dto_model, fields_to_index=fields_to_index
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class DaoFactoryProtocol(DaoFactoryBase, ABC):
|
|
358
|
+
"""A protocol describing a factory to produce Data Access Objects (DAO) objects
|
|
359
|
+
that are enclosed in transactional scopes.
|
|
360
|
+
"""
|
|
361
|
+
|
|
338
362
|
@overload
|
|
339
363
|
async def get_dao(
|
|
340
364
|
self,
|
|
@@ -344,8 +368,7 @@ class DaoFactoryProtocol(ABC):
|
|
|
344
368
|
id_field: str,
|
|
345
369
|
fields_to_index: Optional[Collection[str]] = None,
|
|
346
370
|
id_generator: Optional[AsyncGenerator[str, None]] = None,
|
|
347
|
-
) -> DaoNaturalId[Dto]:
|
|
348
|
-
...
|
|
371
|
+
) -> DaoNaturalId[Dto]: ...
|
|
349
372
|
|
|
350
373
|
@overload
|
|
351
374
|
async def get_dao(
|
|
@@ -357,8 +380,7 @@ class DaoFactoryProtocol(ABC):
|
|
|
357
380
|
dto_creation_model: type[DtoCreation],
|
|
358
381
|
fields_to_index: Optional[Collection[str]] = None,
|
|
359
382
|
id_generator: Optional[AsyncGenerator[str, None]] = None,
|
|
360
|
-
) -> DaoSurrogateId[Dto, DtoCreation]:
|
|
361
|
-
...
|
|
383
|
+
) -> DaoSurrogateId[Dto, DtoCreation]: ...
|
|
362
384
|
|
|
363
385
|
async def get_dao(
|
|
364
386
|
self,
|
|
@@ -408,16 +430,11 @@ class DaoFactoryProtocol(ABC):
|
|
|
408
430
|
self.IdFieldNotFoundError:
|
|
409
431
|
Raised when the dto_model did not contain the expected id_field.
|
|
410
432
|
"""
|
|
411
|
-
self.
|
|
412
|
-
|
|
413
|
-
self._validate_dto_creation_model(
|
|
433
|
+
self._validate(
|
|
414
434
|
dto_model=dto_model,
|
|
415
|
-
dto_creation_model=dto_creation_model,
|
|
416
435
|
id_field=id_field,
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
self._validate_fields_to_index(
|
|
420
|
-
dto_model=dto_model, fields_to_index=fields_to_index
|
|
436
|
+
dto_creation_model=dto_creation_model,
|
|
437
|
+
fields_to_index=fields_to_index,
|
|
421
438
|
)
|
|
422
439
|
|
|
423
440
|
if id_generator is None:
|
|
@@ -430,7 +447,6 @@ class DaoFactoryProtocol(ABC):
|
|
|
430
447
|
id_field=id_field,
|
|
431
448
|
fields_to_index=fields_to_index,
|
|
432
449
|
dto_creation_model=dto_creation_model,
|
|
433
|
-
# (above behavior by mypy seems incorrect)
|
|
434
450
|
id_generator=id_generator,
|
|
435
451
|
)
|
|
436
452
|
|
|
@@ -445,8 +461,7 @@ class DaoFactoryProtocol(ABC):
|
|
|
445
461
|
dto_creation_model: None,
|
|
446
462
|
fields_to_index: Optional[Collection[str]],
|
|
447
463
|
id_generator: AsyncGenerator[str, None],
|
|
448
|
-
) -> DaoNaturalId[Dto]:
|
|
449
|
-
...
|
|
464
|
+
) -> DaoNaturalId[Dto]: ...
|
|
450
465
|
|
|
451
466
|
@overload
|
|
452
467
|
@abstractmethod
|
|
@@ -459,8 +474,7 @@ class DaoFactoryProtocol(ABC):
|
|
|
459
474
|
dto_creation_model: type[DtoCreation],
|
|
460
475
|
fields_to_index: Optional[Collection[str]],
|
|
461
476
|
id_generator: AsyncGenerator[str, None],
|
|
462
|
-
) -> DaoSurrogateId[Dto, DtoCreation]:
|
|
463
|
-
...
|
|
477
|
+
) -> DaoSurrogateId[Dto, DtoCreation]: ...
|
|
464
478
|
|
|
465
479
|
@abstractmethod
|
|
466
480
|
async def _get_dao(
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
|
|
2
|
+
# for the German Human Genome-Phenome Archive (GHGA)
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
"""Protocol for creating Data Access Objects to perform CRUD (plus find) interactions
|
|
18
|
+
with the database plus automatically publish the changes as events using a modification
|
|
19
|
+
of the outbox pattern.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# ruff: noqa: PLR0913
|
|
23
|
+
|
|
24
|
+
import typing
|
|
25
|
+
from abc import ABC, abstractmethod
|
|
26
|
+
from collections.abc import Collection
|
|
27
|
+
from typing import Callable, Optional
|
|
28
|
+
|
|
29
|
+
from hexkit.custom_types import JsonObject
|
|
30
|
+
from hexkit.protocols.dao import (
|
|
31
|
+
DaoFactoryBase,
|
|
32
|
+
DaoNaturalId,
|
|
33
|
+
Dto,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DaoOutbox(DaoNaturalId[Dto], typing.Protocol[Dto]):
|
|
38
|
+
"""A Data Access Object (DAO) that automatically publishes changes according to the
|
|
39
|
+
outbox pattern.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
async def publish_pending(self) -> None:
|
|
43
|
+
"""Publishes all non-published changes."""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
async def republish(self) -> None:
|
|
47
|
+
"""Republishes the state of all resources independent of whether they have
|
|
48
|
+
already been published or not.
|
|
49
|
+
"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DaoOutboxFactoryProtocol(DaoFactoryBase, ABC):
|
|
54
|
+
"""A protocol describing a factory to produce Data Access Objects (DAO) objects
|
|
55
|
+
which automatically publish changes according to the outbox pattern.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
async def get_dao(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
name: str,
|
|
62
|
+
dto_model: type[Dto],
|
|
63
|
+
id_field: str,
|
|
64
|
+
fields_to_index: Optional[Collection[str]] = None,
|
|
65
|
+
dto_to_event: Callable[[Dto], JsonObject],
|
|
66
|
+
event_topic: str,
|
|
67
|
+
autopublish: bool = True,
|
|
68
|
+
) -> DaoOutbox[Dto]:
|
|
69
|
+
"""Constructs an Outbox DAO for interacting with resources in a database.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name:
|
|
73
|
+
The name of the resource type (roughly equivalent to the name of a
|
|
74
|
+
database table or collection).
|
|
75
|
+
dto_model:
|
|
76
|
+
A DTO (Data Transfer Object) model describing the shape of resources.
|
|
77
|
+
id_field:
|
|
78
|
+
The name of the field of the `dto_model` that serves as resource ID.
|
|
79
|
+
(DAO implementation might use this field as primary key.)
|
|
80
|
+
fields_to_index:
|
|
81
|
+
Optionally, provide any fields that should be indexed in addition to the
|
|
82
|
+
`id_field`. Defaults to None.
|
|
83
|
+
dto_to_event:
|
|
84
|
+
A function that takes a DTO and returns the payload for an event.
|
|
85
|
+
event_topic:
|
|
86
|
+
The topic to which events should be published.
|
|
87
|
+
autopublish:
|
|
88
|
+
Whether to automatically publish changes. Defaults to True.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
A DAO of type DaoOutbox, which requires ID specification upon resource
|
|
92
|
+
creation.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
self.IdFieldNotFoundError:
|
|
96
|
+
Raised when the dto_model did not contain the expected id_field.
|
|
97
|
+
"""
|
|
98
|
+
self._validate(
|
|
99
|
+
dto_model=dto_model,
|
|
100
|
+
id_field=id_field,
|
|
101
|
+
dto_creation_model=None,
|
|
102
|
+
fields_to_index=fields_to_index,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return await self._get_dao(
|
|
106
|
+
name=name,
|
|
107
|
+
dto_model=dto_model,
|
|
108
|
+
id_field=id_field,
|
|
109
|
+
fields_to_index=fields_to_index,
|
|
110
|
+
dto_to_event=dto_to_event,
|
|
111
|
+
event_topic=event_topic,
|
|
112
|
+
autopublish=autopublish,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
async def _get_dao(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
name: str,
|
|
120
|
+
dto_model: type[Dto],
|
|
121
|
+
id_field: str,
|
|
122
|
+
fields_to_index: Optional[Collection[str]],
|
|
123
|
+
dto_to_event: Callable[[Dto], JsonObject],
|
|
124
|
+
event_topic: str,
|
|
125
|
+
autopublish: bool,
|
|
126
|
+
) -> DaoOutbox[Dto]:
|
|
127
|
+
"""*To be implemented by the provider. Input validation is done outside of this
|
|
128
|
+
method.*
|
|
129
|
+
"""
|
|
130
|
+
...
|
|
@@ -29,7 +29,7 @@ from typing import Any, Callable, Literal, Optional, Protocol, TypeVar
|
|
|
29
29
|
|
|
30
30
|
from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
|
|
31
31
|
from aiokafka.helpers import create_ssl_context
|
|
32
|
-
from pydantic import Field
|
|
32
|
+
from pydantic import Field, SecretStr
|
|
33
33
|
from pydantic_settings import BaseSettings
|
|
34
34
|
|
|
35
35
|
from hexkit.base import InboundProviderBase
|
|
@@ -93,7 +93,7 @@ class KafkaConfig(BaseSettings):
|
|
|
93
93
|
kafka_ssl_keyfile: str = Field(
|
|
94
94
|
default="", description="Optional filename containing the client private key."
|
|
95
95
|
)
|
|
96
|
-
kafka_ssl_password:
|
|
96
|
+
kafka_ssl_password: SecretStr = Field(
|
|
97
97
|
default="",
|
|
98
98
|
description="Optional password to be used for the client private key.",
|
|
99
99
|
)
|
|
@@ -131,7 +131,7 @@ def generate_ssl_context(config: KafkaConfig) -> Optional[ssl.SSLContext]:
|
|
|
131
131
|
cafile=config.kafka_ssl_cafile,
|
|
132
132
|
certfile=config.kafka_ssl_certfile,
|
|
133
133
|
keyfile=config.kafka_ssl_keyfile,
|
|
134
|
-
password=config.kafka_ssl_password,
|
|
134
|
+
password=config.kafka_ssl_password.get_secret_value(),
|
|
135
135
|
)
|
|
136
136
|
if config.kafka_security_protocol == "SSL"
|
|
137
137
|
else None
|
|
@@ -259,7 +259,7 @@ class KafkaEventPublisher(EventPublisherProtocol):
|
|
|
259
259
|
raise
|
|
260
260
|
|
|
261
261
|
correlation_id = new_correlation_id()
|
|
262
|
-
logging.info("Generated new correlation
|
|
262
|
+
logging.info("Generated new correlation ID: %s", correlation_id)
|
|
263
263
|
|
|
264
264
|
validate_correlation_id(correlation_id)
|
|
265
265
|
|
|
@@ -311,6 +311,7 @@ class KafkaConsumerCompatible(Protocol):
|
|
|
311
311
|
client_id: str,
|
|
312
312
|
group_id: str,
|
|
313
313
|
auto_offset_reset: Literal["earliest"],
|
|
314
|
+
enable_auto_commit: bool,
|
|
314
315
|
key_deserializer: Callable[[bytes], str],
|
|
315
316
|
value_deserializer: Callable[[bytes], str],
|
|
316
317
|
):
|
|
@@ -334,6 +335,10 @@ class KafkaConsumerCompatible(Protocol):
|
|
|
334
335
|
"""
|
|
335
336
|
...
|
|
336
337
|
|
|
338
|
+
async def commit(self, offsets=None):
|
|
339
|
+
"""Commit offsets to Kafka Broker."""
|
|
340
|
+
...
|
|
341
|
+
|
|
337
342
|
async def start(self) -> None:
|
|
338
343
|
"""Setup the consumer."""
|
|
339
344
|
...
|
|
@@ -390,6 +395,7 @@ class KafkaEventSubscriber(InboundProviderBase):
|
|
|
390
395
|
client_id=client_id,
|
|
391
396
|
group_id=config.service_name,
|
|
392
397
|
auto_offset_reset="earliest",
|
|
398
|
+
enable_auto_commit=False,
|
|
393
399
|
key_deserializer=lambda event_key: event_key.decode("ascii"),
|
|
394
400
|
value_deserializer=lambda event_value: json.loads(
|
|
395
401
|
event_value.decode("ascii")
|
|
@@ -437,6 +443,8 @@ class KafkaEventSubscriber(InboundProviderBase):
|
|
|
437
443
|
)
|
|
438
444
|
except EventHeaderNotFoundError as err:
|
|
439
445
|
logging.warning("Ignored an event: %s. %s", event_label, err.args[0])
|
|
446
|
+
# acknowledge event receipt
|
|
447
|
+
await self._consumer.commit()
|
|
440
448
|
return
|
|
441
449
|
|
|
442
450
|
if type_ in self._types_whitelist:
|
|
@@ -450,6 +458,8 @@ class KafkaEventSubscriber(InboundProviderBase):
|
|
|
450
458
|
type_=type_,
|
|
451
459
|
topic=event.topic,
|
|
452
460
|
)
|
|
461
|
+
# acknowledge successfully processed event
|
|
462
|
+
await self._consumer.commit()
|
|
453
463
|
except Exception:
|
|
454
464
|
logging.error(
|
|
455
465
|
"A fatal error occurred while processing the event: %s",
|
|
@@ -459,6 +469,8 @@ class KafkaEventSubscriber(InboundProviderBase):
|
|
|
459
469
|
|
|
460
470
|
else:
|
|
461
471
|
logging.info("Ignored event of type %s: %s", type_, event_label)
|
|
472
|
+
# acknowledge event receipt
|
|
473
|
+
await self._consumer.commit()
|
|
462
474
|
|
|
463
475
|
async def run(self, forever: bool = True) -> None:
|
|
464
476
|
"""
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
Please note, only use for testing purposes.
|
|
20
20
|
"""
|
|
21
|
+
|
|
21
22
|
import json
|
|
22
23
|
import warnings
|
|
23
24
|
from collections.abc import AsyncGenerator, Sequence
|
|
@@ -301,6 +302,12 @@ class EventRecorder:
|
|
|
301
302
|
consumer = self._get_consumer()
|
|
302
303
|
await consumer.start()
|
|
303
304
|
|
|
305
|
+
# The number of events to consume is determined by the difference between the
|
|
306
|
+
# starting and final offsets. To avoid waiting for too many events (and thus
|
|
307
|
+
# waiting forever), seek_to_end so only events published inside the context of
|
|
308
|
+
# the event recorder are considered.
|
|
309
|
+
await consumer.seek_to_end()
|
|
310
|
+
|
|
304
311
|
try:
|
|
305
312
|
self._starting_offsets = await self._get_consumer_offsets(consumer=consumer)
|
|
306
313
|
finally:
|