kpops 3.2.0__tar.gz → 3.2.2__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 (70) hide show
  1. {kpops-3.2.0 → kpops-3.2.2}/PKG-INFO +1 -1
  2. {kpops-3.2.0 → kpops-3.2.2}/kpops/__init__.py +1 -1
  3. {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/main.py +2 -2
  4. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/base_defaults_component.py +61 -9
  5. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/kafka_app.py +6 -2
  6. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/kafka_connector.py +12 -2
  7. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/producer/producer_app.py +3 -2
  8. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/streams/streams_app.py +3 -2
  9. {kpops-3.2.0 → kpops-3.2.2}/kpops/pipeline.py +52 -104
  10. {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/gen_schema.py +1 -3
  11. {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/yaml.py +35 -9
  12. {kpops-3.2.0 → kpops-3.2.2}/pyproject.toml +1 -1
  13. {kpops-3.2.0 → kpops-3.2.2}/LICENSE +0 -0
  14. {kpops-3.2.0 → kpops-3.2.2}/README.md +0 -0
  15. {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/__init__.py +0 -0
  16. {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/custom_formatter.py +0 -0
  17. {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/exception.py +0 -0
  18. {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/options.py +0 -0
  19. {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/registry.py +0 -0
  20. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/__init__.py +0 -0
  21. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/__init__.py +0 -0
  22. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/dry_run_handler.py +0 -0
  23. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/exception.py +0 -0
  24. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/helm.py +0 -0
  25. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/helm_diff.py +0 -0
  26. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/model.py +0 -0
  27. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/utils.py +0 -0
  28. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/__init__.py +0 -0
  29. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/connect_wrapper.py +0 -0
  30. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/exception.py +0 -0
  31. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/kafka_connect_handler.py +0 -0
  32. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/model.py +0 -0
  33. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/timeout.py +0 -0
  34. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kubernetes/__init__.py +0 -0
  35. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kubernetes/model.py +0 -0
  36. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/schema_handler/__init__.py +0 -0
  37. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/schema_handler/schema_handler.py +0 -0
  38. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/schema_handler/schema_provider.py +0 -0
  39. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/__init__.py +0 -0
  40. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/exception.py +0 -0
  41. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/handler.py +0 -0
  42. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/model.py +0 -0
  43. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/proxy_wrapper.py +0 -0
  44. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/utils.py +0 -0
  45. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/utils/__init__.py +0 -0
  46. {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/utils/exception.py +0 -0
  47. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/__init__.py +0 -0
  48. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/__init__.py +0 -0
  49. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/helm_app.py +0 -0
  50. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/kubernetes_app.py +0 -0
  51. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/models/__init__.py +0 -0
  52. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/models/from_section.py +0 -0
  53. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/models/resource.py +0 -0
  54. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/models/to_section.py +0 -0
  55. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/pipeline_component.py +0 -0
  56. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/__init__.py +0 -0
  57. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/app_type.py +0 -0
  58. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/producer/__init__.py +0 -0
  59. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/producer/model.py +0 -0
  60. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/streams/__init__.py +0 -0
  61. {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/streams/model.py +0 -0
  62. {kpops-3.2.0 → kpops-3.2.2}/kpops/config.py +0 -0
  63. {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/__init__.py +0 -0
  64. {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/colorify.py +0 -0
  65. {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/dict_differ.py +0 -0
  66. {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/dict_ops.py +0 -0
  67. {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/docstring.py +0 -0
  68. {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/environment.py +0 -0
  69. {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/pydantic.py +0 -0
  70. {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kpops
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Summary: KPOps is a tool to deploy Kafka pipelines to Kubernetes
5
5
  Home-page: https://github.com/bakdata/kpops
6
6
  License: MIT
@@ -1,4 +1,4 @@
1
- __version__ = "3.2.0"
1
+ __version__ = "3.2.2"
2
2
 
3
3
  # export public API functions
4
4
  from kpops.cli.main import clean, deploy, destroy, generate, manifest, reset
@@ -440,7 +440,7 @@ def reset(
440
440
  pipeline_tasks = pipeline.build_execution_graph(reset_runner, reverse=True)
441
441
  await pipeline_tasks
442
442
  else:
443
- for component in pipeline.components:
443
+ for component in reversed(pipeline.components):
444
444
  await reset_runner(component)
445
445
 
446
446
  asyncio.run(async_reset())
@@ -481,7 +481,7 @@ def clean(
481
481
  pipeline_tasks = pipeline.build_execution_graph(clean_runner, reverse=True)
482
482
  await pipeline_tasks
483
483
  else:
484
- for component in pipeline.components:
484
+ for component in reversed(pipeline.components):
485
485
  await clean_runner(component)
486
486
 
487
487
  asyncio.run(async_clean())
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import logging
4
5
  from abc import ABC
5
6
  from collections.abc import Sequence
@@ -21,11 +22,16 @@ from pydantic.json_schema import SkipJsonSchema
21
22
  from kpops.component_handlers import ComponentHandlers
22
23
  from kpops.config import KpopsConfig
23
24
  from kpops.utils import cached_classproperty
24
- from kpops.utils.dict_ops import update_nested, update_nested_pair
25
+ from kpops.utils.dict_ops import (
26
+ generate_substitution,
27
+ update_nested,
28
+ update_nested_pair,
29
+ )
25
30
  from kpops.utils.docstring import describe_attr
26
31
  from kpops.utils.environment import ENV
27
32
  from kpops.utils.pydantic import DescConfigModel, issubclass_patched, to_dash
28
- from kpops.utils.yaml import load_yaml_file
33
+ from kpops.utils.types import JsonType
34
+ from kpops.utils.yaml import load_yaml_file, substitute_nested
29
35
 
30
36
  try:
31
37
  from typing import Self
@@ -54,7 +60,7 @@ class BaseDefaultsComponent(DescConfigModel, ABC):
54
60
  )
55
61
 
56
62
  enrich: SkipJsonSchema[bool] = Field(
57
- default=False,
63
+ default=True,
58
64
  description=describe_attr("enrich", __doc__),
59
65
  exclude=True,
60
66
  )
@@ -70,21 +76,31 @@ class BaseDefaultsComponent(DescConfigModel, ABC):
70
76
  )
71
77
  validate_: SkipJsonSchema[bool] = Field(
72
78
  validation_alias=AliasChoices("validate", "validate_"),
73
- default=True,
79
+ default=False,
74
80
  description=describe_attr("validate", __doc__),
75
81
  exclude=True,
76
82
  )
77
83
 
78
- @pydantic.model_validator(mode="before")
79
- @classmethod
80
- def enrich_component(cls, values: dict[str, Any]) -> dict[str, Any]:
84
+ def __init__(self, **values: Any) -> None:
81
85
  if values.get("enrich", True):
86
+ cls = self.__class__
82
87
  values = cls.extend_with_defaults(**values)
83
- return values
88
+ tmp_self = cls(**values, enrich=False)
89
+ values = tmp_self.model_dump(mode="json", by_alias=True)
90
+ values = cls.substitute_in_component(tmp_self.config, **values)
91
+ self.__init__(
92
+ enrich=False,
93
+ validate=True,
94
+ config=tmp_self.config,
95
+ handlers=tmp_self.handlers,
96
+ **values,
97
+ )
98
+ else:
99
+ super().__init__(**values)
84
100
 
85
101
  @pydantic.model_validator(mode="after")
86
102
  def validate_component(self) -> Self:
87
- if self.validate_:
103
+ if not self.enrich and self.validate_:
88
104
  self._validate_custom()
89
105
  return self
90
106
 
@@ -113,6 +129,42 @@ class BaseDefaultsComponent(DescConfigModel, ABC):
113
129
 
114
130
  return tuple(gen_parents())
115
131
 
132
+ @classmethod
133
+ def substitute_in_component(
134
+ cls, config: KpopsConfig, **component_data: Any
135
+ ) -> dict[str, Any]:
136
+ """Substitute all $-placeholders in a component in dict representation.
137
+
138
+ :param component_as_dict: Component represented as dict
139
+ :return: Updated component
140
+ """
141
+ # Leftover variables that were previously introduced in the component by the substitution
142
+ # functions, still hardcoded, because of their names.
143
+ # TODO(Ivan Yordanov): Get rid of them
144
+ substitution_hardcoded: dict[str, JsonType] = {
145
+ "error_topic_name": config.topic_name_config.default_error_topic_name,
146
+ "output_topic_name": config.topic_name_config.default_output_topic_name,
147
+ }
148
+ component_substitution = generate_substitution(
149
+ component_data,
150
+ "component",
151
+ substitution_hardcoded,
152
+ separator=".",
153
+ )
154
+ substitution = generate_substitution(
155
+ config.model_dump(mode="json"),
156
+ "config",
157
+ existing_substitution=component_substitution,
158
+ separator=".",
159
+ )
160
+
161
+ return json.loads(
162
+ substitute_nested(
163
+ json.dumps(component_data),
164
+ **update_nested_pair(substitution, ENV),
165
+ )
166
+ )
167
+
116
168
  @classmethod
117
169
  def extend_with_defaults(cls, config: KpopsConfig, **kwargs: Any) -> dict[str, Any]:
118
170
  """Merge parent components' defaults with own.
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
  from abc import ABC
5
5
 
6
- from pydantic import ConfigDict, Field
6
+ from pydantic import AliasChoices, ConfigDict, Field
7
7
  from typing_extensions import override
8
8
 
9
9
  from kpops.component_handlers.helm_wrapper.model import (
@@ -28,7 +28,11 @@ class KafkaStreamsConfig(CamelCaseConfigModel, DescConfigModel):
28
28
 
29
29
  brokers: str = Field(default=..., description=describe_attr("brokers", __doc__))
30
30
  schema_registry_url: str | None = Field(
31
- default=None, description=describe_attr("schema_registry_url", __doc__)
31
+ default=None,
32
+ validation_alias=AliasChoices(
33
+ "schema_registry_url", "schemaRegistryUrl"
34
+ ), # TODO: same for other camelcase fields, avoids duplicates during enrichment
35
+ description=describe_attr("schema_registry_url", __doc__),
32
36
  )
33
37
 
34
38
  model_config = ConfigDict(
@@ -148,7 +148,6 @@ class KafkaConnector(PipelineComponent, ABC):
148
148
  app["name"] = component_name
149
149
  return KafkaConnectorConfig(**app)
150
150
 
151
- @computed_field
152
151
  @cached_property
153
152
  def _resetter(self) -> KafkaConnectorResetter:
154
153
  kwargs: dict[str, Any] = {}
@@ -159,7 +158,8 @@ class KafkaConnector(PipelineComponent, ABC):
159
158
  handlers=self.handlers,
160
159
  **kwargs,
161
160
  **self.model_dump(
162
- exclude={"_resetter", "resetter_values", "resetter_namespace", "app"}
161
+ by_alias=True,
162
+ exclude={"_resetter", "resetter_values", "resetter_namespace", "app"},
163
163
  ),
164
164
  app=KafkaConnectorResetterValues(
165
165
  connector_type=self._connector_type.value,
@@ -218,6 +218,11 @@ class KafkaSourceConnector(KafkaConnector):
218
218
 
219
219
  _connector_type: KafkaConnectorType = PrivateAttr(KafkaConnectorType.SOURCE)
220
220
 
221
+ @computed_field
222
+ @cached_property
223
+ def _resetter(self) -> KafkaConnectorResetter:
224
+ return super()._resetter
225
+
221
226
  @override
222
227
  def apply_from_inputs(self, name: str, topic: FromTopic) -> NoReturn:
223
228
  msg = "Kafka source connector doesn't support FromSection"
@@ -240,6 +245,11 @@ class KafkaSinkConnector(KafkaConnector):
240
245
 
241
246
  _connector_type: KafkaConnectorType = PrivateAttr(KafkaConnectorType.SINK)
242
247
 
248
+ @computed_field
249
+ @cached_property
250
+ def _resetter(self) -> KafkaConnectorResetter:
251
+ return super()._resetter
252
+
243
253
  @property
244
254
  @override
245
255
  def input_topics(self) -> list[str]:
@@ -1,6 +1,6 @@
1
1
  from functools import cached_property
2
2
 
3
- from pydantic import Field
3
+ from pydantic import Field, computed_field
4
4
  from typing_extensions import override
5
5
 
6
6
  from kpops.components.base_components.kafka_app import (
@@ -51,12 +51,13 @@ class ProducerApp(KafkaApp, StreamsBootstrap):
51
51
  description=describe_attr("from_", __doc__),
52
52
  )
53
53
 
54
+ @computed_field
54
55
  @cached_property
55
56
  def _cleaner(self) -> ProducerAppCleaner:
56
57
  return ProducerAppCleaner(
57
58
  config=self.config,
58
59
  handlers=self.handlers,
59
- **self.model_dump(),
60
+ **self.model_dump(by_alias=True, exclude={"_cleaner"}),
60
61
  )
61
62
 
62
63
  @override
@@ -1,6 +1,6 @@
1
1
  from functools import cached_property
2
2
 
3
- from pydantic import Field
3
+ from pydantic import Field, computed_field
4
4
  from typing_extensions import override
5
5
 
6
6
  from kpops.components.base_components.kafka_app import (
@@ -33,12 +33,13 @@ class StreamsApp(KafkaApp, StreamsBootstrap):
33
33
  description=describe_attr("app", __doc__),
34
34
  )
35
35
 
36
+ @computed_field
36
37
  @cached_property
37
38
  def _cleaner(self) -> StreamsAppCleaner:
38
39
  return StreamsAppCleaner(
39
40
  config=self.config,
40
41
  handlers=self.handlers,
41
- **self.model_dump(),
42
+ **self.model_dump(by_alias=True, exclude={"_cleaner"}),
42
43
  )
43
44
 
44
45
  @property
@@ -1,21 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import json
5
4
  import logging
6
- from collections.abc import Callable, Iterable
5
+ from collections.abc import Callable
7
6
  from dataclasses import dataclass, field
8
- from typing import TYPE_CHECKING, TypeAlias
7
+ from typing import TYPE_CHECKING, Any, TypeAlias
9
8
 
10
9
  import networkx as nx
11
10
  import yaml
12
- from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny, computed_field
11
+ from pydantic import (
12
+ BaseModel,
13
+ ConfigDict,
14
+ SerializeAsAny,
15
+ computed_field,
16
+ )
13
17
 
14
18
  from kpops.components.base_components.pipeline_component import PipelineComponent
15
- from kpops.utils.dict_ops import generate_substitution, update_nested_pair
19
+ from kpops.utils.dict_ops import update_nested_pair
16
20
  from kpops.utils.environment import ENV
17
- from kpops.utils.types import JsonType
18
- from kpops.utils.yaml import load_yaml_file, substitute_nested
21
+ from kpops.utils.yaml import load_yaml_file
19
22
 
20
23
  if TYPE_CHECKING:
21
24
  from collections.abc import Awaitable, Coroutine, Iterator
@@ -42,8 +45,8 @@ ComponentFilterPredicate: TypeAlias = Callable[[PipelineComponent], bool]
42
45
  class Pipeline(BaseModel):
43
46
  """Pipeline representation."""
44
47
 
45
- graph: nx.DiGraph = Field(default_factory=nx.DiGraph, exclude=True)
46
48
  _component_index: dict[str, PipelineComponent] = {}
49
+ _graph: nx.DiGraph = nx.DiGraph()
47
50
 
48
51
  model_config = ConfigDict(arbitrary_types_allowed=True)
49
52
 
@@ -93,6 +96,9 @@ class Pipeline(BaseModel):
93
96
  if not predicate(component):
94
97
  self.remove(component.id)
95
98
 
99
+ def validate(self) -> None:
100
+ self.__validate_graph()
101
+
96
102
  def to_yaml(self) -> str:
97
103
  return yaml.dump(
98
104
  self.model_dump(mode="json", by_alias=True, exclude_none=True)["components"]
@@ -101,42 +107,33 @@ class Pipeline(BaseModel):
101
107
  def build_execution_graph(
102
108
  self, runner: Callable[[PipelineComponent], Coroutine], /, reverse: bool = False
103
109
  ) -> Awaitable:
104
- sub_graph_nodes = self.__collect_graph_nodes(
105
- reversed(self.components) if reverse else self.components
106
- )
107
-
108
110
  async def run_parallel_tasks(coroutines: list[Coroutine]) -> None:
109
111
  tasks = []
110
112
  for coro in coroutines:
111
113
  tasks.append(asyncio.create_task(coro))
112
114
  await asyncio.gather(*tasks)
113
115
 
114
- async def run_graph_tasks(pending_tasks: list[Awaitable]):
116
+ async def run_graph_tasks(pending_tasks: list[Awaitable]) -> None:
115
117
  for pending_task in pending_tasks:
116
118
  await pending_task
117
119
 
118
- sub_graph = self.graph.subgraph(sub_graph_nodes)
119
- transformed_graph = sub_graph.copy()
120
+ graph: nx.DiGraph = self._graph.copy() # pyright: ignore[reportAssignmentType, reportGeneralTypeIssues] imprecise type hint in networkx
120
121
 
121
- root_node = "root_node_bfs"
122
122
  # We add an extra node to the graph, connecting all the leaf nodes to it
123
123
  # in that way we make this node the root of the graph, avoiding backtracking
124
- transformed_graph.add_node(root_node)
124
+ root_node = "root_node_bfs"
125
+ graph.add_node(root_node)
125
126
 
126
- for node in sub_graph:
127
- predecessors = list(sub_graph.predecessors(node))
127
+ for node in graph:
128
+ predecessors = list(graph.predecessors(node))
128
129
  if not predecessors:
129
- transformed_graph.add_edge(root_node, node)
130
+ graph.add_edge(root_node, node)
130
131
 
131
- layers_graph: list[list[str]] = list(
132
- nx.bfs_layers(transformed_graph, root_node)
133
- )
132
+ layers_graph: list[list[str]] = list(nx.bfs_layers(graph, root_node))
134
133
 
135
134
  sorted_tasks = []
136
135
  for layer in layers_graph[1:]:
137
- parallel_tasks = self.__get_parallel_tasks_from(layer, runner)
138
-
139
- if parallel_tasks:
136
+ if parallel_tasks := self.__get_parallel_tasks_from(layer, runner):
140
137
  sorted_tasks.append(run_parallel_tasks(parallel_tasks))
141
138
 
142
139
  if reverse:
@@ -161,7 +158,7 @@ class Pipeline(BaseModel):
161
158
  return len(self.components)
162
159
 
163
160
  def __add_to_graph(self, component: PipelineComponent):
164
- self.graph.add_node(component.id)
161
+ self._graph.add_node(component.id)
165
162
 
166
163
  for input_topic in component.inputs:
167
164
  self.__add_input(input_topic, component.id)
@@ -169,12 +166,13 @@ class Pipeline(BaseModel):
169
166
  for output_topic in component.outputs:
170
167
  self.__add_output(output_topic, component.id)
171
168
 
172
- @staticmethod
173
- def __collect_graph_nodes(components: Iterable[PipelineComponent]) -> Iterator[str]:
174
- for component in components:
175
- yield component.id
176
- yield from component.inputs
177
- yield from component.outputs
169
+ def __add_output(self, topic_id: str, source: str) -> None:
170
+ self._graph.add_node(topic_id)
171
+ self._graph.add_edge(source, topic_id)
172
+
173
+ def __add_input(self, topic_id: str, target: str) -> None:
174
+ self._graph.add_node(topic_id)
175
+ self._graph.add_edge(topic_id, target)
178
176
 
179
177
  def __get_parallel_tasks_from(
180
178
  self, layer: list[str], runner: Callable[[PipelineComponent], Coroutine]
@@ -188,36 +186,25 @@ class Pipeline(BaseModel):
188
186
  return list(gen_parallel_tasks())
189
187
 
190
188
  def __validate_graph(self) -> None:
191
- if not nx.is_directed_acyclic_graph(self.graph):
189
+ if not nx.is_directed_acyclic_graph(self._graph):
192
190
  msg = "Pipeline is not a valid DAG."
193
191
  raise ValueError(msg)
194
192
 
195
- def validate(self) -> None:
196
- self.__validate_graph()
197
-
198
- def __add_output(self, topic_id: str, source: str) -> None:
199
- self.graph.add_node(topic_id)
200
- self.graph.add_edge(source, topic_id)
201
-
202
- def __add_input(self, topic_id: str, target: str) -> None:
203
- self.graph.add_node(topic_id)
204
- self.graph.add_edge(topic_id, target)
205
-
206
193
 
207
194
  def create_env_components_index(
208
- environment_components: list[dict],
209
- ) -> dict[str, dict]:
195
+ environment_components: list[dict[str, Any]],
196
+ ) -> dict[str, dict[str, Any]]:
210
197
  """Create an index for all registered components in the project.
211
198
 
212
199
  :param environment_components: List of all components to be included
213
200
  :return: component index
214
201
  """
215
- index: dict[str, dict] = {}
202
+ index: dict[str, dict[str, Any]] = {}
216
203
  for component in environment_components:
217
204
  if "type" not in component or "name" not in component:
218
205
  msg = "To override components per environment, every component should at least have a type and a name."
219
206
  raise ValueError(msg)
220
- index[component["name"]] = component
207
+ index[component["name"]] = component # TODO: id
221
208
  return index
222
209
 
223
210
 
@@ -305,7 +292,7 @@ class PipelineGenerator:
305
292
  raise ParsingException from ex
306
293
 
307
294
  def apply_component(
308
- self, component_class: type[PipelineComponent], component_data: dict
295
+ self, component_class: type[PipelineComponent], component_data: dict[str, Any]
309
296
  ) -> None:
310
297
  """Instantiate, enrich and inflate pipeline component.
311
298
 
@@ -339,90 +326,51 @@ class PipelineGenerator:
339
326
  component = component_class(
340
327
  config=self.config,
341
328
  handlers=self.handlers,
342
- validate=False,
343
329
  **component_data,
344
330
  )
345
- component = self.enrich_component(component)
331
+ component = self.enrich_component_with_env(component)
346
332
  # inflate & enrich components
347
333
  for inflated_component in component.inflate(): # TODO: recursively
348
- enriched_component = self.enrich_component(inflated_component)
349
- if enriched_component.from_:
334
+ if inflated_component.from_:
350
335
  # read from specified components
351
336
  for (
352
337
  original_from_component_name,
353
338
  from_topic,
354
- ) in enriched_component.from_.components.items():
339
+ ) in inflated_component.from_.components.items():
355
340
  original_from_component = find(original_from_component_name)
356
341
 
357
342
  inflated_from_component = original_from_component.inflate()[-1]
358
343
  resolved_from_component = find(inflated_from_component.name)
359
344
 
360
- enriched_component.weave_from_topics(
345
+ inflated_component.weave_from_topics(
361
346
  resolved_from_component.to, from_topic
362
347
  )
363
348
  elif self.pipeline:
364
349
  # read from previous component
365
350
  prev_component = self.pipeline.last
366
- enriched_component.weave_from_topics(prev_component.to)
367
- self.pipeline.add(enriched_component)
351
+ inflated_component.weave_from_topics(prev_component.to)
352
+ self.pipeline.add(inflated_component)
368
353
 
369
- def enrich_component(
370
- self,
371
- component: PipelineComponent,
354
+ def enrich_component_with_env(
355
+ self, component: PipelineComponent
372
356
  ) -> PipelineComponent:
373
- """Enrich a pipeline component with env-specific config and substitute variables.
357
+ """Enrich a pipeline component with env-specific config.
374
358
 
375
359
  :param component: Component to be enriched
376
360
  :returns: Enriched component
377
361
  """
378
- component.validate_ = True
362
+ env_component = self.env_components_index.get(component.name)
363
+ if not env_component:
364
+ return component
379
365
  env_component_as_dict = update_nested_pair(
380
- self.env_components_index.get(component.name, {}),
366
+ env_component,
381
367
  component.model_dump(mode="json", by_alias=True),
382
368
  )
383
369
 
384
- component_data = self.substitute_in_component(env_component_as_dict)
385
-
386
- component_class = type(component)
387
- return component_class(
388
- enrich=False,
370
+ return component.__class__(
389
371
  config=self.config,
390
372
  handlers=self.handlers,
391
- **component_data,
392
- )
393
-
394
- def substitute_in_component(self, component_as_dict: dict) -> dict:
395
- """Substitute all $-placeholders in a component in dict representation.
396
-
397
- :param component_as_dict: Component represented as dict
398
- :return: Updated component
399
- """
400
- config = self.config
401
- # Leftover variables that were previously introduced in the component by the substitution
402
- # functions, still hardcoded, because of their names.
403
- # TODO(Ivan Yordanov): Get rid of them
404
- substitution_hardcoded: dict[str, JsonType] = {
405
- "error_topic_name": config.topic_name_config.default_error_topic_name,
406
- "output_topic_name": config.topic_name_config.default_output_topic_name,
407
- }
408
- component_substitution = generate_substitution(
409
- component_as_dict,
410
- "component",
411
- substitution_hardcoded,
412
- separator=".",
413
- )
414
- substitution = generate_substitution(
415
- config.model_dump(mode="json"),
416
- "config",
417
- existing_substitution=component_substitution,
418
- separator=".",
419
- )
420
-
421
- return json.loads(
422
- substitute_nested(
423
- json.dumps(component_as_dict),
424
- **update_nested_pair(substitution, ENV),
425
- )
373
+ **env_component_as_dict,
426
374
  )
427
375
 
428
376
  @staticmethod
@@ -138,9 +138,7 @@ def gen_pipeline_schema(
138
138
  )
139
139
  core_schema: DefinitionsSchema = component.__pydantic_core_schema__ # pyright:ignore[reportGeneralTypeIssues]
140
140
 
141
- model_schema: ModelFieldsSchema = core_schema["schema"]["schema"]["schema"][ # pyright:ignore[reportGeneralTypeIssues,reportTypedDictNotRequiredAccess]
142
- "schema"
143
- ]
141
+ model_schema: ModelFieldsSchema = core_schema["schema"]["schema"]["schema"] # pyright:ignore[reportGeneralTypeIssues,reportTypedDictNotRequiredAccess]
144
142
  model_schema["fields"]["type"] = ModelField(
145
143
  type="model-field",
146
144
  schema=LiteralSchema(
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from collections.abc import Mapping
2
3
  from pathlib import Path
3
4
  from typing import Any
@@ -44,14 +45,22 @@ def substitute(input: str, substitution: Mapping[str, Any] | None = None) -> str
44
45
  return ImprovedTemplate(input).safe_substitute(**prepare_substitution(substitution))
45
46
 
46
47
 
48
+ def _diff_substituted_str(s1: str, s2: str):
49
+ """Compare 2 strings, raise exception if not equal.
50
+
51
+ :param s1: String to compare
52
+ :param s2: String to compare
53
+ :raises ValueError: An infinite loop condition detected. Check substitution variables.
54
+ """
55
+ if s1 != s2:
56
+ msg = "An infinite loop condition detected. Check substitution variables."
57
+ raise ValueError(msg)
58
+
59
+
47
60
  def substitute_nested(input: str, **kwargs) -> str:
48
61
  """Allow for multiple substitutions to be passed.
49
62
 
50
63
  Will make as many passes as needed to substitute all possible placeholders.
51
- A ceiling is set to avoid infinite loops.
52
-
53
- HINT: If :param input: is a ``Mapping`` that you converted into ``str``,
54
- You can pass it as a string, and as a ``Mapping`` to enable self-reference.
55
64
 
56
65
  :Example:
57
66
 
@@ -63,26 +72,43 @@ def substitute_nested(input: str, **kwargs) -> str:
63
72
  }
64
73
  >>> input = "${a}, ${b}, ${c}, ${d}"
65
74
  >>> print("Substituted string: " + substitute_nested(input, **substitution))
66
- 0, 0, 0, 0
75
+ "0, 0, 0, 0"
67
76
 
68
77
  :param input: The raw input containing $-placeholders
69
78
  :param **kwargs: Substitutions
70
- :raises Exception: An infinite loop condition detected. Check substitution variables.
79
+ :raises ValueError: An infinite loop condition detected. Check substitution variables.
71
80
  :return: Substituted input string
72
81
  """
73
82
  if not kwargs:
74
83
  return input
84
+ kwargs = substitute_in_self(kwargs)
75
85
  old_str, new_str = "", substitute(input, kwargs)
76
86
  steps = set()
77
87
  while new_str not in steps:
78
88
  steps.add(new_str)
79
89
  old_str, new_str = new_str, substitute(new_str, kwargs)
80
- if new_str != old_str:
81
- msg = "An infinite loop condition detected. Check substitution variables."
82
- raise ValueError(msg)
90
+ _diff_substituted_str(new_str, old_str)
83
91
  return old_str
84
92
 
85
93
 
94
+ def substitute_in_self(input: dict[str, Any]) -> dict[str, Any]:
95
+ """Substitute all self-references in mapping.
96
+
97
+ Will make as many passes as needed to substitute all possible placeholders.
98
+
99
+ :param input: Mapping containing $-placeholders
100
+ :raises ValueError: An infinite loop condition detected. Check substitution variables.
101
+ :return: Substituted input mapping as dict
102
+ """
103
+ old_str, new_str = "", substitute(json.dumps(input), input)
104
+ steps = set()
105
+ while new_str not in steps:
106
+ steps.add(new_str)
107
+ old_str, new_str = new_str, substitute(new_str, json.loads(new_str))
108
+ _diff_substituted_str(new_str, old_str)
109
+ return json.loads(old_str)
110
+
111
+
86
112
  def print_yaml(data: Mapping | str, *, substitution: dict | None = None) -> None:
87
113
  """Print YAML object with syntax highlighting.
88
114
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "kpops"
3
- version = "3.2.0"
3
+ version = "3.2.2"
4
4
  description = "KPOps is a tool to deploy Kafka pipelines to Kubernetes"
5
5
  authors = ["bakdata <opensource@bakdata.com>"]
6
6
  license = "MIT"
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes