hgraph-kafka 0.8.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 (36) hide show
  1. hgraph_kafka-0.8.0/.gitignore +7 -0
  2. hgraph_kafka-0.8.0/CHANGELOG.md +6 -0
  3. hgraph_kafka-0.8.0/CMakeLists.txt +204 -0
  4. hgraph_kafka-0.8.0/LICENSE +21 -0
  5. hgraph_kafka-0.8.0/PKG-INFO +169 -0
  6. hgraph_kafka-0.8.0/README.md +155 -0
  7. hgraph_kafka-0.8.0/cmake/hgraph-kafkaConfig.cmake.in +15 -0
  8. hgraph_kafka-0.8.0/include/hgraph/kafka/export.h +16 -0
  9. hgraph_kafka-0.8.0/include/hgraph/kafka/service.h +59 -0
  10. hgraph_kafka-0.8.0/include/hgraph/kafka/testing/fake_broker.h +67 -0
  11. hgraph_kafka-0.8.0/include/hgraph/kafka/testing/mock_cluster.h +51 -0
  12. hgraph_kafka-0.8.0/include/hgraph/kafka/types.h +434 -0
  13. hgraph_kafka-0.8.0/include/hgraph/kafka/value_builders.h +191 -0
  14. hgraph_kafka-0.8.0/pyproject.toml +48 -0
  15. hgraph_kafka-0.8.0/python/hgraph/adaptors/kafka/__init__.py +12 -0
  16. hgraph_kafka-0.8.0/python/hgraph/adaptors/kafka/_api.py +18 -0
  17. hgraph_kafka-0.8.0/python/hgraph/adaptors/kafka/_impl.py +5 -0
  18. hgraph_kafka-0.8.0/python/hgraph_kafka/__init__.py +505 -0
  19. hgraph_kafka-0.8.0/python/hgraph_kafka/compat.py +525 -0
  20. hgraph_kafka-0.8.0/python/hgraph_kafka/testing.py +5 -0
  21. hgraph_kafka-0.8.0/python/tests/test_api.py +157 -0
  22. hgraph_kafka-0.8.0/python/tests/test_compat.py +191 -0
  23. hgraph_kafka-0.8.0/python/tests/test_packaging.py +139 -0
  24. hgraph_kafka-0.8.0/python/tests/test_runtime.py +524 -0
  25. hgraph_kafka-0.8.0/src/detail/service_bridge.h +618 -0
  26. hgraph_kafka-0.8.0/src/librdkafka_service.cpp +2634 -0
  27. hgraph_kafka-0.8.0/src/python_module.cpp +220 -0
  28. hgraph_kafka-0.8.0/src/service.cpp +33 -0
  29. hgraph_kafka-0.8.0/src/testing/fake_broker.cpp +513 -0
  30. hgraph_kafka-0.8.0/src/testing/mock_cluster.cpp +214 -0
  31. hgraph_kafka-0.8.0/src/value_builders.cpp +676 -0
  32. hgraph_kafka-0.8.0/test_package/CMakeLists.txt +13 -0
  33. hgraph_kafka-0.8.0/test_package/main.cpp +44 -0
  34. hgraph_kafka-0.8.0/tests/CMakeLists.txt +17 -0
  35. hgraph_kafka-0.8.0/tests/test_service.cpp +2228 -0
  36. hgraph_kafka-0.8.0/tools/audit_distribution.py +170 -0
