Skip to content

Commit 59a1f54

Browse files
authored
refactor(pipeline): replace networkx with rustworkx (#518)
1 parent dded6e3 commit 59a1f54

4 files changed

Lines changed: 692 additions & 583 deletions

File tree

kpops/pipeline/__init__.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from dataclasses import dataclass, field
77
from typing import TYPE_CHECKING, Any, TypeAlias
88

9-
import networkx as nx
9+
import rustworkx as rx
1010
import yaml
1111
from pydantic import (
1212
SerializeAsAny,
@@ -37,7 +37,8 @@ class Pipeline:
3737
"""Pipeline representation."""
3838

3939
_component_index: dict[str, PipelineComponent] = field(default_factory=dict)
40-
_graph: nx.DiGraph[str] = field(default_factory=nx.DiGraph)
40+
_graph: rx.PyDiGraph[str, None] = field(default_factory=rx.PyDiGraph)
41+
_node_index: dict[str, int] = field(default_factory=dict)
4142

4243
@property
4344
def step_names(self) -> list[str]:
@@ -118,23 +119,23 @@ async def run_graph_layers(
118119
for layer_components in pending_layers:
119120
await run_layer_parallel(layer_components)
120121

121-
graph: nx.DiGraph[str] = self._graph.copy()
122+
graph = self._graph.copy()
122123

123124
# We add an extra node to the graph, connecting all the leaf nodes to it
124125
# in that way we make this node the root of the graph, avoiding backtracking
125-
root_node = "root_node_bfs"
126-
graph.add_node(root_node)
126+
root_node = graph.add_node("root_node_bfs")
127127

128-
for node in graph:
129-
predecessors = list(graph.predecessors(node))
130-
if not predecessors:
131-
graph.add_edge(root_node, node)
128+
for node in graph.node_indices():
129+
if node != root_node and not list(graph.predecessors(node)):
130+
graph.add_edge(root_node, node, None)
132131

133-
layers_graph: list[list[str]] = list(nx.bfs_layers(graph, root_node))
132+
layers_graph = list(rx.bfs_layers(graph, [root_node]))
134133

135134
sorted_layers: list[list[PipelineComponent]] = []
136135
for layer in layers_graph[1:]:
137-
if parallel_components := self.__get_parallel_components_from(layer):
136+
if parallel_components := self.__get_parallel_components_from(
137+
[graph[idx] for idx in layer]
138+
):
138139
sorted_layers.append(parallel_components)
139140

140141
if reverse:
@@ -158,22 +159,27 @@ def __iter__(self) -> Iterator[PipelineComponent]:
158159
def __len__(self) -> int:
159160
return len(self.components)
160161

162+
def __get_or_add_node(self, node_id: str) -> int:
163+
if node_id not in self._node_index:
164+
self._node_index[node_id] = self._graph.add_node(node_id)
165+
return self._node_index[node_id]
166+
161167
def __add_to_graph(self, component: PipelineComponent):
162-
self._graph.add_node(component.id)
168+
node = self.__get_or_add_node(component.id)
163169

164170
for input_topic in component.inputs:
165-
self.__add_input(input_topic.id, component.id)
171+
self.__add_input(input_topic.id, node)
166172

167173
for output_topic in component.outputs:
168-
self.__add_output(output_topic.id, component.id)
174+
self.__add_output(output_topic.id, node)
169175

170-
def __add_output(self, topic_id: str, source: str) -> None:
171-
self._graph.add_node(topic_id)
172-
self._graph.add_edge(source, topic_id)
176+
def __add_output(self, topic_id: str, source: int) -> None:
177+
topic = self.__get_or_add_node(topic_id)
178+
self._graph.add_edge(source, topic, None)
173179

174-
def __add_input(self, topic_id: str, target: str) -> None:
175-
self._graph.add_node(topic_id)
176-
self._graph.add_edge(topic_id, target)
180+
def __add_input(self, topic_id: str, target: int) -> None:
181+
topic = self.__get_or_add_node(topic_id)
182+
self._graph.add_edge(topic, target, None)
177183

178184
def __get_parallel_components_from(
179185
self, layer: list[str]
@@ -187,7 +193,7 @@ def gen_parallel_components() -> Iterator[PipelineComponent]:
187193
return list(gen_parallel_components())
188194

189195
def __validate_graph(self) -> None:
190-
if not nx.is_directed_acyclic_graph(self._graph):
196+
if not rx.is_directed_acyclic_graph(self._graph):
191197
msg = "Pipeline is not a valid DAG."
192198
raise ValueError(msg)
193199

pyproject.toml

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,36 @@
22
name = "kpops"
33
version = "10.9.0"
44
description = "KPOps is a tool to deploy Kafka pipelines to Kubernetes"
5+
readme = "README.md"
6+
requires-python = ">=3.11"
57
authors = [
68
{ name = "bakdata", email = "opensource@bakdata.com" },
79
]
8-
readme = "README.md"
9-
keywords = ["kafka", "kubernetes", "stream-processing", "pipelines"]
10+
keywords = ["kafka", "kubernetes", "pipelines", "stream-processing"]
1011
classifiers = [
1112
"Environment :: Console",
13+
"Intended Audience :: Developers",
14+
"License :: OSI Approved :: MIT License",
1215
"Operating System :: OS Independent",
13-
"Topic :: Software Development :: Libraries :: Python Modules",
14-
"Topic :: Software Development :: Libraries",
1516
"Topic :: Software Development",
16-
"Intended Audience :: Developers",
17+
"Topic :: Software Development :: Libraries",
18+
"Topic :: Software Development :: Libraries :: Python Modules",
1719
"Typing :: Typed",
18-
"License :: OSI Approved :: MIT License",
1920
]
20-
requires-python = ">=3.11"
2121
dependencies = [
2222
"anyio>=4.3.0",
2323
"cachetools>=5.2.0",
2424
"croniter>=3.0.3",
2525
"dictdiffer>=0.9.0",
2626
"httpx>=0.24.1",
2727
"lightkube>=0.15.3",
28-
"networkx>=3.1",
2928
"pydantic>=2.10.6",
3029
"pydantic-settings>=2.0.3",
3130
"pyhumps>=3.7.3",
3231
"python-schema-registry-client>=2.4.1",
3332
"pyyaml>=6.0",
3433
"rich>=12.4.4",
34+
"rustworkx>=0.18.0",
3535
"typer>=0.12.5",
3636
]
3737

@@ -40,9 +40,9 @@ kpops = "kpops.cli.main:app"
4040

4141
[dependency-groups]
4242
dev = [
43+
"basedpyright>=1.31.5",
4344
"lefthook>=1.10.4",
4445
"polyfactory>=2.13.0",
45-
"basedpyright>=1.31.5",
4646
"pytablewriter[from]>=1.0.0",
4747
"pytest>=8.3.2",
4848
"pytest-asyncio>=0.23.8",
@@ -54,31 +54,20 @@ dev = [
5454
"ruff>=0.9.9",
5555
]
5656
docs = [
57+
"mdx-truly-sane-lists>=1.3",
5758
"mike>=1.1.2",
5859
"mkdocs>=1.4.2",
5960
"mkdocs-exclude-search>=0.6.5",
6061
"mkdocs-glightbox>=0.3.1",
6162
"mkdocs-macros-plugin>=1.0.5",
6263
"mkdocs-material>=9.0.0",
6364
"mkdocstrings[python]>=0.25.1",
64-
"mdx-truly-sane-lists>=1.3",
6565
]
6666

67-
[tool.uv]
68-
package = true
69-
trusted-publishing = "always"
70-
7167
[build-system]
7268
requires = ["hatchling"]
7369
build-backend = "hatchling.build"
7470

75-
[tool.hatch.build.targets.wheel]
76-
packages = ["kpops"]
77-
78-
[tool.pytest.ini_options]
79-
asyncio_mode = "auto"
80-
asyncio_default_fixture_loop_scope = "function"
81-
8271
[tool.basedpyright]
8372
include = ["kpops", "tests", "hooks"]
8473
allowedUntypedLibraries = ["pytablewriter"]
@@ -114,6 +103,13 @@ strictParameterNoneValue = false
114103
# TODO: apply only to tests https://github.com/DetachHead/basedpyright/issues/668
115104
reportPrivateUsage = false
116105

106+
[tool.hatch.build.targets.wheel]
107+
packages = ["kpops"]
108+
109+
[tool.pytest.ini_options]
110+
asyncio_mode = "auto"
111+
asyncio_default_fixture_loop_scope = "function"
112+
117113
[tool.ruff]
118114
output-format = "grouped"
119115
show-fixes = true
@@ -219,3 +215,7 @@ extend-immutable-calls = ["typer.Argument"]
219215

220216
[tool.ruff.lint.flake8-type-checking]
221217
runtime-evaluated-base-classes = ["pydantic.BaseModel"]
218+
219+
[tool.uv]
220+
package = true
221+
trusted-publishing = "always"

tests/pipeline/test_generate.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -692,13 +692,15 @@ def test_validate_simple_graph(self):
692692
RESOURCE_PATH / "pipelines-with-graphs" / "simple-pipeline" / PIPELINE_YAML,
693693
)
694694
assert len(pipeline.components) == 2
695-
assert len(pipeline._graph.nodes) == 3
696-
assert len(pipeline._graph.edges) == 2
695+
assert len(pipeline._graph.nodes()) == 3
696+
assert len(pipeline._graph.edges()) == 2
697697
topic_nodes = [
698-
node for node in pipeline._graph.nodes if node.startswith("topic-")
698+
node for node in pipeline._graph.nodes() if node.startswith("topic-")
699699
]
700700
assert len(topic_nodes) == 1
701-
assert len(pipeline.components) == len(pipeline._graph.nodes) - len(topic_nodes)
701+
assert len(pipeline.components) == len(pipeline._graph.nodes()) - len(
702+
topic_nodes
703+
)
702704

703705
def test_validate_components_are_disabled_in_production_but_enabled_on_development(
704706
self,
@@ -719,27 +721,27 @@ def test_validate_components_are_disabled_in_production_but_enabled_on_developme
719721
)
720722

721723
assert len(pipeline_production.components) == 1
722-
assert len(pipeline_production._graph.edges) == 0
724+
assert len(pipeline_production._graph.edges()) == 0
723725
topic_nodes_production = [
724726
node
725-
for node in pipeline_production._graph.nodes
727+
for node in pipeline_production._graph.nodes()
726728
if node.startswith("topic-")
727729
]
728730
assert len(topic_nodes_production) == 0
729731
assert len(pipeline_production.components) == len(
730-
pipeline_production._graph.nodes
732+
pipeline_production._graph.nodes()
731733
) - len(topic_nodes_production)
732734

733-
assert len(pipeline_development._graph.nodes) == 3
734-
assert len(pipeline_development._graph.edges) == 2
735+
assert len(pipeline_development._graph.nodes()) == 3
736+
assert len(pipeline_development._graph.edges()) == 2
735737
topic_nodes_development = [
736738
node
737-
for node in pipeline_development._graph.nodes
739+
for node in pipeline_development._graph.nodes()
738740
if node.startswith("topic-")
739741
]
740742
assert len(topic_nodes_development) == 1
741743
assert len(pipeline_development.components) == len(
742-
pipeline_development._graph.nodes
744+
pipeline_development._graph.nodes()
743745
) - len(topic_nodes_development)
744746

745747
def test_validate_topic_and_component_same_name(self):
@@ -749,8 +751,11 @@ def test_validate_topic_and_component_same_name(self):
749751
/ "same-topic-and-component-name"
750752
/ PIPELINE_YAML,
751753
)
752-
component, topic = list(pipeline._graph.nodes)
753-
edges = list(pipeline._graph.edges)
754+
component, topic = list(pipeline._graph.nodes())
755+
edges = [
756+
(pipeline._graph[s], pipeline._graph[t])
757+
for s, t in pipeline._graph.edge_list()
758+
]
754759
assert component == topic.removeprefix("topic-")
755760
assert (component, topic) in edges
756761

0 commit comments

Comments
 (0)