tkati-node-el 0.3.0a3__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.
- tkati_node_el-0.3.0a3/PKG-INFO +116 -0
- tkati_node_el-0.3.0a3/README.md +106 -0
- tkati_node_el-0.3.0a3/pyproject.toml +32 -0
- tkati_node_el-0.3.0a3/setup.cfg +4 -0
- tkati_node_el-0.3.0a3/src/tkati_node_el/__init__.py +0 -0
- tkati_node_el-0.3.0a3/src/tkati_node_el/__main__.py +4 -0
- tkati_node_el-0.3.0a3/src/tkati_node_el/main.py +40 -0
- tkati_node_el-0.3.0a3/src/tkati_node_el/py.typed +0 -0
- tkati_node_el-0.3.0a3/src/tkati_node_el/settings.py +8 -0
- tkati_node_el-0.3.0a3/src/tkati_node_el.egg-info/PKG-INFO +116 -0
- tkati_node_el-0.3.0a3/src/tkati_node_el.egg-info/SOURCES.txt +14 -0
- tkati_node_el-0.3.0a3/src/tkati_node_el.egg-info/dependency_links.txt +1 -0
- tkati_node_el-0.3.0a3/src/tkati_node_el.egg-info/entry_points.txt +2 -0
- tkati_node_el-0.3.0a3/src/tkati_node_el.egg-info/requires.txt +3 -0
- tkati_node_el-0.3.0a3/src/tkati_node_el.egg-info/top_level.txt +1 -0
- tkati_node_el-0.3.0a3/tests/test_node_el.py +103 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tkati-node-el
|
|
3
|
+
Version: 0.3.0a3
|
|
4
|
+
Summary: Generic extract/load node: reads from a configurable input and writes to a configurable output
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: tkati-core==0.3.0-alpha.3
|
|
8
|
+
Requires-Dist: loguru>=0.7.0
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.11.0
|
|
10
|
+
|
|
11
|
+
# tkati-node-el — generic extract/load node
|
|
12
|
+
|
|
13
|
+
Reads batches from a configurable input and writes them to a configurable output. Offsets are committed only after a successful write (at-least-once delivery).
|
|
14
|
+
|
|
15
|
+
Input and output kinds are selected via the `type` field in each section — pick from whatever `tkati-core` supports. Every backend's settings split a **`connection`** tier (server-specific: how to reach the broker/database) from the resource tier (`topic` for Kafka, `table` for ClickHouse) and, where relevant, a tier local to this reader/writer instance (Kafka's `consumer` settings).
|
|
16
|
+
|
|
17
|
+
- **Input**: `"kafka"` (JSON or Arrow-batch messages from a Kafka/Redpanda topic)
|
|
18
|
+
- **Output**: `"kafka"` or `"clickhouse"` (native Arrow insert)
|
|
19
|
+
- **DLQ**: same `OutputSettings` shape as `output` — a DLQ can be Kafka or ClickHouse too
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
Settings are loaded from a TOML file. Set the `SETTINGS_FILE` environment variable to point to it (defaults to `settings.toml`).
|
|
24
|
+
|
|
25
|
+
```toml
|
|
26
|
+
[input]
|
|
27
|
+
type = "kafka"
|
|
28
|
+
|
|
29
|
+
[input.connection]
|
|
30
|
+
broker = "redpanda:29092"
|
|
31
|
+
|
|
32
|
+
[input.topic]
|
|
33
|
+
name = "traffic_event"
|
|
34
|
+
|
|
35
|
+
[input.topic.schema]
|
|
36
|
+
uid = "string"
|
|
37
|
+
time = "timestamp[ms]"
|
|
38
|
+
traffic_in = "uint32"
|
|
39
|
+
# … other columns
|
|
40
|
+
|
|
41
|
+
[input.consumer]
|
|
42
|
+
group_id = "node-el-group"
|
|
43
|
+
batch_size = 1000
|
|
44
|
+
batch_timeout_sec = 10
|
|
45
|
+
auto_offset_reset = "latest"
|
|
46
|
+
|
|
47
|
+
[output]
|
|
48
|
+
type = "clickhouse"
|
|
49
|
+
dlq_split_factor = 10
|
|
50
|
+
|
|
51
|
+
[output.connection]
|
|
52
|
+
host = "clickhouse"
|
|
53
|
+
port = 9000
|
|
54
|
+
user = "default"
|
|
55
|
+
password = ""
|
|
56
|
+
secure = false
|
|
57
|
+
|
|
58
|
+
[output.table]
|
|
59
|
+
database = "default"
|
|
60
|
+
name = "traffic_event"
|
|
61
|
+
|
|
62
|
+
[dlq]
|
|
63
|
+
type = "kafka"
|
|
64
|
+
|
|
65
|
+
[dlq.connection]
|
|
66
|
+
broker = "redpanda:29092"
|
|
67
|
+
|
|
68
|
+
[dlq.topic]
|
|
69
|
+
name = "node-el-dlq"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
A Kafka output instead looks like:
|
|
73
|
+
|
|
74
|
+
```toml
|
|
75
|
+
[output]
|
|
76
|
+
type = "kafka"
|
|
77
|
+
|
|
78
|
+
[output.connection]
|
|
79
|
+
broker = "redpanda:29092"
|
|
80
|
+
|
|
81
|
+
[output.topic]
|
|
82
|
+
name = "some-other-topic"
|
|
83
|
+
format = "json" # or "arrow-batch"
|
|
84
|
+
key_column = "uid" # optional
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A ClickHouse DLQ instead looks like:
|
|
88
|
+
|
|
89
|
+
```toml
|
|
90
|
+
[dlq]
|
|
91
|
+
type = "clickhouse"
|
|
92
|
+
|
|
93
|
+
[dlq.connection]
|
|
94
|
+
host = "clickhouse"
|
|
95
|
+
port = 9000
|
|
96
|
+
user = "default"
|
|
97
|
+
password = ""
|
|
98
|
+
secure = false
|
|
99
|
+
|
|
100
|
+
[dlq.table]
|
|
101
|
+
database = "default"
|
|
102
|
+
name = "traffic_event_dlq"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## DLQ semantics
|
|
106
|
+
|
|
107
|
+
DLQ *fallback triggering* is currently only implemented for the `clickhouse` output kind — `KafkaProducer` has no retry/split logic of its own. The DLQ *sink* itself (where isolated bad rows end up) can be Kafka or ClickHouse, independent of the primary output. When a batch insert fails after all retries, the app switches to a recursive fallback to isolate the problematic rows:
|
|
108
|
+
|
|
109
|
+
1. The failing batch is split into `dlq_split_factor` equal sub-batches and each is retried independently.
|
|
110
|
+
2. If a sub-batch also fails it is split again — this repeats until individual rows are reached.
|
|
111
|
+
3. A single row that ClickHouse still rejects is written to the DLQ sink, preserving the full schema (Arrow IPC `arrow-batch` format for a Kafka DLQ).
|
|
112
|
+
4. After all rows are handled (inserted or DLQ'd), the input offset is committed and the app resumes normal large-batch processing.
|
|
113
|
+
|
|
114
|
+
`dlq_split_factor` is a setting on the `clickhouse` `[output]` block (see above), not on `[dlq]` — it describes how the primary output retries, independent of where the DLQ sink sends isolated rows. With `dlq_split_factor=10` and a 1 000-row batch this takes at most 3 recursive levels (1000 → 100 → 10 → 1).
|
|
115
|
+
|
|
116
|
+
**Delivery guarantee: at-least-once.** If the process crashes mid-recursion the uncommitted batch is re-read on restart and re-processed from the beginning, which may produce duplicate rows in the output and duplicate messages in the DLQ.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# tkati-node-el — generic extract/load node
|
|
2
|
+
|
|
3
|
+
Reads batches from a configurable input and writes them to a configurable output. Offsets are committed only after a successful write (at-least-once delivery).
|
|
4
|
+
|
|
5
|
+
Input and output kinds are selected via the `type` field in each section — pick from whatever `tkati-core` supports. Every backend's settings split a **`connection`** tier (server-specific: how to reach the broker/database) from the resource tier (`topic` for Kafka, `table` for ClickHouse) and, where relevant, a tier local to this reader/writer instance (Kafka's `consumer` settings).
|
|
6
|
+
|
|
7
|
+
- **Input**: `"kafka"` (JSON or Arrow-batch messages from a Kafka/Redpanda topic)
|
|
8
|
+
- **Output**: `"kafka"` or `"clickhouse"` (native Arrow insert)
|
|
9
|
+
- **DLQ**: same `OutputSettings` shape as `output` — a DLQ can be Kafka or ClickHouse too
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Settings are loaded from a TOML file. Set the `SETTINGS_FILE` environment variable to point to it (defaults to `settings.toml`).
|
|
14
|
+
|
|
15
|
+
```toml
|
|
16
|
+
[input]
|
|
17
|
+
type = "kafka"
|
|
18
|
+
|
|
19
|
+
[input.connection]
|
|
20
|
+
broker = "redpanda:29092"
|
|
21
|
+
|
|
22
|
+
[input.topic]
|
|
23
|
+
name = "traffic_event"
|
|
24
|
+
|
|
25
|
+
[input.topic.schema]
|
|
26
|
+
uid = "string"
|
|
27
|
+
time = "timestamp[ms]"
|
|
28
|
+
traffic_in = "uint32"
|
|
29
|
+
# … other columns
|
|
30
|
+
|
|
31
|
+
[input.consumer]
|
|
32
|
+
group_id = "node-el-group"
|
|
33
|
+
batch_size = 1000
|
|
34
|
+
batch_timeout_sec = 10
|
|
35
|
+
auto_offset_reset = "latest"
|
|
36
|
+
|
|
37
|
+
[output]
|
|
38
|
+
type = "clickhouse"
|
|
39
|
+
dlq_split_factor = 10
|
|
40
|
+
|
|
41
|
+
[output.connection]
|
|
42
|
+
host = "clickhouse"
|
|
43
|
+
port = 9000
|
|
44
|
+
user = "default"
|
|
45
|
+
password = ""
|
|
46
|
+
secure = false
|
|
47
|
+
|
|
48
|
+
[output.table]
|
|
49
|
+
database = "default"
|
|
50
|
+
name = "traffic_event"
|
|
51
|
+
|
|
52
|
+
[dlq]
|
|
53
|
+
type = "kafka"
|
|
54
|
+
|
|
55
|
+
[dlq.connection]
|
|
56
|
+
broker = "redpanda:29092"
|
|
57
|
+
|
|
58
|
+
[dlq.topic]
|
|
59
|
+
name = "node-el-dlq"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A Kafka output instead looks like:
|
|
63
|
+
|
|
64
|
+
```toml
|
|
65
|
+
[output]
|
|
66
|
+
type = "kafka"
|
|
67
|
+
|
|
68
|
+
[output.connection]
|
|
69
|
+
broker = "redpanda:29092"
|
|
70
|
+
|
|
71
|
+
[output.topic]
|
|
72
|
+
name = "some-other-topic"
|
|
73
|
+
format = "json" # or "arrow-batch"
|
|
74
|
+
key_column = "uid" # optional
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A ClickHouse DLQ instead looks like:
|
|
78
|
+
|
|
79
|
+
```toml
|
|
80
|
+
[dlq]
|
|
81
|
+
type = "clickhouse"
|
|
82
|
+
|
|
83
|
+
[dlq.connection]
|
|
84
|
+
host = "clickhouse"
|
|
85
|
+
port = 9000
|
|
86
|
+
user = "default"
|
|
87
|
+
password = ""
|
|
88
|
+
secure = false
|
|
89
|
+
|
|
90
|
+
[dlq.table]
|
|
91
|
+
database = "default"
|
|
92
|
+
name = "traffic_event_dlq"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## DLQ semantics
|
|
96
|
+
|
|
97
|
+
DLQ *fallback triggering* is currently only implemented for the `clickhouse` output kind — `KafkaProducer` has no retry/split logic of its own. The DLQ *sink* itself (where isolated bad rows end up) can be Kafka or ClickHouse, independent of the primary output. When a batch insert fails after all retries, the app switches to a recursive fallback to isolate the problematic rows:
|
|
98
|
+
|
|
99
|
+
1. The failing batch is split into `dlq_split_factor` equal sub-batches and each is retried independently.
|
|
100
|
+
2. If a sub-batch also fails it is split again — this repeats until individual rows are reached.
|
|
101
|
+
3. A single row that ClickHouse still rejects is written to the DLQ sink, preserving the full schema (Arrow IPC `arrow-batch` format for a Kafka DLQ).
|
|
102
|
+
4. After all rows are handled (inserted or DLQ'd), the input offset is committed and the app resumes normal large-batch processing.
|
|
103
|
+
|
|
104
|
+
`dlq_split_factor` is a setting on the `clickhouse` `[output]` block (see above), not on `[dlq]` — it describes how the primary output retries, independent of where the DLQ sink sends isolated rows. With `dlq_split_factor=10` and a 1 000-row batch this takes at most 3 recursive levels (1000 → 100 → 10 → 1).
|
|
105
|
+
|
|
106
|
+
**Delivery guarantee: at-least-once.** If the process crashes mid-recursion the uncommitted batch is re-read on restart and re-processed from the beginning, which may produce duplicate rows in the output and duplicate messages in the DLQ.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tkati-node-el"
|
|
3
|
+
version = "0.3.0-alpha.3"
|
|
4
|
+
description = "Generic extract/load node: reads from a configurable input and writes to a configurable output"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"tkati-core==0.3.0-alpha.3",
|
|
9
|
+
"loguru>=0.7.0",
|
|
10
|
+
"pydantic-settings>=2.11.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
tkati-node-el = "tkati_node_el.main:main"
|
|
15
|
+
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
dev = ["pytest>=9.0.1", "confluent-kafka>=2.11.0", "clickhouse-connect>=0.11.0"]
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["setuptools", "wheel"]
|
|
21
|
+
build-backend = "setuptools.build_meta"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
25
|
+
include = ["tkati_node_el*"]
|
|
26
|
+
|
|
27
|
+
[tool.uv]
|
|
28
|
+
package = true
|
|
29
|
+
|
|
30
|
+
[tool.uv-workspace-codegen]
|
|
31
|
+
generate = true
|
|
32
|
+
template_type = ["test", "publish"]
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from loguru import logger
|
|
2
|
+
from tkati_core import Consumer, Producer, build_consumer, build_producer
|
|
3
|
+
|
|
4
|
+
from tkati_node_el.settings import AppSettings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run_one_iteration(
|
|
8
|
+
consumer: Consumer,
|
|
9
|
+
producer: Producer,
|
|
10
|
+
settings: AppSettings,
|
|
11
|
+
) -> None:
|
|
12
|
+
batch = consumer.read_arrow(
|
|
13
|
+
num_messages=settings.input.consumer.batch_size,
|
|
14
|
+
timeout=settings.input.consumer.batch_timeout_sec,
|
|
15
|
+
)
|
|
16
|
+
if batch is None:
|
|
17
|
+
return
|
|
18
|
+
producer.produce_arrow(batch)
|
|
19
|
+
consumer.commit()
|
|
20
|
+
logger.info(f"Produced {len(batch)} rows")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> None:
|
|
24
|
+
settings = AppSettings() # type: ignore
|
|
25
|
+
|
|
26
|
+
consumer = build_consumer(settings.input)
|
|
27
|
+
|
|
28
|
+
dlq_producer: Producer | None = None
|
|
29
|
+
if settings.dlq is not None:
|
|
30
|
+
dlq_producer = build_producer(settings.dlq)
|
|
31
|
+
|
|
32
|
+
producer = build_producer(settings.output, dlq_producer=dlq_producer)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
while True:
|
|
36
|
+
run_one_iteration(consumer, producer, settings)
|
|
37
|
+
finally:
|
|
38
|
+
consumer.close()
|
|
39
|
+
if dlq_producer is not None:
|
|
40
|
+
dlq_producer.close()
|
|
File without changes
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tkati-node-el
|
|
3
|
+
Version: 0.3.0a3
|
|
4
|
+
Summary: Generic extract/load node: reads from a configurable input and writes to a configurable output
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: tkati-core==0.3.0-alpha.3
|
|
8
|
+
Requires-Dist: loguru>=0.7.0
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.11.0
|
|
10
|
+
|
|
11
|
+
# tkati-node-el — generic extract/load node
|
|
12
|
+
|
|
13
|
+
Reads batches from a configurable input and writes them to a configurable output. Offsets are committed only after a successful write (at-least-once delivery).
|
|
14
|
+
|
|
15
|
+
Input and output kinds are selected via the `type` field in each section — pick from whatever `tkati-core` supports. Every backend's settings split a **`connection`** tier (server-specific: how to reach the broker/database) from the resource tier (`topic` for Kafka, `table` for ClickHouse) and, where relevant, a tier local to this reader/writer instance (Kafka's `consumer` settings).
|
|
16
|
+
|
|
17
|
+
- **Input**: `"kafka"` (JSON or Arrow-batch messages from a Kafka/Redpanda topic)
|
|
18
|
+
- **Output**: `"kafka"` or `"clickhouse"` (native Arrow insert)
|
|
19
|
+
- **DLQ**: same `OutputSettings` shape as `output` — a DLQ can be Kafka or ClickHouse too
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
Settings are loaded from a TOML file. Set the `SETTINGS_FILE` environment variable to point to it (defaults to `settings.toml`).
|
|
24
|
+
|
|
25
|
+
```toml
|
|
26
|
+
[input]
|
|
27
|
+
type = "kafka"
|
|
28
|
+
|
|
29
|
+
[input.connection]
|
|
30
|
+
broker = "redpanda:29092"
|
|
31
|
+
|
|
32
|
+
[input.topic]
|
|
33
|
+
name = "traffic_event"
|
|
34
|
+
|
|
35
|
+
[input.topic.schema]
|
|
36
|
+
uid = "string"
|
|
37
|
+
time = "timestamp[ms]"
|
|
38
|
+
traffic_in = "uint32"
|
|
39
|
+
# … other columns
|
|
40
|
+
|
|
41
|
+
[input.consumer]
|
|
42
|
+
group_id = "node-el-group"
|
|
43
|
+
batch_size = 1000
|
|
44
|
+
batch_timeout_sec = 10
|
|
45
|
+
auto_offset_reset = "latest"
|
|
46
|
+
|
|
47
|
+
[output]
|
|
48
|
+
type = "clickhouse"
|
|
49
|
+
dlq_split_factor = 10
|
|
50
|
+
|
|
51
|
+
[output.connection]
|
|
52
|
+
host = "clickhouse"
|
|
53
|
+
port = 9000
|
|
54
|
+
user = "default"
|
|
55
|
+
password = ""
|
|
56
|
+
secure = false
|
|
57
|
+
|
|
58
|
+
[output.table]
|
|
59
|
+
database = "default"
|
|
60
|
+
name = "traffic_event"
|
|
61
|
+
|
|
62
|
+
[dlq]
|
|
63
|
+
type = "kafka"
|
|
64
|
+
|
|
65
|
+
[dlq.connection]
|
|
66
|
+
broker = "redpanda:29092"
|
|
67
|
+
|
|
68
|
+
[dlq.topic]
|
|
69
|
+
name = "node-el-dlq"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
A Kafka output instead looks like:
|
|
73
|
+
|
|
74
|
+
```toml
|
|
75
|
+
[output]
|
|
76
|
+
type = "kafka"
|
|
77
|
+
|
|
78
|
+
[output.connection]
|
|
79
|
+
broker = "redpanda:29092"
|
|
80
|
+
|
|
81
|
+
[output.topic]
|
|
82
|
+
name = "some-other-topic"
|
|
83
|
+
format = "json" # or "arrow-batch"
|
|
84
|
+
key_column = "uid" # optional
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A ClickHouse DLQ instead looks like:
|
|
88
|
+
|
|
89
|
+
```toml
|
|
90
|
+
[dlq]
|
|
91
|
+
type = "clickhouse"
|
|
92
|
+
|
|
93
|
+
[dlq.connection]
|
|
94
|
+
host = "clickhouse"
|
|
95
|
+
port = 9000
|
|
96
|
+
user = "default"
|
|
97
|
+
password = ""
|
|
98
|
+
secure = false
|
|
99
|
+
|
|
100
|
+
[dlq.table]
|
|
101
|
+
database = "default"
|
|
102
|
+
name = "traffic_event_dlq"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## DLQ semantics
|
|
106
|
+
|
|
107
|
+
DLQ *fallback triggering* is currently only implemented for the `clickhouse` output kind — `KafkaProducer` has no retry/split logic of its own. The DLQ *sink* itself (where isolated bad rows end up) can be Kafka or ClickHouse, independent of the primary output. When a batch insert fails after all retries, the app switches to a recursive fallback to isolate the problematic rows:
|
|
108
|
+
|
|
109
|
+
1. The failing batch is split into `dlq_split_factor` equal sub-batches and each is retried independently.
|
|
110
|
+
2. If a sub-batch also fails it is split again — this repeats until individual rows are reached.
|
|
111
|
+
3. A single row that ClickHouse still rejects is written to the DLQ sink, preserving the full schema (Arrow IPC `arrow-batch` format for a Kafka DLQ).
|
|
112
|
+
4. After all rows are handled (inserted or DLQ'd), the input offset is committed and the app resumes normal large-batch processing.
|
|
113
|
+
|
|
114
|
+
`dlq_split_factor` is a setting on the `clickhouse` `[output]` block (see above), not on `[dlq]` — it describes how the primary output retries, independent of where the DLQ sink sends isolated rows. With `dlq_split_factor=10` and a 1 000-row batch this takes at most 3 recursive levels (1000 → 100 → 10 → 1).
|
|
115
|
+
|
|
116
|
+
**Delivery guarantee: at-least-once.** If the process crashes mid-recursion the uncommitted batch is re-read on restart and re-processed from the beginning, which may produce duplicate rows in the output and duplicate messages in the DLQ.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/tkati_node_el/__init__.py
|
|
4
|
+
src/tkati_node_el/__main__.py
|
|
5
|
+
src/tkati_node_el/main.py
|
|
6
|
+
src/tkati_node_el/py.typed
|
|
7
|
+
src/tkati_node_el/settings.py
|
|
8
|
+
src/tkati_node_el.egg-info/PKG-INFO
|
|
9
|
+
src/tkati_node_el.egg-info/SOURCES.txt
|
|
10
|
+
src/tkati_node_el.egg-info/dependency_links.txt
|
|
11
|
+
src/tkati_node_el.egg-info/entry_points.txt
|
|
12
|
+
src/tkati_node_el.egg-info/requires.txt
|
|
13
|
+
src/tkati_node_el.egg-info/top_level.txt
|
|
14
|
+
tests/test_node_el.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tkati_node_el
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from unittest.mock import MagicMock
|
|
3
|
+
|
|
4
|
+
import clickhouse_connect.driver as ch_driver
|
|
5
|
+
import orjson
|
|
6
|
+
import pytest
|
|
7
|
+
from confluent_kafka import Producer
|
|
8
|
+
from tkati_core.clickhouse.producer import ClickhouseProducer
|
|
9
|
+
from tkati_core.clickhouse.settings import ClickHouseOutputSettings
|
|
10
|
+
from tkati_core.kafka.consumer import KafkaConsumer
|
|
11
|
+
from tkati_node_el.main import run_one_iteration
|
|
12
|
+
from tkati_node_el.settings import AppSettings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _make_consumer(test_settings: AppSettings) -> KafkaConsumer:
|
|
16
|
+
return KafkaConsumer(
|
|
17
|
+
kafka_config={
|
|
18
|
+
"bootstrap.servers": test_settings.input.connection.broker,
|
|
19
|
+
"group.id": test_settings.input.consumer.group_id,
|
|
20
|
+
"auto.offset.reset": "earliest",
|
|
21
|
+
"enable.auto.commit": False,
|
|
22
|
+
},
|
|
23
|
+
topic_name=test_settings.input.topic.name,
|
|
24
|
+
input_schema=test_settings.input.topic.schema,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_node_el_valid_flow(
|
|
29
|
+
kafka_producer_and_topic: Producer,
|
|
30
|
+
ch_client: ch_driver.Client,
|
|
31
|
+
ch_table: str,
|
|
32
|
+
mock_dlq_producer: MagicMock,
|
|
33
|
+
test_settings: AppSettings,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Produce a valid event to Kafka, run one iteration, verify the row lands in ClickHouse."""
|
|
36
|
+
event = {
|
|
37
|
+
"uid": "abc123",
|
|
38
|
+
"time": int(time.time() * 1000),
|
|
39
|
+
"package_id": 1,
|
|
40
|
+
"user_hash": "uhash",
|
|
41
|
+
"sdk_hash": "shash",
|
|
42
|
+
"conn_type": "https",
|
|
43
|
+
"country": "US",
|
|
44
|
+
"local_ip": "10.0.0.1",
|
|
45
|
+
"frontend_ip": "1.2.3.4",
|
|
46
|
+
"dest_addr": "8.8.8.8",
|
|
47
|
+
"client_ip": "192.168.1.1",
|
|
48
|
+
"traffic_in": 100,
|
|
49
|
+
"traffic_out": 200,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
kafka_producer_and_topic.produce(
|
|
53
|
+
test_settings.input.topic.name, value=orjson.dumps(event)
|
|
54
|
+
)
|
|
55
|
+
kafka_producer_and_topic.flush()
|
|
56
|
+
|
|
57
|
+
assert isinstance(test_settings.output, ClickHouseOutputSettings)
|
|
58
|
+
|
|
59
|
+
consumer = _make_consumer(test_settings)
|
|
60
|
+
ch_producer = ClickhouseProducer(
|
|
61
|
+
ch_client=ch_client,
|
|
62
|
+
table=ch_table,
|
|
63
|
+
dlq_producer=mock_dlq_producer,
|
|
64
|
+
)
|
|
65
|
+
try:
|
|
66
|
+
run_one_iteration(consumer, ch_producer, test_settings)
|
|
67
|
+
finally:
|
|
68
|
+
consumer.close()
|
|
69
|
+
|
|
70
|
+
result = ch_client.query(f"SELECT uid, traffic_in, traffic_out FROM {ch_table}")
|
|
71
|
+
assert result.result_rows == [("abc123", 100, 200)]
|
|
72
|
+
mock_dlq_producer.produce_arrow.assert_not_called()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_node_el_malformed_data(
|
|
76
|
+
kafka_producer_and_topic: Producer,
|
|
77
|
+
ch_client: ch_driver.Client,
|
|
78
|
+
ch_table: str,
|
|
79
|
+
mock_dlq_producer: MagicMock,
|
|
80
|
+
test_settings: AppSettings,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Produce malformed JSON to Kafka; run_one_iteration must raise with 'JSON parse error'."""
|
|
83
|
+
kafka_producer_and_topic.produce(
|
|
84
|
+
test_settings.input.topic.name, value=b"not a json object"
|
|
85
|
+
)
|
|
86
|
+
kafka_producer_and_topic.flush()
|
|
87
|
+
|
|
88
|
+
assert isinstance(test_settings.output, ClickHouseOutputSettings)
|
|
89
|
+
|
|
90
|
+
consumer = _make_consumer(test_settings)
|
|
91
|
+
ch_producer = ClickhouseProducer(
|
|
92
|
+
ch_client=ch_client,
|
|
93
|
+
table=ch_table,
|
|
94
|
+
dlq_producer=mock_dlq_producer,
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
with pytest.raises(Exception, match="JSON parse error"):
|
|
98
|
+
run_one_iteration(consumer, ch_producer, test_settings)
|
|
99
|
+
finally:
|
|
100
|
+
consumer.close()
|
|
101
|
+
|
|
102
|
+
result = ch_client.query(f"SELECT count() FROM {ch_table}")
|
|
103
|
+
assert result.result_rows[0][0] == 0
|