@@ -0,0 +1,7 @@
1
+ /build/
2
+ /build-*/
3
+ /cmake-build-*/
4
+ /dist/
5
+ /.venv/
6
+ __pycache__/
7
+ *.pyc
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 0.8.0
4
+
5
+ - Introduce the C++-first Kafka service implementation, native and Python APIs,
6
+ and compatibility ownership of `hgraph.adaptors.kafka`.
@@ -0,0 +1,204 @@
1
+ cmake_minimum_required(VERSION 3.25)
2
+
3
+ project(hgraph_kafka VERSION 0.8.0 LANGUAGES C CXX)
4
+
5
+ include(GNUInstallDirs)
6
+ include(CMakePackageConfigHelpers)
7
+ include(CTest)
8
+ include(FetchContent)
9
+
10
+ option(HGRAPH_KAFKA_FETCH_LIBRDKAFKA
11
+ "Fetch the pinned librdkafka release when a CMake package is unavailable"
12
+ ON)
13
+ option(HGRAPH_KAFKA_FORCE_FETCH_LIBRDKAFKA
14
+ "Use the pinned librdkafka release even when a system package is available"
15
+ OFF)
16
+ option(HGRAPH_KAFKA_BUILD_PYTHON "Build the optional Python authoring bridge" OFF)
17
+
18
+ # Load only the limited-API Python targets before the installed hgraph SDK.
19
+ # Linking an embedding target such as Python::Python would pin this ABI3
20
+ # extension to the interpreter used for the build.
21
+ if(HGRAPH_KAFKA_BUILD_PYTHON)
22
+ find_package(Python 3.12 COMPONENTS
23
+ Interpreter Development.Module Development.SABIModule REQUIRED)
24
+ endif()
25
+
26
+ if(NOT TARGET hgraph::core)
27
+ find_package(hgraph CONFIG REQUIRED)
28
+ endif()
29
+
30
+ if(HGRAPH_KAFKA_BUILD_PYTHON)
31
+ get_target_property(_hgraph_kafka_core_links hgraph::options INTERFACE_LINK_LIBRARIES)
32
+ if("Python::Python" IN_LIST _hgraph_kafka_core_links)
33
+ message(FATAL_ERROR
34
+ "The selected hgraph SDK embeds a specific Python interpreter and "
35
+ "cannot produce an ABI3 hgraph-kafka wheel; use the SDK installed "
36
+ "by an hgraph stable-ABI wheel")
37
+ endif()
38
+ endif()
39
+
40
+ if(NOT HGRAPH_KAFKA_FORCE_FETCH_LIBRDKAFKA)
41
+ find_package(RdKafka 2.15 CONFIG QUIET)
42
+ endif()
43
+ if(NOT TARGET RdKafka::rdkafka AND NOT TARGET rdkafka)
44
+ if(NOT HGRAPH_KAFKA_FETCH_LIBRDKAFKA)
45
+ message(FATAL_ERROR
46
+ "librdkafka >= 2.15 was not found; install RdKafka or enable "
47
+ "HGRAPH_KAFKA_FETCH_LIBRDKAFKA")
48
+ endif()
49
+
50
+ set(RDKAFKA_BUILD_STATIC ON CACHE BOOL "" FORCE)
51
+ set(RDKAFKA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
52
+ set(RDKAFKA_BUILD_TESTS OFF CACHE BOOL "" FORCE)
53
+
54
+ # A Python extension statically embeds librdkafka. Keep OpenSSL inside the
55
+ # extension on wheel platforms where a package-local dynamic dependency
56
+ # would otherwise require loader-path repair. In particular, Python 3.8+
57
+ # does not search the Windows process PATH for dependent DLLs.
58
+ if(HGRAPH_KAFKA_BUILD_PYTHON AND (APPLE OR WIN32))
59
+ set(OPENSSL_USE_STATIC_LIBS TRUE)
60
+ endif()
61
+
62
+ # On macOS, prefer the static codec library as well so the resulting wheel
63
+ # does not retain an absolute Homebrew dylib path. System libraries remain
64
+ # normal platform dependencies.
65
+ if(APPLE AND HGRAPH_KAFKA_BUILD_PYTHON)
66
+ set(ENABLE_LZ4_EXT OFF CACHE BOOL "" FORCE)
67
+ find_library(HGRAPH_KAFKA_ZSTD_STATIC_LIBRARY NAMES libzstd.a)
68
+ if(NOT HGRAPH_KAFKA_ZSTD_STATIC_LIBRARY)
69
+ message(FATAL_ERROR
70
+ "A static zstd library is required to build a portable macOS wheel")
71
+ endif()
72
+ set(ZSTD_LIBRARY_DEBUG "${HGRAPH_KAFKA_ZSTD_STATIC_LIBRARY}"
73
+ CACHE FILEPATH "" FORCE)
74
+ set(ZSTD_LIBRARY_RELEASE "${HGRAPH_KAFKA_ZSTD_STATIC_LIBRARY}"
75
+ CACHE FILEPATH "" FORCE)
76
+ endif()
77
+
78
+ FetchContent_Declare(librdkafka
79
+ GIT_REPOSITORY https://github.com/confluentinc/librdkafka.git
80
+ GIT_TAG v2.15.0
81
+ GIT_SHALLOW TRUE
82
+ )
83
+ FetchContent_MakeAvailable(librdkafka)
84
+
85
+ if(MSVC AND OPENSSL_USE_STATIC_LIBS)
86
+ # librdkafka's Windows TLS source embeds default-library directives for
87
+ # the OpenSSL import libraries. Ignore those names when CMake selected
88
+ # the explicitly linked *_static.lib archives for a self-contained
89
+ # consumer; otherwise MSVC still searches for the absent import libs.
90
+ target_link_options(rdkafka INTERFACE
91
+ /NODEFAULTLIB:libcrypto.lib
92
+ /NODEFAULTLIB:libssl.lib
93
+ )
94
+ endif()
95
+ endif()
96
+
97
+ if(TARGET RdKafka::rdkafka)
98
+ set(HGRAPH_KAFKA_RDKAFKA_TARGET RdKafka::rdkafka)
99
+ elseif(TARGET rdkafka)
100
+ add_library(RdKafka::rdkafka ALIAS rdkafka)
101
+ set(HGRAPH_KAFKA_RDKAFKA_TARGET RdKafka::rdkafka)
102
+ else()
103
+ message(FATAL_ERROR "librdkafka did not provide its C target")
104
+ endif()
105
+
106
+ add_library(hgraph_kafka
107
+ src/librdkafka_service.cpp
108
+ src/service.cpp
109
+ src/value_builders.cpp
110
+ src/testing/fake_broker.cpp
111
+ src/testing/mock_cluster.cpp
112
+ )
113
+ add_library(hgraph::kafka ALIAS hgraph_kafka)
114
+
115
+ get_target_property(HGRAPH_KAFKA_LIBRARY_TYPE hgraph_kafka TYPE)
116
+ if(HGRAPH_KAFKA_LIBRARY_TYPE STREQUAL "STATIC_LIBRARY")
117
+ target_compile_definitions(hgraph_kafka PUBLIC HGRAPH_KAFKA_STATIC_DEFINE)
118
+ endif()
119
+
120
+ target_compile_features(hgraph_kafka PUBLIC cxx_std_23)
121
+ target_link_libraries(hgraph_kafka
122
+ PUBLIC hgraph::core
123
+ PRIVATE ${HGRAPH_KAFKA_RDKAFKA_TARGET}
124
+ )
125
+ target_include_directories(hgraph_kafka
126
+ PUBLIC
127
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
128
+ $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
129
+ PRIVATE
130
+ ${CMAKE_CURRENT_SOURCE_DIR}/src
131
+ )
132
+ set_target_properties(hgraph_kafka PROPERTIES
133
+ EXPORT_NAME kafka
134
+ POSITION_INDEPENDENT_CODE ON
135
+ )
136
+
137
+ if(MSVC)
138
+ # librdkafka includes Windows headers in the implementation. Keep their
139
+ # min/max macros from rewriting standard-library calls in this target.
140
+ target_compile_definitions(hgraph_kafka PRIVATE NOMINMAX)
141
+ target_compile_options(hgraph_kafka PRIVATE /W4 /permissive-)
142
+ else()
143
+ target_compile_options(hgraph_kafka PRIVATE -Wall -Wextra -Wpedantic)
144
+ endif()
145
+
146
+ if(BUILD_TESTING)
147
+ add_subdirectory(tests)
148
+ endif()
149
+
150
+ if(HGRAPH_KAFKA_BUILD_PYTHON)
151
+ find_package(Python 3.12 COMPONENTS Interpreter Development.Module Development.SABIModule REQUIRED)
152
+ if(NOT COMMAND hgraph_add_python_module OR NOT TARGET hgraph::nanobind)
153
+ message(FATAL_ERROR
154
+ "HGRAPH_KAFKA_BUILD_PYTHON requires a Python-enabled installed hgraph SDK")
155
+ endif()
156
+ hgraph_add_python_module(_hgraph_kafka STABLE_ABI NOMINSIZE src/python_module.cpp)
157
+ target_link_libraries(_hgraph_kafka PRIVATE hgraph::kafka)
158
+ if(APPLE)
159
+ set_target_properties(_hgraph_kafka PROPERTIES
160
+ INSTALL_RPATH "@loader_path/../${CMAKE_INSTALL_LIBDIR}")
161
+ elseif(UNIX)
162
+ set_target_properties(_hgraph_kafka PROPERTIES
163
+ INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}")
164
+ endif()
165
+ install(TARGETS _hgraph_kafka
166
+ COMPONENT Python
167
+ LIBRARY DESTINATION hgraph_kafka
168
+ RUNTIME DESTINATION hgraph_kafka
169
+ )
170
+ endif()
171
+
172
+ install(TARGETS hgraph_kafka
173
+ EXPORT hgraphKafkaTargets
174
+ COMPONENT Development
175
+ ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
176
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
177
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
178
+ )
179
+ install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
180
+ COMPONENT Development FILES_MATCHING PATTERN "*.h")
181
+
182
+ write_basic_package_version_file(
183
+ "${PROJECT_BINARY_DIR}/hgraph-kafkaConfigVersion.cmake"
184
+ VERSION ${PROJECT_VERSION}
185
+ COMPATIBILITY SameMajorVersion
186
+ )
187
+ configure_package_config_file(
188
+ "${CMAKE_CURRENT_SOURCE_DIR}/cmake/hgraph-kafkaConfig.cmake.in"
189
+ "${PROJECT_BINARY_DIR}/hgraph-kafkaConfig.cmake"
190
+ INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hgraph-kafka
191
+ )
192
+
193
+ install(EXPORT hgraphKafkaTargets
194
+ FILE hgraphKafkaTargets.cmake
195
+ NAMESPACE hgraph::
196
+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hgraph-kafka
197
+ COMPONENT Development
198
+ )
199
+ install(FILES
200
+ "${PROJECT_BINARY_DIR}/hgraph-kafkaConfig.cmake"
201
+ "${PROJECT_BINARY_DIR}/hgraph-kafkaConfigVersion.cmake"
202
+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hgraph-kafka
203
+ COMPONENT Development
204
+ )
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Howard Henson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.2
2
+ Name: hgraph-kafka
3
+ Version: 0.8.0
4
+ Summary: C++-first Kafka services for hgraph
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/hhenson/hgraph
7
+ Project-URL: Repository, https://github.com/hhenson/hgraph.git
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: hgraph>=0.8.0
10
+ Requires-Dist: frozendict>=2
11
+ Provides-Extra: test
12
+ Requires-Dist: pytest>=8; extra == "test"
13
+ Description-Content-Type: text/markdown
14
+
15
+ # hgraph-kafka
16
+
17
+ C++-first Kafka services for hgraph, implementing the contract in hgraph RFC
18
+ 0015. The extension uses librdkafka's C API and exposes the same service model
19
+ to native C++ and Python graphs.
20
+
21
+ One path-bound, multi-interface `service_impl` owns the Kafka clients for a
22
+ configuration. Subscriptions, publish requests, explicit commits, and events
23
+ all bind to that service instance. Graph output reaches Kafka through the
24
+ service's sink inputs; records, delivery reports, and events re-enter the root
25
+ graph through bounded push sources. Kafka clients and worker threads are
26
+ created on graph start and stopped with the graph.
27
+
28
+ The public record and configuration shapes are hgraph compound scalars. Kafka
29
+ headers preserve order, duplicates, null values, and empty byte strings.
30
+
31
+ ## Native C++
32
+
33
+ The installed package exports `hgraph::kafka`:
34
+
35
+ ```cpp
36
+ #include <hgraph/kafka/service.h>
37
+ #include <hgraph/kafka/value_builders.h>
38
+ #include <hgraph/lib/std/operators/conversion.h>
39
+ #include <hgraph/lib/std/operators/registration.h>
40
+
41
+ using namespace hgraph;
42
+ using namespace hgraph::kafka;
43
+
44
+ struct KafkaGraph {
45
+ static constexpr auto name = "kafka_graph";
46
+
47
+ static void compose(Wiring &w) {
48
+ const auto path = service::path("primary");
49
+ register_service(
50
+ w, path,
51
+ service_config().bootstrap_servers({Str{"localhost:9092"}}).build());
52
+
53
+ auto key = wire<stdlib::const_, TS<KafkaSubscriptionKey>>(
54
+ w, subscription_key()
55
+ .topics({Str{"orders"}})
56
+ .group_id(Str{"orders-worker"})
57
+ .build());
58
+ auto subscription = subscribe(w, path, key);
59
+
60
+ auto record = wire<stdlib::const_, TS<KafkaProduceRecord>>(
61
+ w, make_produce_record(Bytes{"ready"}));
62
+ auto delivery = publish(
63
+ w, path, publish_request(w, Str{"status"}, record));
64
+
65
+ auto cursor = wire<stdlib::getattr_, TS<KafkaCursor>>(
66
+ w, subscription, Str{"cursor"});
67
+ commit(w, path, cursor);
68
+ auto event = events(w, path);
69
+ }
70
+ };
71
+ ```
72
+
73
+ `KafkaSubscriptionOutput` provides the record and its matching next-offset
74
+ cursor on the same graph tick, plus subscription state. A cursor is accepted
75
+ only while its subscription identity, assignment generation, and partition
76
+ remain live. Commits are monotonic per assigned partition.
77
+
78
+ ## Python
79
+
80
+ The Python authoring surface lowers to the same native service:
81
+
82
+ ```python
83
+ import hgraph as hg
84
+ import hgraph_kafka as kafka
85
+
86
+ @hg.graph
87
+ def app():
88
+ kafka.register_kafka_service(
89
+ kafka.KafkaServiceConfig.from_bootstrap_servers(
90
+ ["localhost:9092"], client_id="orders-worker"
91
+ ),
92
+ path="primary",
93
+ )
94
+ key = kafka.KafkaSubscriptionKey(
95
+ topics=("orders",),
96
+ group_id="orders-worker",
97
+ start_position=kafka.KafkaStartPosition.committed(),
98
+ )
99
+ subscription = kafka.kafka_subscribe(
100
+ hg.const(key, tp=hg.TS[kafka.KafkaSubscriptionKey]),
101
+ path="primary",
102
+ )
103
+ kafka.kafka_commit(subscription["cursor"], path="primary")
104
+ ```
105
+
106
+ The extension wheel also owns the released import path
107
+ `hgraph.adaptors.kafka`. Existing `message_publisher`, `message_subscriber`,
108
+ `KafkaMessage`, and `register_kafka_adaptor` imports therefore continue to
109
+ work without making the core `hgraph` package depend on this extension.
110
+
111
+ ## Recovery and simulation
112
+
113
+ Subscriptions support explicit topic, pattern, or partition selection; group
114
+ or independent assignment; earliest, latest, committed, timestamp, explicit,
115
+ and graph-start positions; snapshot, timestamp, and explicit stop boundaries;
116
+ key filters; deterministic timestamp/topic/partition/offset replay; and
117
+ explicit or graph-delivery commits.
118
+
119
+ Simulation is intentionally limited to bounded, record-time recovery. The
120
+ consumer preloads the finite replay and schedules records at deterministic
121
+ graph times. Publish, commit, unbounded asynchronous input, and
122
+ `OnGraphDelivery` commit mode are rejected in simulation rather than silently
123
+ changing their semantics.
124
+
125
+ ## Build and test
126
+
127
+ This is a first-party extension in the hgraph monorepo. It remains a separate
128
+ CMake package and Python distribution: the top-level core package does not
129
+ link librdkafka or install these modules.
130
+
131
+ For an in-tree native development build from the repository root:
132
+
133
+ ```sh
134
+ cmake -S . -B build-kafka \
135
+ -DHGRAPH_BUILD_KAFKA_EXTENSION=ON \
136
+ -DBUILD_TESTING=ON
137
+ cmake --build build-kafka --parallel
138
+ ctest --test-dir build-kafka --output-on-failure
139
+ ```
140
+
141
+ The extension can still be configured independently against an installed
142
+ hgraph SDK:
143
+
144
+ ```sh
145
+ cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/hgraph/install
146
+ cmake --build build --parallel
147
+ ctest --test-dir build --output-on-failure
148
+ ```
149
+
150
+ Build its separately deployable ABI3 wheel from the repository root after
151
+ making the matching hgraph SDK discoverable through `CMAKE_PREFIX_PATH`:
152
+
153
+ ```sh
154
+ CMAKE_PREFIX_PATH=/path/to/hgraph/sdk \
155
+ uv build --wheel --package hgraph-kafka --python 3.12
156
+ ```
157
+
158
+ The deterministic suite uses librdkafka's mock cluster and the extension fake
159
+ transport. To include a real broker round trip, provide a clean topic:
160
+
161
+ ```sh
162
+ HGRAPH_KAFKA_INTEGRATION_BOOTSTRAP=localhost:9092 \
163
+ HGRAPH_KAFKA_INTEGRATION_TOPIC=hgraph-kafka-integration \
164
+ ctest --test-dir build --output-on-failure
165
+ ```
166
+
167
+ Wheel builds require the SDK installed by a stable-ABI hgraph wheel. The
168
+ extension rejects an SDK that links `Python::Python`, because that would pin
169
+ the nominal ABI3 module to the build interpreter.
@@ -0,0 +1,155 @@
1
+ # hgraph-kafka
2
+
3
+ C++-first Kafka services for hgraph, implementing the contract in hgraph RFC
4
+ 0015. The extension uses librdkafka's C API and exposes the same service model
5
+ to native C++ and Python graphs.
6
+
7
+ One path-bound, multi-interface `service_impl` owns the Kafka clients for a
8
+ configuration. Subscriptions, publish requests, explicit commits, and events
9
+ all bind to that service instance. Graph output reaches Kafka through the
10
+ service's sink inputs; records, delivery reports, and events re-enter the root
11
+ graph through bounded push sources. Kafka clients and worker threads are
12
+ created on graph start and stopped with the graph.
13
+
14
+ The public record and configuration shapes are hgraph compound scalars. Kafka
15
+ headers preserve order, duplicates, null values, and empty byte strings.
16
+
17
+ ## Native C++
18
+
19
+ The installed package exports `hgraph::kafka`:
20
+
21
+ ```cpp
22
+ #include <hgraph/kafka/service.h>
23
+ #include <hgraph/kafka/value_builders.h>
24
+ #include <hgraph/lib/std/operators/conversion.h>
25
+ #include <hgraph/lib/std/operators/registration.h>
26
+
27
+ using namespace hgraph;
28
+ using namespace hgraph::kafka;
29
+
30
+ struct KafkaGraph {
31
+ static constexpr auto name = "kafka_graph";
32
+
33
+ static void compose(Wiring &w) {
34
+ const auto path = service::path("primary");
35
+ register_service(
36
+ w, path,
37
+ service_config().bootstrap_servers({Str{"localhost:9092"}}).build());
38
+
39
+ auto key = wire<stdlib::const_, TS<KafkaSubscriptionKey>>(
40
+ w, subscription_key()
41
+ .topics({Str{"orders"}})
42
+ .group_id(Str{"orders-worker"})
43
+ .build());
44
+ auto subscription = subscribe(w, path, key);
45
+
46
+ auto record = wire<stdlib::const_, TS<KafkaProduceRecord>>(
47
+ w, make_produce_record(Bytes{"ready"}));
48
+ auto delivery = publish(
49
+ w, path, publish_request(w, Str{"status"}, record));
50
+
51
+ auto cursor = wire<stdlib::getattr_, TS<KafkaCursor>>(
52
+ w, subscription, Str{"cursor"});
53
+ commit(w, path, cursor);
54
+ auto event = events(w, path);
55
+ }
56
+ };
57
+ ```
58
+
59
+ `KafkaSubscriptionOutput` provides the record and its matching next-offset
60
+ cursor on the same graph tick, plus subscription state. A cursor is accepted
61
+ only while its subscription identity, assignment generation, and partition
62
+ remain live. Commits are monotonic per assigned partition.
63
+
64
+ ## Python
65
+
66
+ The Python authoring surface lowers to the same native service:
67
+
68
+ ```python
69
+ import hgraph as hg
70
+ import hgraph_kafka as kafka
71
+
72
+ @hg.graph
73
+ def app():
74
+ kafka.register_kafka_service(
75
+ kafka.KafkaServiceConfig.from_bootstrap_servers(
76
+ ["localhost:9092"], client_id="orders-worker"
77
+ ),
78
+ path="primary",
79
+ )
80
+ key = kafka.KafkaSubscriptionKey(
81
+ topics=("orders",),
82
+ group_id="orders-worker",
83
+ start_position=kafka.KafkaStartPosition.committed(),
84
+ )
85
+ subscription = kafka.kafka_subscribe(
86
+ hg.const(key, tp=hg.TS[kafka.KafkaSubscriptionKey]),
87
+ path="primary",
88
+ )
89
+ kafka.kafka_commit(subscription["cursor"], path="primary")
90
+ ```
91
+
92
+ The extension wheel also owns the released import path
93
+ `hgraph.adaptors.kafka`. Existing `message_publisher`, `message_subscriber`,
94
+ `KafkaMessage`, and `register_kafka_adaptor` imports therefore continue to
95
+ work without making the core `hgraph` package depend on this extension.
96
+
97
+ ## Recovery and simulation
98
+
99
+ Subscriptions support explicit topic, pattern, or partition selection; group
100
+ or independent assignment; earliest, latest, committed, timestamp, explicit,
101
+ and graph-start positions; snapshot, timestamp, and explicit stop boundaries;
102
+ key filters; deterministic timestamp/topic/partition/offset replay; and
103
+ explicit or graph-delivery commits.
104
+
105
+ Simulation is intentionally limited to bounded, record-time recovery. The
106
+ consumer preloads the finite replay and schedules records at deterministic
107
+ graph times. Publish, commit, unbounded asynchronous input, and
108
+ `OnGraphDelivery` commit mode are rejected in simulation rather than silently
109
+ changing their semantics.
110
+
111
+ ## Build and test
112
+
113
+ This is a first-party extension in the hgraph monorepo. It remains a separate
114
+ CMake package and Python distribution: the top-level core package does not
115
+ link librdkafka or install these modules.
116
+
117
+ For an in-tree native development build from the repository root:
118
+
119
+ ```sh
120
+ cmake -S . -B build-kafka \
121
+ -DHGRAPH_BUILD_KAFKA_EXTENSION=ON \
122
+ -DBUILD_TESTING=ON
123
+ cmake --build build-kafka --parallel
124
+ ctest --test-dir build-kafka --output-on-failure
125
+ ```
126
+
127
+ The extension can still be configured independently against an installed
128
+ hgraph SDK:
129
+
130
+ ```sh
131
+ cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/hgraph/install
132
+ cmake --build build --parallel
133
+ ctest --test-dir build --output-on-failure
134
+ ```
135
+
136
+ Build its separately deployable ABI3 wheel from the repository root after
137
+ making the matching hgraph SDK discoverable through `CMAKE_PREFIX_PATH`:
138
+
139
+ ```sh
140
+ CMAKE_PREFIX_PATH=/path/to/hgraph/sdk \
141
+ uv build --wheel --package hgraph-kafka --python 3.12
142
+ ```
143
+
144
+ The deterministic suite uses librdkafka's mock cluster and the extension fake
145
+ transport. To include a real broker round trip, provide a clean topic:
146
+
147
+ ```sh
148
+ HGRAPH_KAFKA_INTEGRATION_BOOTSTRAP=localhost:9092 \
149
+ HGRAPH_KAFKA_INTEGRATION_TOPIC=hgraph-kafka-integration \
150
+ ctest --test-dir build --output-on-failure
151
+ ```
152
+
153
+ Wheel builds require the SDK installed by a stable-ABI hgraph wheel. The
154
+ extension rejects an SDK that links `Python::Python`, because that would pin
155
+ the nominal ABI3 module to the build interpreter.
@@ -0,0 +1,15 @@
1
+ @PACKAGE_INIT@
2
+
3
+ include(CMakeFindDependencyMacro)
4
+ find_dependency(hgraph CONFIG)
5
+ set(_hgraph_kafka_saved_module_path "${CMAKE_MODULE_PATH}")
6
+ if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../RdKafka/FindLZ4.cmake")
7
+ list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../RdKafka")
8
+ endif()
9
+ find_dependency(RdKafka 2.15 CONFIG)
10
+ set(CMAKE_MODULE_PATH "${_hgraph_kafka_saved_module_path}")
11
+ unset(_hgraph_kafka_saved_module_path)
12
+
13
+ include("${CMAKE_CURRENT_LIST_DIR}/hgraphKafkaTargets.cmake")
14
+
15
+ check_required_components(hgraph-kafka)
@@ -0,0 +1,16 @@
1
+ #ifndef HGRAPH_KAFKA_EXPORT_H
2
+ #define HGRAPH_KAFKA_EXPORT_H
3
+
4
+ #if defined(HGRAPH_KAFKA_STATIC_DEFINE)
5
+ #define HGRAPH_KAFKA_EXPORT
6
+ #elif defined(_WIN32)
7
+ #if defined(hgraph_kafka_EXPORTS)
8
+ #define HGRAPH_KAFKA_EXPORT __declspec(dllexport)
9
+ #else
10
+ #define HGRAPH_KAFKA_EXPORT __declspec(dllimport)
11
+ #endif
12
+ #else
13
+ #define HGRAPH_KAFKA_EXPORT __attribute__((visibility("default")))
14
+ #endif
15
+
16
+ #endif // HGRAPH_KAFKA_EXPORT_H
@@ -0,0 +1,59 @@
1
+ #ifndef HGRAPH_KAFKA_SERVICE_H
2
+ #define HGRAPH_KAFKA_SERVICE_H
3
+
4
+ #include <hgraph/kafka/export.h>
5
+ #include <hgraph/kafka/types.h>
6
+
7
+ #include <hgraph/types/graph_wiring.h>
8
+ #include <hgraph/types/service_wiring.h>
9
+
10
+ namespace hgraph::kafka
11
+ {
12
+ struct KafkaSubscriptionService
13
+ {
14
+ static constexpr std::string_view name{"kafka_subscription"};
15
+ using key_type = KafkaSubscriptionKey;
16
+ using value_schema = KafkaSubscriptionOutput;
17
+ };
18
+
19
+ struct KafkaPublishService
20
+ {
21
+ static constexpr std::string_view name{"kafka_publish"};
22
+ using request_schema = KafkaPublishRequest;
23
+ using response_schema = TS<KafkaDeliveryReport>;
24
+ };
25
+
26
+ struct KafkaCommitService
27
+ {
28
+ static constexpr std::string_view name{"kafka_commit"};
29
+ using request_schema = TS<KafkaCursor>;
30
+ };
31
+
32
+ struct KafkaEventService
33
+ {
34
+ static constexpr std::string_view name{"kafka_events"};
35
+ using output_schema = TS<KafkaEvent>;
36
+ };
37
+
38
+ /** Register one lazy, path-bound production Kafka service implementation.
39
+ * No Kafka client or worker thread is created until the graph is started. */
40
+ HGRAPH_KAFKA_EXPORT void register_service(Wiring &w, service::ServicePath path, Value service_config);
41
+
42
+ [[nodiscard]] HGRAPH_KAFKA_EXPORT Port<KafkaSubscriptionOutput> subscribe(Wiring &w, service::ServicePath path,
43
+ Port<TS<KafkaSubscriptionKey>> key);
44
+
45
+ [[nodiscard]] HGRAPH_KAFKA_EXPORT Port<KafkaPublishRequest> publish_request(Wiring &w, Port<TS<Str>> topic,
46
+ Port<TS<KafkaProduceRecord>> record);
47
+
48
+ [[nodiscard]] HGRAPH_KAFKA_EXPORT Port<KafkaPublishRequest> publish_request(Wiring &w, Str topic,
49
+ Port<TS<KafkaProduceRecord>> record);
50
+
51
+ [[nodiscard]] HGRAPH_KAFKA_EXPORT Port<TS<KafkaDeliveryReport>> publish(Wiring &w, service::ServicePath path,
52
+ Port<KafkaPublishRequest> request);
53
+
54
+ HGRAPH_KAFKA_EXPORT void commit(Wiring &w, service::ServicePath path, Port<TS<KafkaCursor>> cursor);
55
+
56
+ [[nodiscard]] HGRAPH_KAFKA_EXPORT Port<TS<KafkaEvent>> events(Wiring &w, service::ServicePath path);
57
+ } // namespace hgraph::kafka
58
+
59
+ #endif // HGRAPH_KAFKA_SERVICE_H