hexkit 0.11.0__tar.gz → 1.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 (42) hide show
  1. hexkit-1.0.0/MANIFEST.in +1 -0
  2. {hexkit-0.11.0/src/hexkit.egg-info → hexkit-1.0.0}/PKG-INFO +1 -1
  3. {hexkit-0.11.0 → hexkit-1.0.0}/pyproject.toml +1 -1
  4. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/akafka/provider.py +49 -2
  5. hexkit-1.0.0/src/hexkit/providers/akafka/testcontainer.py +177 -0
  6. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/akafka/testutils.py +5 -4
  7. hexkit-1.0.0/src/hexkit/providers/s3/test_files/test_file1.yaml +3 -0
  8. hexkit-1.0.0/src/hexkit/providers/s3/test_files/test_file2.yaml +3 -0
  9. hexkit-1.0.0/src/hexkit/providers/s3/test_files/test_file3.yaml +3 -0
  10. hexkit-1.0.0/src/hexkit/providers/s3/test_files/test_file4.yaml +3 -0
  11. {hexkit-0.11.0 → hexkit-1.0.0/src/hexkit.egg-info}/PKG-INFO +1 -1
  12. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit.egg-info/SOURCES.txt +6 -0
  13. {hexkit-0.11.0 → hexkit-1.0.0}/LICENSE +0 -0
  14. {hexkit-0.11.0 → hexkit-1.0.0}/README.md +0 -0
  15. {hexkit-0.11.0 → hexkit-1.0.0}/setup.cfg +0 -0
  16. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/__init__.py +0 -0
  17. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/__main__.py +0 -0
  18. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/base.py +0 -0
  19. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/config.py +0 -0
  20. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/custom_types.py +0 -0
  21. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/inject.py +0 -0
  22. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/protocols/__init__.py +0 -0
  23. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/protocols/dao.py +0 -0
  24. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/protocols/eventpub.py +0 -0
  25. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/protocols/eventsub.py +0 -0
  26. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/protocols/objstorage.py +0 -0
  27. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/__init__.py +0 -0
  28. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/akafka/__init__.py +0 -0
  29. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
  30. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/mongodb/provider.py +0 -0
  31. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/mongodb/testutils.py +0 -0
  32. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/s3/__init__.py +0 -0
  33. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/s3/provider.py +0 -0
  34. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  35. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/s3/testutils.py +0 -0
  36. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/testing/__init__.py +0 -0
  37. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/testing/eventpub.py +0 -0
  38. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/providers/testing/utils.py +0 -0
  39. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit/utils.py +0 -0
  40. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  41. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit.egg-info/requires.txt +0 -0
  42. {hexkit-0.11.0 → hexkit-1.0.0}/src/hexkit.egg-info/top_level.txt +0 -0
