hexkit 4.0.0__tar.gz → 4.1.1__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 (56) hide show
  1. {hexkit-4.0.0/src/hexkit.egg-info → hexkit-4.1.1}/PKG-INFO +1 -1
  2. {hexkit-4.0.0 → hexkit-4.1.1}/pyproject.toml +1 -1
  3. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/log.py +5 -1
  4. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/akafka/__init__.py +2 -0
  5. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/akafka/provider/__init__.py +2 -0
  6. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/akafka/provider/daosub.py +10 -66
  7. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/akafka/provider/eventsub.py +105 -0
  8. {hexkit-4.0.0 → hexkit-4.1.1/src/hexkit.egg-info}/PKG-INFO +1 -1
  9. {hexkit-4.0.0 → hexkit-4.1.1}/LICENSE +0 -0
  10. {hexkit-4.0.0 → hexkit-4.1.1}/MANIFEST.in +0 -0
  11. {hexkit-4.0.0 → hexkit-4.1.1}/README.md +0 -0
  12. {hexkit-4.0.0 → hexkit-4.1.1}/setup.cfg +0 -0
  13. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/__init__.py +0 -0
  14. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/__main__.py +0 -0
  15. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/base.py +0 -0
  16. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/config.py +0 -0
  17. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/correlation.py +0 -0
  18. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/custom_types.py +0 -0
  19. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/protocols/__init__.py +0 -0
  20. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/protocols/dao.py +0 -0
  21. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/protocols/daopub.py +0 -0
  22. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/protocols/daosub.py +0 -0
  23. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/protocols/eventpub.py +0 -0
  24. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/protocols/eventsub.py +0 -0
  25. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/protocols/objstorage.py +0 -0
  26. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/__init__.py +0 -0
  27. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/akafka/config.py +0 -0
  28. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
  29. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/akafka/provider/utils.py +0 -0
  30. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/akafka/testcontainer.py +0 -0
  31. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/akafka/testutils.py +0 -0
  32. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/mongodb/__init__.py +0 -0
  33. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/mongodb/provider.py +0 -0
  34. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/mongodb/testutils.py +0 -0
  35. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/mongokafka/__init__.py +0 -0
  36. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/mongokafka/provider.py +0 -0
  37. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/mongokafka/testutils.py +0 -0
  38. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/s3/__init__.py +0 -0
  39. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/s3/provider.py +0 -0
  40. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  41. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  42. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  43. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  44. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  45. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
  46. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/s3/testutils/_fixtures.py +0 -0
  47. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
  48. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
  49. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/testing/__init__.py +0 -0
  50. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/providers/testing/eventpub.py +0 -0
  51. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/py.typed +0 -0
  52. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit/utils.py +0 -0
  53. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit.egg-info/SOURCES.txt +0 -0
  54. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit.egg-info/dependency_links.txt +0 -0
  55. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit.egg-info/requires.txt +0 -0
  56. {hexkit-4.0.0 → hexkit-4.1.1}/src/hexkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: hexkit
3
- Version: 4.0.0
3
+ Version: 4.1.1
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
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Intended Audience :: Developers",
24
24
  ]
25
25
  name = "hexkit"
26
- version = "4.0.0"
26
+ version = "4.1.1"
27
27
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
28
28
  dependencies = [
29
29
  "pydantic >=2, <3",
@@ -118,13 +118,17 @@ class JsonFormatter(Formatter):
118
118
  output["details"] = log_record["details"]
119
119
 
120
120
  if log_record["exc_info"]:
121
- exc_type, exc_value = log_record["exc_info"][:2]
121
+ exc_info = log_record["exc_info"]
122
+ exc_type, exc_value = exc_info[:2]
122
123
  exception = {
123
124
  "type": exc_type.__name__,
124
125
  "message": str(exc_value),
125
126
  }
126
127
  if self._include_traceback:
127
128
  exc_text = log_record["exc_text"]
129
+ if not exc_text:
130
+ # if there is not pre-formatted cached traceback, create it
131
+ exc_text = super().formatException(exc_info)
128
132
  if exc_text:
129
133
  exception["traceback"] = exc_text
130
134
  output["exception"] = exception
@@ -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.custom_types import Ascii, JsonObject
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
- CHANGE_EVENT_TYPE = "upserted"
44
- DELETE_EVENT_TYPE = "deleted"
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"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: hexkit
3
- Version: 4.0.0
3
+ Version: 4.1.1
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
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