robotframework-okw-kafka 0.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.
@@ -0,0 +1,17 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2026 Zoltan Hrabovszki
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as
8
+ published by the Free Software Foundation, either version 3 of the
9
+ License, or (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: robotframework-okw-kafka
3
+ Version: 0.1.1
4
+ Summary: Robot Framework keyword library for Apache Kafka integration testing — produce, consume, and verify messages using the OKW keyword pattern.
5
+ Author-email: Zoltan Hrabovszki <hrabovszki@googlemail.com>
6
+ License-Expression: AGPL-3.0-only
7
+ Project-URL: Homepage, https://github.com/Hrabovszki1023/robotframework-okw-kafka
8
+ Project-URL: Repository, https://github.com/Hrabovszki1023/robotframework-okw-kafka
9
+ Project-URL: Documentation, https://hrabovszki1023.github.io/robotframework-okw-kafka/
10
+ Project-URL: Keyword Documentation, https://hrabovszki1023.github.io/robotframework-okw-kafka/
11
+ Project-URL: Runnable Examples, https://github.com/Hrabovszki1023/okw-examples/tree/main/kafka/
12
+ Keywords: robotframework,kafka,testing,okw,keyword-driven
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: Robot Framework
15
+ Classifier: Framework :: Robot Framework :: Library
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: robotframework>=6.0
29
+ Requires-Dist: confluent-kafka>=2.0
30
+ Requires-Dist: okw-contract-utils>=0.2.0
31
+ Requires-Dist: PyYAML>=6.0
32
+ Dynamic: license-file
33
+
34
+ # robotframework-okw-kafka
35
+
36
+ [![PyPI](https://img.shields.io/pypi/v/robotframework-okw-kafka.svg)](https://pypi.org/project/robotframework-okw-kafka/)
37
+ [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
38
+
39
+ Robot Framework keyword library for Apache Kafka integration testing —
40
+ produce, consume, and verify messages using the OKW keyword pattern.
41
+
42
+ > Deutsche Version: [README_de.md](README_de.md)
43
+
44
+ ## Signal vs. NOISE
45
+
46
+ | Signal (your test) | NOISE (hidden in YAML) |
47
+ |---|---|
48
+ | `KafkaSetValue $.action create` | Bootstrap servers, serializers |
49
+ | `KafkaConsume order-events TIMEOUT=30` | Consumer group, offset config |
50
+ | `KafkaVerifyValue $.status completed` | SSL, authentication, partitions |
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install robotframework-okw-kafka
56
+ ```
57
+
58
+ ## Minimal Example
59
+
60
+ ```robot
61
+ *** Settings ***
62
+ Library okw_kafka.library.OkwKafkaLibrary
63
+
64
+ *** Test Cases ***
65
+ Order Event Roundtrip
66
+ KafkaStart kafka-local
67
+ KafkaSetTopic order-commands
68
+ KafkaSetValue $.action create
69
+ KafkaSetValue $.item Widget-42
70
+ KafkaProduce
71
+
72
+ KafkaConsume order-events FILTER=$.item=Widget-42 TIMEOUT=30
73
+ KafkaVerifyValue $.status created
74
+ KafkaMemorizeValue $.orderId ORDER_ID
75
+
76
+ KafkaStop
77
+ ```
78
+
79
+ ## Keyword Pattern
80
+
81
+ The same pattern across all OKW technologies:
82
+
83
+ | Phase | GUI | REST | SSH | Kafka |
84
+ |---|---|---|---|---|
85
+ | Scope | `SelectWindow` | `RESTSelectEndpoint` | `Open Remote Session` | `KafkaSetTopic` |
86
+ | Input | `SetValue` | `RESTSetValue` | `Set Remote` | `KafkaSetValue` |
87
+ | Action | `ClickOn` | `RESTSendRequest` | `Execute Remote` | `KafkaProduce` |
88
+ | Verify | `VerifyValue` | `RESTVerifyValue` | `Verify Remote Response` | `KafkaVerifyValue` |
89
+
90
+ ## YAML Configuration
91
+
92
+ ```yaml
93
+ # configs/kafka-local.yaml
94
+ bootstrap_servers: "localhost:9092"
95
+ consumer_group: "okw-test"
96
+ auto_offset_reset: "latest"
97
+ value_serializer: "json"
98
+ value_deserializer: "json"
99
+ ```
100
+
101
+ ## Keywords
102
+
103
+ ### Session
104
+ - `KafkaStart` / `KafkaStop`
105
+
106
+ ### Produce
107
+ - `KafkaSetTopic`, `KafkaSetValue`, `KafkaSetValueAsString`
108
+ - `KafkaSetHeader`, `KafkaSetKey`, `KafkaProduce`
109
+
110
+ ### Consume
111
+ - `KafkaConsume` (timeout-based polling, optional JSONPath filter)
112
+ - `KafkaConsumeLatest`
113
+
114
+ ### Verify
115
+ - `KafkaVerifyValue` / `KafkaVerifyValueWCM` / `KafkaVerifyValueREGX`
116
+ - `KafkaVerifyHeader`, `KafkaVerifyKey`
117
+
118
+ ### Memorize
119
+ - `KafkaMemorizeValue`, `KafkaMemorizeKey`
120
+
121
+ ### Admin
122
+ - `KafkaVerifyTopicExists`, `KafkaVerifyPartitionCount`, `KafkaLogTopics`
123
+
124
+ ## Keyword Documentation
125
+
126
+ [Libdoc (GitHub Pages)](https://hrabovszki1023.github.io/robotframework-okw-kafka/)
127
+
128
+ ## Runnable Examples
129
+
130
+ [okw-examples/kafka/](https://github.com/Hrabovszki1023/okw-examples/tree/main/kafka/)
131
+
132
+ ## License
133
+
134
+ AGPL-3.0 — see [LICENSE](LICENSE).
@@ -0,0 +1,101 @@
1
+ # robotframework-okw-kafka
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/robotframework-okw-kafka.svg)](https://pypi.org/project/robotframework-okw-kafka/)
4
+ [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
5
+
6
+ Robot Framework keyword library for Apache Kafka integration testing —
7
+ produce, consume, and verify messages using the OKW keyword pattern.
8
+
9
+ > Deutsche Version: [README_de.md](README_de.md)
10
+
11
+ ## Signal vs. NOISE
12
+
13
+ | Signal (your test) | NOISE (hidden in YAML) |
14
+ |---|---|
15
+ | `KafkaSetValue $.action create` | Bootstrap servers, serializers |
16
+ | `KafkaConsume order-events TIMEOUT=30` | Consumer group, offset config |
17
+ | `KafkaVerifyValue $.status completed` | SSL, authentication, partitions |
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install robotframework-okw-kafka
23
+ ```
24
+
25
+ ## Minimal Example
26
+
27
+ ```robot
28
+ *** Settings ***
29
+ Library okw_kafka.library.OkwKafkaLibrary
30
+
31
+ *** Test Cases ***
32
+ Order Event Roundtrip
33
+ KafkaStart kafka-local
34
+ KafkaSetTopic order-commands
35
+ KafkaSetValue $.action create
36
+ KafkaSetValue $.item Widget-42
37
+ KafkaProduce
38
+
39
+ KafkaConsume order-events FILTER=$.item=Widget-42 TIMEOUT=30
40
+ KafkaVerifyValue $.status created
41
+ KafkaMemorizeValue $.orderId ORDER_ID
42
+
43
+ KafkaStop
44
+ ```
45
+
46
+ ## Keyword Pattern
47
+
48
+ The same pattern across all OKW technologies:
49
+
50
+ | Phase | GUI | REST | SSH | Kafka |
51
+ |---|---|---|---|---|
52
+ | Scope | `SelectWindow` | `RESTSelectEndpoint` | `Open Remote Session` | `KafkaSetTopic` |
53
+ | Input | `SetValue` | `RESTSetValue` | `Set Remote` | `KafkaSetValue` |
54
+ | Action | `ClickOn` | `RESTSendRequest` | `Execute Remote` | `KafkaProduce` |
55
+ | Verify | `VerifyValue` | `RESTVerifyValue` | `Verify Remote Response` | `KafkaVerifyValue` |
56
+
57
+ ## YAML Configuration
58
+
59
+ ```yaml
60
+ # configs/kafka-local.yaml
61
+ bootstrap_servers: "localhost:9092"
62
+ consumer_group: "okw-test"
63
+ auto_offset_reset: "latest"
64
+ value_serializer: "json"
65
+ value_deserializer: "json"
66
+ ```
67
+
68
+ ## Keywords
69
+
70
+ ### Session
71
+ - `KafkaStart` / `KafkaStop`
72
+
73
+ ### Produce
74
+ - `KafkaSetTopic`, `KafkaSetValue`, `KafkaSetValueAsString`
75
+ - `KafkaSetHeader`, `KafkaSetKey`, `KafkaProduce`
76
+
77
+ ### Consume
78
+ - `KafkaConsume` (timeout-based polling, optional JSONPath filter)
79
+ - `KafkaConsumeLatest`
80
+
81
+ ### Verify
82
+ - `KafkaVerifyValue` / `KafkaVerifyValueWCM` / `KafkaVerifyValueREGX`
83
+ - `KafkaVerifyHeader`, `KafkaVerifyKey`
84
+
85
+ ### Memorize
86
+ - `KafkaMemorizeValue`, `KafkaMemorizeKey`
87
+
88
+ ### Admin
89
+ - `KafkaVerifyTopicExists`, `KafkaVerifyPartitionCount`, `KafkaLogTopics`
90
+
91
+ ## Keyword Documentation
92
+
93
+ [Libdoc (GitHub Pages)](https://hrabovszki1023.github.io/robotframework-okw-kafka/)
94
+
95
+ ## Runnable Examples
96
+
97
+ [okw-examples/kafka/](https://github.com/Hrabovszki1023/okw-examples/tree/main/kafka/)
98
+
99
+ ## License
100
+
101
+ AGPL-3.0 — see [LICENSE](LICENSE).
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "robotframework-okw-kafka"
7
+ version = "0.1.1"
8
+ description = "Robot Framework keyword library for Apache Kafka integration testing — produce, consume, and verify messages using the OKW keyword pattern."
9
+ readme = "README.md"
10
+ license = "AGPL-3.0-only"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "Zoltan Hrabovszki", email = "hrabovszki@googlemail.com"},
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Framework :: Robot Framework",
18
+ "Framework :: Robot Framework :: Library",
19
+ "Intended Audience :: Developers",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Software Development :: Testing",
28
+ ]
29
+ keywords = ["robotframework", "kafka", "testing", "okw", "keyword-driven"]
30
+ dependencies = [
31
+ "robotframework>=6.0",
32
+ "confluent-kafka>=2.0",
33
+ "okw-contract-utils>=0.2.0",
34
+ "PyYAML>=6.0",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/Hrabovszki1023/robotframework-okw-kafka"
39
+ Repository = "https://github.com/Hrabovszki1023/robotframework-okw-kafka"
40
+ Documentation = "https://hrabovszki1023.github.io/robotframework-okw-kafka/"
41
+ "Keyword Documentation" = "https://hrabovszki1023.github.io/robotframework-okw-kafka/"
42
+ "Runnable Examples" = "https://github.com/Hrabovszki1023/okw-examples/tree/main/kafka/"
43
+
44
+ [tool.setuptools.packages.find]
45
+ where = ["src"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """OKW Kafka — Robot Framework keyword library for Apache Kafka integration testing."""
2
+
3
+ from .library import OkwKafkaLibrary
4
+
5
+ __all__ = ["OkwKafkaLibrary"]
@@ -0,0 +1,339 @@
1
+ """KafkaContext — mutable state for one Kafka session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from collections import defaultdict
8
+
9
+ try:
10
+ import confluent_kafka
11
+ import confluent_kafka.admin
12
+ except ImportError:
13
+ confluent_kafka = None
14
+
15
+
16
+ class KafkaContext:
17
+ """Holds producer, consumer, admin client and message build state."""
18
+
19
+ def __init__(self, config: dict, backend: str = "confluent"):
20
+ self._config = config
21
+ self._backend = backend
22
+
23
+ self._producer = None
24
+ self._consumer = None
25
+ self._admin = None
26
+
27
+ self._value_serializer = config.get("value_serializer", "json")
28
+ self._value_deserializer = config.get("value_deserializer", "json")
29
+
30
+ self._current_topic: str | None = None
31
+ self._message_body: dict = {}
32
+ self._message_headers: dict[str, str] = {}
33
+ self._message_key: str | None = None
34
+
35
+ self._last_message_value: dict | str | None = None
36
+ self._last_message_key: str | None = None
37
+ self._last_message_headers: dict[str, str] = {}
38
+
39
+ self._stub_messages: dict[str, list] = defaultdict(list)
40
+
41
+ if self._backend == "confluent":
42
+ self._ensure_confluent()
43
+
44
+ def _ensure_confluent(self):
45
+ if confluent_kafka is None:
46
+ raise ImportError(
47
+ "confluent-kafka is not installed. "
48
+ "Install it with: pip install confluent-kafka"
49
+ )
50
+
51
+ def _build_confluent_config(self, *, consumer: bool = False) -> dict:
52
+ cfg = {"bootstrap.servers": self._config.get("bootstrap_servers", "localhost:9092")}
53
+
54
+ if self._config.get("security_protocol"):
55
+ cfg["security.protocol"] = self._config["security_protocol"]
56
+ if self._config.get("sasl_mechanism"):
57
+ cfg["sasl.mechanism"] = self._config["sasl_mechanism"]
58
+ if self._config.get("sasl_username"):
59
+ cfg["sasl.username"] = self._config["sasl_username"]
60
+ if self._config.get("sasl_password"):
61
+ cfg["sasl.password"] = self._config["sasl_password"]
62
+ if self._config.get("ssl_ca_location"):
63
+ cfg["ssl.ca.location"] = self._config["ssl_ca_location"]
64
+
65
+ if consumer:
66
+ cfg["group.id"] = self._config.get("consumer_group", "okw-test")
67
+ cfg["auto.offset.reset"] = self._config.get("auto_offset_reset", "latest")
68
+ cfg["enable.auto.commit"] = False
69
+
70
+ return cfg
71
+
72
+ # ── Producer ────────────────────────────────────────────────
73
+
74
+ def _ensure_producer(self):
75
+ if self._backend == "stub":
76
+ return
77
+ if self._producer is None:
78
+ self._producer = confluent_kafka.Producer(self._build_confluent_config())
79
+
80
+ def produce(self, topic: str, value: dict | str, key: str | None,
81
+ headers: dict[str, str]):
82
+ serialized = self._serialize(value)
83
+ key_bytes = key.encode("utf-8") if key else None
84
+ header_tuples = [(k, v.encode("utf-8")) for k, v in headers.items()]
85
+
86
+ if self._backend == "stub":
87
+ self._stub_messages[topic].append({
88
+ "value": value,
89
+ "key": key,
90
+ "headers": dict(headers),
91
+ })
92
+ return
93
+
94
+ self._ensure_producer()
95
+ self._producer.produce(
96
+ topic,
97
+ value=serialized,
98
+ key=key_bytes,
99
+ headers=header_tuples if header_tuples else None,
100
+ )
101
+ self._producer.flush(timeout=10)
102
+
103
+ def _serialize(self, value) -> bytes:
104
+ if self._value_serializer == "json":
105
+ return json.dumps(value).encode("utf-8")
106
+ return str(value).encode("utf-8")
107
+
108
+ def _deserialize(self, data: bytes):
109
+ if self._value_deserializer == "json":
110
+ return json.loads(data.decode("utf-8"))
111
+ return data.decode("utf-8")
112
+
113
+ # ── Consumer ────────────────────────────────────────────────
114
+
115
+ def consume(self, topic: str, filter_fn=None, timeout: int = 30):
116
+ if self._backend == "stub":
117
+ return self._stub_consume(topic, filter_fn)
118
+
119
+ self._ensure_confluent()
120
+ if self._consumer is None:
121
+ self._consumer = confluent_kafka.Consumer(
122
+ self._build_confluent_config(consumer=True)
123
+ )
124
+ self._consumer.subscribe([topic])
125
+
126
+ deadline = time.time() + timeout
127
+ while time.time() < deadline:
128
+ msg = self._consumer.poll(1.0)
129
+ if msg is None:
130
+ continue
131
+ if msg.error():
132
+ continue
133
+
134
+ value = self._deserialize(msg.value())
135
+ if filter_fn and not filter_fn(value):
136
+ continue
137
+
138
+ self._store_consumed(value, msg.key(), msg.headers())
139
+ return
140
+
141
+ raise RuntimeError(
142
+ f"KafkaConsume: No matching message on '{topic}' within {timeout}s."
143
+ )
144
+
145
+ def consume_latest(self, topic: str, timeout: int = 30):
146
+ if self._backend == "stub":
147
+ return self._stub_consume_latest(topic)
148
+
149
+ self._ensure_confluent()
150
+ if self._consumer is None:
151
+ self._consumer = confluent_kafka.Consumer(
152
+ self._build_confluent_config(consumer=True)
153
+ )
154
+ self._consumer.subscribe([topic])
155
+
156
+ # Seek to end, then poll for new messages
157
+ deadline = time.time() + timeout
158
+ while time.time() < deadline:
159
+ msg = self._consumer.poll(1.0)
160
+ if msg is None:
161
+ continue
162
+ if msg.error():
163
+ continue
164
+
165
+ value = self._deserialize(msg.value())
166
+ self._store_consumed(value, msg.key(), msg.headers())
167
+ return
168
+
169
+ raise RuntimeError(
170
+ f"KafkaConsumeLatest: No message on '{topic}' within {timeout}s."
171
+ )
172
+
173
+ def _store_consumed(self, value, raw_key, raw_headers):
174
+ self._last_message_value = value
175
+ self._last_message_key = raw_key.decode("utf-8") if isinstance(raw_key, bytes) else raw_key
176
+ self._last_message_headers = {}
177
+ if raw_headers:
178
+ for k, v in raw_headers:
179
+ self._last_message_headers[k] = v.decode("utf-8") if isinstance(v, bytes) else str(v)
180
+
181
+ def _stub_consume(self, topic: str, filter_fn=None):
182
+ messages = self._stub_messages.get(topic, [])
183
+ for msg in messages:
184
+ if filter_fn and not filter_fn(msg["value"]):
185
+ continue
186
+ self._last_message_value = msg["value"]
187
+ self._last_message_key = msg["key"]
188
+ self._last_message_headers = msg["headers"]
189
+ return
190
+ raise RuntimeError(
191
+ f"KafkaConsume (stub): No matching message on '{topic}'."
192
+ )
193
+
194
+ def _stub_consume_latest(self, topic: str):
195
+ messages = self._stub_messages.get(topic, [])
196
+ if not messages:
197
+ raise RuntimeError(
198
+ f"KafkaConsumeLatest (stub): No messages on '{topic}'."
199
+ )
200
+ msg = messages[-1]
201
+ self._last_message_value = msg["value"]
202
+ self._last_message_key = msg["key"]
203
+ self._last_message_headers = msg["headers"]
204
+
205
+ # ── Admin ───────────────────────────────────────────────────
206
+
207
+ def _ensure_admin(self):
208
+ if self._backend == "stub":
209
+ return
210
+ self._ensure_confluent()
211
+ if self._admin is None:
212
+ self._admin = confluent_kafka.admin.AdminClient(
213
+ self._build_confluent_config()
214
+ )
215
+
216
+ def list_topics(self, timeout: float = 10) -> list[str]:
217
+ if self._backend == "stub":
218
+ return sorted(self._stub_messages.keys())
219
+
220
+ self._ensure_admin()
221
+ metadata = self._admin.list_topics(timeout=timeout)
222
+ return sorted(metadata.topics.keys())
223
+
224
+ def get_topic_partitions(self, topic: str, timeout: float = 10) -> int:
225
+ if self._backend == "stub":
226
+ return 1
227
+
228
+ self._ensure_admin()
229
+ metadata = self._admin.list_topics(topic=topic, timeout=timeout)
230
+ if topic not in metadata.topics:
231
+ raise RuntimeError(f"Topic '{topic}' not found.")
232
+ return len(metadata.topics[topic].partitions)
233
+
234
+ # ── Value resolution (JSONPath-like) ────────────────────────
235
+
236
+ def resolve_value(self, jsonpath: str) -> str:
237
+ if self._last_message_value is None:
238
+ raise RuntimeError("No consumed message available.")
239
+ path = self._strip_jsonpath(jsonpath)
240
+ return str(self._resolve_path(self._last_message_value, path))
241
+
242
+ def set_value(self, jsonpath: str, value, force_string: bool = False):
243
+ path = self._strip_jsonpath(jsonpath)
244
+ typed = value if force_string else self._auto_type(value)
245
+ self._set_nested(self._message_body, path, typed)
246
+
247
+ @staticmethod
248
+ def _strip_jsonpath(path: str) -> str:
249
+ if path.startswith("$."):
250
+ return path[2:]
251
+ if path.startswith("$"):
252
+ return path[1:]
253
+ return path
254
+
255
+ def reset_message(self):
256
+ self._message_body = {}
257
+ self._message_headers = {}
258
+ self._message_key = None
259
+
260
+ # ── Helpers (same as RestContext) ────────────────────────────
261
+
262
+ @staticmethod
263
+ def _resolve_path(obj, path: str):
264
+ for part in path.split("."):
265
+ if "[" in part:
266
+ key, idx_str = part.split("[", 1)
267
+ idx = int(idx_str.rstrip("]"))
268
+ if key:
269
+ obj = obj[key]
270
+ obj = obj[idx]
271
+ else:
272
+ obj = obj[part]
273
+ return obj
274
+
275
+ @staticmethod
276
+ def _set_nested(target: dict, path: str, value):
277
+ parts = path.split(".")
278
+ for part in parts[:-1]:
279
+ if "[" in part:
280
+ key, idx_str = part.split("[", 1)
281
+ idx = int(idx_str.rstrip("]"))
282
+ if key:
283
+ target.setdefault(key, [])
284
+ target = target[key]
285
+ while len(target) <= idx:
286
+ target.append({})
287
+ target = target[idx]
288
+ else:
289
+ target.setdefault(part, {})
290
+ target = target[part]
291
+ leaf = parts[-1]
292
+ if "[" in leaf:
293
+ key, idx_str = leaf.split("[", 1)
294
+ idx = int(idx_str.rstrip("]"))
295
+ if key:
296
+ target.setdefault(key, [])
297
+ target = target[key]
298
+ while len(target) <= idx:
299
+ target.append(None)
300
+ target[idx] = value
301
+ else:
302
+ target[leaf] = value
303
+
304
+ @staticmethod
305
+ def _auto_type(value: str):
306
+ if not isinstance(value, str):
307
+ return value
308
+ if value.lower() == "true":
309
+ return True
310
+ if value.lower() == "false":
311
+ return False
312
+ if value.lower() == "null" or value == "$NULL":
313
+ return None
314
+ try:
315
+ return int(value)
316
+ except ValueError:
317
+ pass
318
+ try:
319
+ return float(value)
320
+ except ValueError:
321
+ pass
322
+ return value
323
+
324
+ # ── Teardown ────────────────────────────────────────────────
325
+
326
+ def close(self):
327
+ if self._consumer:
328
+ try:
329
+ self._consumer.close()
330
+ except Exception:
331
+ pass
332
+ self._consumer = None
333
+ if self._producer:
334
+ try:
335
+ self._producer.flush(timeout=5)
336
+ except Exception:
337
+ pass
338
+ self._producer = None
339
+ self._admin = None