@@ -0,0 +1 @@
1
+ include src/hexkit/providers/s3/test_files/*.yaml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hexkit
3
- Version: 0.11.0
3
+ Version: 1.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hexkit"
7
- version = "0.11.0"
7
+ version = "1.0.0"
8
8
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -23,10 +23,12 @@ Require dependencies of the `akafka` extra. See the `setup.cfg`.
23
23
 
24
24
  import json
25
25
  import logging
26
+ import ssl
26
27
  from contextlib import asynccontextmanager
27
- from typing import Any, Callable, Protocol, TypeVar
28
+ from typing import Any, Callable, Optional, Protocol, TypeVar
28
29
 
29
30
  from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
31
+ from aiokafka.helpers import create_ssl_context
30
32
  from pydantic import Field
31
33
  from pydantic_settings import BaseSettings
32
34
 
@@ -70,6 +72,29 @@ class KafkaConfig(BaseSettings):
70
72
  examples=[["localhost:9092"]],
71
73
  description="A list of connection strings to connect to Kafka bootstrap servers.",
72
74
  )
75
+ kafka_security_protocol: Literal["PLAINTEXT", "SSL"] = Field(
76
+ "PLAINTEXT",
77
+ description="Protocol used to communicate with brokers. "
78
+ + "Valid values are: PLAINTEXT, SSL.",
79
+ )
80
+ kafka_ssl_cafile: str = Field(
81
+ "",
82
+ description="Certificate Authority file path containing certificates"
83
+ + " used to sign broker certificates. If a CA not specified, the default"
84
+ + " system CA will be used if found by OpenSSL.",
85
+ )
86
+ kafka_ssl_certfile: str = Field(
87
+ "",
88
+ description="Optional filename of client certificate, as well as any"
89
+ + " CA certificates needed to establish the certificate's authenticity.",
90
+ )
91
+ kafka_ssl_keyfile: str = Field(
92
+ "", description="Optional filename containing the client private key."
93
+ )
94
+ kafka_ssl_password: str = Field(
95
+ "",
96
+ description="Optional password to be used for the client private key.",
97
+ )
73
98
 
74
99
 
75
100
  class EventTypeNotFoundError(RuntimeError):
@@ -84,13 +109,29 @@ def generate_client_id(*, service_name: str, instance_id: str) -> str:
84
109
  return f"{service_name}.{instance_id}"
85
110
 
86
111
 
112
+ def generate_ssl_context(config: KafkaConfig) -> Optional[ssl.SSLContext]:
113
+ """Generate SSL context for an encrypted SSL connection to Kafka broker."""
114
+ return (
115
+ create_ssl_context(
116
+ cafile=config.kafka_ssl_cafile,
117
+ certfile=config.kafka_ssl_certfile,
118
+ keyfile=config.kafka_ssl_keyfile,
119
+ password=config.kafka_ssl_password,
120
+ )
121
+ if config.kafka_security_protocol == "SSL"
122
+ else None
123
+ )
124
+
125
+
87
126
  class KafkaProducerCompatible(Protocol):
88
127
  """A python duck type protocol describing an AIOKafkaProducer or equivalent."""
89
128
 
90
- def __init__(
129
+ def __init__( # noqa: PLR0913
91
130
  self,
92
131
  *,
93
132
  bootstrap_servers: str,
133
+ security_protocol: str,
134
+ ssl_context: Optional[ssl.SSLContext],
94
135
  client_id: str,
95
136
  key_serializer: Callable[[Any], bytes],
96
137
  value_serializer: Callable[[Any], bytes],
@@ -149,6 +190,8 @@ class KafkaEventPublisher(EventPublisherProtocol):
149
190
 
150
191
  producer = kafka_producer_cls(
151
192
  bootstrap_servers=",".join(config.kafka_servers),
193
+ security_protocol=config.kafka_security_protocol,
194
+ ssl_context=generate_ssl_context(config),
152
195
  client_id=client_id,
153
196
  key_serializer=lambda key: key.encode("ascii"),
154
197
  value_serializer=lambda event_value: json.dumps(event_value).encode(
@@ -220,6 +263,8 @@ class KafkaConsumerCompatible(Protocol):
220
263
  self,
221
264
  *topics: Ascii,
222
265
  bootstrap_servers: str,
266
+ security_protocol: str,
267
+ ssl_context: Optional[ssl.SSLContext],
223
268
  client_id: str,
224
269
  group_id: str,
225
270
  auto_offset_reset: Literal["earliest"],
@@ -297,6 +342,8 @@ class KafkaEventSubscriber(InboundProviderBase):
297
342
  consumer = kafka_consumer_cls(
298
343
  *topics,
299
344
  bootstrap_servers=",".join(config.kafka_servers),
345
+ security_protocol=config.kafka_security_protocol,
346
+ ssl_context=generate_ssl_context(config),
300
347
  client_id=client_id,
301
348
  group_id=config.service_name,
302
349
  auto_offset_reset="earliest",
@@ -0,0 +1,177 @@
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
+ """Improved Kafka test containers."""
18
+
19
+ import tarfile
20
+ import time
21
+ from io import BytesIO
22
+ from ssl import SSLError
23
+ from textwrap import dedent
24
+ from typing import Literal, Optional
25
+
26
+ from kafka import KafkaConsumer
27
+ from kafka.errors import KafkaError, NoBrokersAvailable, UnrecognizedBrokerVersion
28
+ from testcontainers.core.container import DockerContainer
29
+ from testcontainers.core.waiting_utils import wait_container_is_ready
30
+
31
+ __all__ = ["KafkaSSLContainer"]
32
+
33
+ DEFAULT_IMAGE = "confluentinc/cp-kafka:7.5.1"
34
+
35
+ DEFAULT_PORT = 9093 # default port for the Kafka container
36
+ BROKER_PORT = 9092 # auxiliary port for inter broker listener
37
+
38
+
39
+ class KafkaSSLContainer(DockerContainer):
40
+ """Kafka container that supports SSL (or actually TLS)."""
41
+
42
+ TC_START_SCRIPT = "/tc-start.sh"
43
+ SECRETS_PATH = "/etc/kafka/secrets"
44
+
45
+ def __init__( # noqa: C901, PLR0912, PLR0913
46
+ self,
47
+ image: str = DEFAULT_IMAGE,
48
+ port: int = DEFAULT_PORT,
49
+ cert: Optional[str] = None,
50
+ key: Optional[str] = None,
51
+ trusted: Optional[str] = None,
52
+ password: Optional[str] = None,
53
+ client_auth: Optional[Literal["requested", "required", "none"]] = None,
54
+ **kwargs,
55
+ ) -> None:
56
+ """Initialize the Kafka SSL container with the given parameters.
57
+
58
+ "cert" must contain the certificate of the broker and if needed also
59
+ intermediate certificates. "key" must contain the private key of the
60
+ broker. If it password protected, "password" must be specified as well.
61
+ "trusted" must contain the trusted certificates. In "client_auth" you can
62
+ specify whether authentication is requested, required or not needed at all.
63
+ """
64
+ super().__init__(image, **kwargs)
65
+ env = self.with_env
66
+ self.port = port
67
+ ssl = bool(cert or trusted or client_auth)
68
+ protocol = "SSL" if ssl else "PLAINTEXT"
69
+ self.protocol = protocol
70
+ self.with_exposed_ports(port)
71
+ self.broker_port = DEFAULT_PORT if port == BROKER_PORT else BROKER_PORT
72
+ listeners = f"{protocol}://0.0.0.0:{port},BROKER://0.0.0.0:{self.broker_port}"
73
+ protocol_map = f"BROKER:PLAINTEXT,{protocol}:{protocol}"
74
+ env("KAFKA_LISTENERS", listeners)
75
+ env("KAFKA_INTER_BROKER_LISTENER_NAME", "BROKER")
76
+ env("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", protocol_map)
77
+ env("KAFKA_BROKER_ID", "1")
78
+ env("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", "1")
79
+ env("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", "1")
80
+ env("KAFKA_LOG_FLUSH_INTERVAL_MESSAGES", "10000000")
81
+ env("KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS", "0")
82
+ if ssl:
83
+ if cert:
84
+ cert = cert.strip().replace("\n", "\\n")
85
+ if key:
86
+ key = key.strip().replace("\n", "\\n")
87
+ if password:
88
+ password = password.strip().replace("\n", "\\n")
89
+ if trusted:
90
+ trusted = trusted.strip().replace("\n", "\\n")
91
+ if cert:
92
+ if not cert.startswith("-----BEGIN") or "CERTIFICATE" not in cert:
93
+ raise ValueError("Certificate chain must be in PEM format")
94
+ env("KAFKA_SSL_KEYSTORE_CERTIFICATE_CHAIN", cert)
95
+ if key:
96
+ if not key.startswith("-----BEGIN") or "PRIVATE KEY" not in key:
97
+ raise ValueError("Private key must be in PEM format")
98
+ env("KAFKA_SSL_KEYSTORE_KEY", key)
99
+ if cert or key:
100
+ env("KAFKA_SSL_KEYSTORE_TYPE", "PEM")
101
+ if key and password:
102
+ env("KAFKA_SSL_KEY_PASSWORD", password)
103
+ if trusted:
104
+ if not trusted.startswith("-----BEGIN") or "CERTIFICATE" not in trusted:
105
+ raise ValueError("Trusted certificates must be in PEM format")
106
+ env("KAFKA_SSL_TRUSTSTORE_CERTIFICATES", trusted)
107
+ env("KAFKA_SSL_TRUSTSTORE_TYPE", "PEM")
108
+ if client_auth:
109
+ env("KAFKA_SSL_CLIENT_AUTH", client_auth)
110
+ env("KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM", " ")
111
+
112
+ def get_bootstrap_server(self) -> str:
113
+ """Get the Kafka bootstrap server."""
114
+ host = self.get_container_host_ip()
115
+ port = self.get_exposed_port(self.port)
116
+ return f"{host}:{port}"
117
+
118
+ def start(self) -> "KafkaSSLContainer":
119
+ """Start the Docker container."""
120
+ script = self.TC_START_SCRIPT
121
+ command = f'sh -c "while [ ! -f {script} ]; do sleep 0.1; done; sh {script}"'
122
+ self.with_command(command)
123
+ super().start()
124
+ self.tc_start()
125
+ self._connect()
126
+ return self
127
+
128
+ @wait_container_is_ready(
129
+ UnrecognizedBrokerVersion, NoBrokersAvailable, KafkaError, ValueError
130
+ ) # pyright: ignore
131
+ def _connect(self) -> None:
132
+ bootstrap_server = self.get_bootstrap_server()
133
+ try:
134
+ consumer = KafkaConsumer(
135
+ group_id="test",
136
+ bootstrap_servers=[bootstrap_server],
137
+ security_protocol=self.protocol,
138
+ )
139
+ except SSLError:
140
+ pass # count this as connected
141
+ else:
142
+ if not consumer.bootstrap_connected():
143
+ raise KafkaError("Unable to connect with Kafka container!")
144
+
145
+ def tc_start(self) -> None:
146
+ """Start the test container."""
147
+ protocol = self.protocol
148
+ host = self.get_container_host_ip()
149
+ port = self.get_exposed_port(self.port)
150
+ listeners = f"{protocol}://{host}:{port},BROKER://127.0.0.1:{self.broker_port}"
151
+ script = f"""
152
+ #!/bin/bash
153
+ c=/etc/confluent/docker
154
+ . $c/bash-config
155
+ export KAFKA_ADVERTISED_LISTENERS={listeners}
156
+ export KAFKA_ZOOKEEPER_CONNECT=localhost:2181
157
+ p=zookeeper.properties
158
+ echo "clientPort=2181" > $p
159
+ echo "dataDir=/var/lib/zookeeper/data" >> $p
160
+ echo "dataLogDir=/var/lib/zookeeper/log" >> $p
161
+ zookeeper-server-start $p &
162
+ # workaround for https://github.com/confluentinc/kafka-images/issues/244
163
+ sed -i -E '/^if .*LISTENERS.*SSL:/,/^fi/d' $c/configure
164
+ $c/configure && $c/launch
165
+ """
166
+ self.create_file(dedent(script).strip().encode("utf-8"), self.TC_START_SCRIPT)
167
+
168
+ def create_file(self, content: bytes, path: str) -> None:
169
+ """Create a file inside the container."""
170
+ with BytesIO() as archive:
171
+ with tarfile.TarFile(fileobj=archive, mode="w") as tar:
172
+ tarinfo = tarfile.TarInfo(name=path)
173
+ tarinfo.size = len(content)
174
+ tarinfo.mtime = time.time() # type: ignore
175
+ tar.addfile(tarinfo, BytesIO(content))
176
+ archive.seek(0)
177
+ self.get_wrapped_container().put_archive("/", archive)
@@ -38,6 +38,7 @@ from hexkit.providers.akafka.provider import (
38
38
  KafkaEventPublisher,
39
39
  get_event_type,
40
40
  )
41
+ from hexkit.providers.akafka.testcontainer import DEFAULT_IMAGE as KAFKA_IMAGE
41
42
 
42
43
 
43
44
  @dataclass(frozen=True)
@@ -61,7 +62,7 @@ class ExpectedEvent(EventBase):
61
62
 
62
63
  @dataclass(frozen=True)
63
64
  class RecordedEvent(EventBase):
64
- """Used by the EventyRecorder class to describe events recorded in a specific topic."""
65
+ """Used by the EventRecorder class to describe events recorded in a specific topic."""
65
66
 
66
67
  key: Ascii
67
68
 
@@ -324,7 +325,7 @@ class EventRecorder:
324
325
  return self
325
326
 
326
327
  async def __aexit__(self, error_type, error_val, error_tb):
327
- """Stop recording and check the recorded events agains the expectation when
328
+ """Stop recording and check the recorded events against the expectation when
328
329
  exiting the context block.
329
330
  """
330
331
  await self.stop_recording()
@@ -403,9 +404,9 @@ async def kafka_fixture_function() -> AsyncGenerator[KafkaFixture, None]:
403
404
 
404
405
  **Do not call directly** Instead, use get_kafka_fixture()
405
406
  """
406
- with KafkaContainer(image="confluentinc/cp-kafka:5.4.9-1-deb8") as kafka:
407
+ with KafkaContainer(image=KAFKA_IMAGE) as kafka:
407
408
  kafka_servers = [kafka.get_bootstrap_server()]
408
- config = KafkaConfig(
409
+ config = KafkaConfig( # type: ignore
409
410
  service_name="test_publisher",
410
411
  service_instance_id="001",
411
412
  kafka_servers=kafka_servers,
@@ -0,0 +1,3 @@
1
+ test: |
2
+ A test file with some random content used for testing up-
3
+ and download functionality of the ObjectStorageS3 DAO implementation.
@@ -0,0 +1,3 @@
1
+ test: |
2
+ Another test file with some random content used for testing up-
3
+ and download functionality of the ObjectStorageS3 DAO implementation.
@@ -0,0 +1,3 @@
1
+ test: |
2
+ A third test file with some random content used for testing up-
3
+ and download functionality of the ObjectStorageS3 DAO implementation.
@@ -0,0 +1,3 @@
1
+ test: |
2
+ A fourth test file with some random content used for testing up-
3
+ and download functionality of the ObjectStorageS3 DAO implementation.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hexkit
3
- Version: 0.11.0
3
+ Version: 1.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
@@ -1,4 +1,5 @@
1
1
  LICENSE
2
+ MANIFEST.in
2
3
  README.md
3
4
  pyproject.toml
4
5
  src/hexkit/__init__.py
@@ -21,6 +22,7 @@ src/hexkit/protocols/objstorage.py
21
22
  src/hexkit/providers/__init__.py
22
23
  src/hexkit/providers/akafka/__init__.py
23
24
  src/hexkit/providers/akafka/provider.py
25
+ src/hexkit/providers/akafka/testcontainer.py
24
26
  src/hexkit/providers/akafka/testutils.py
25
27
  src/hexkit/providers/mongodb/__init__.py
26
28
  src/hexkit/providers/mongodb/provider.py
@@ -29,6 +31,10 @@ src/hexkit/providers/s3/__init__.py
29
31
  src/hexkit/providers/s3/provider.py
30
32
  src/hexkit/providers/s3/testutils.py
31
33
  src/hexkit/providers/s3/test_files/__init__.py
34
+ src/hexkit/providers/s3/test_files/test_file1.yaml
35
+ src/hexkit/providers/s3/test_files/test_file2.yaml
36
+ src/hexkit/providers/s3/test_files/test_file3.yaml
37
+ src/hexkit/providers/s3/test_files/test_file4.yaml
32
38
  src/hexkit/providers/testing/__init__.py
33
39
  src/hexkit/providers/testing/eventpub.py
34
40
  src/hexkit/providers/testing/utils.py
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