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.
- {kpops-3.2.0 → kpops-3.2.2}/PKG-INFO +1 -1
- {kpops-3.2.0 → kpops-3.2.2}/kpops/__init__.py +1 -1
- {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/main.py +2 -2
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/base_defaults_component.py +61 -9
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/kafka_app.py +6 -2
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/kafka_connector.py +12 -2
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/producer/producer_app.py +3 -2
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/streams/streams_app.py +3 -2
- {kpops-3.2.0 → kpops-3.2.2}/kpops/pipeline.py +52 -104
- {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/gen_schema.py +1 -3
- {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/yaml.py +35 -9
- {kpops-3.2.0 → kpops-3.2.2}/pyproject.toml +1 -1
- {kpops-3.2.0 → kpops-3.2.2}/LICENSE +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/README.md +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/custom_formatter.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/exception.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/options.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/cli/registry.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/dry_run_handler.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/exception.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/helm.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/helm_diff.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/model.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/helm_wrapper/utils.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/connect_wrapper.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/exception.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/kafka_connect_handler.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/model.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kafka_connect/timeout.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kubernetes/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/kubernetes/model.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/schema_handler/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/schema_handler/schema_handler.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/schema_handler/schema_provider.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/exception.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/handler.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/model.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/proxy_wrapper.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/topic/utils.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/utils/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/component_handlers/utils/exception.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/helm_app.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/kubernetes_app.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/models/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/models/from_section.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/models/resource.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/models/to_section.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/base_components/pipeline_component.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/app_type.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/producer/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/producer/model.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/streams/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/components/streams_bootstrap/streams/model.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/config.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/__init__.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/colorify.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/dict_differ.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/dict_ops.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/docstring.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/environment.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/pydantic.py +0 -0
- {kpops-3.2.0 → kpops-3.2.2}/kpops/utils/types.py +0 -0
|
@@ -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
|
|
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.
|
|
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=
|
|
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=
|
|
79
|
+
default=False,
|
|
74
80
|
description=describe_attr("validate", __doc__),
|
|
75
81
|
exclude=True,
|
|
76
82
|
)
|
|
77
83
|
|
|
78
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
19
|
+
from kpops.utils.dict_ops import update_nested_pair
|
|
16
20
|
from kpops.utils.environment import ENV
|
|
17
|
-
from kpops.utils.
|
|
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
|
-
|
|
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
|
-
|
|
124
|
+
root_node = "root_node_bfs"
|
|
125
|
+
graph.add_node(root_node)
|
|
125
126
|
|
|
126
|
-
for node in
|
|
127
|
-
predecessors = list(
|
|
127
|
+
for node in graph:
|
|
128
|
+
predecessors = list(graph.predecessors(node))
|
|
128
129
|
if not predecessors:
|
|
129
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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.
|
|
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.
|
|
331
|
+
component = self.enrich_component_with_env(component)
|
|
346
332
|
# inflate & enrich components
|
|
347
333
|
for inflated_component in component.inflate(): # TODO: recursively
|
|
348
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
367
|
-
self.pipeline.add(
|
|
351
|
+
inflated_component.weave_from_topics(prev_component.to)
|
|
352
|
+
self.pipeline.add(inflated_component)
|
|
368
353
|
|
|
369
|
-
def
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
366
|
+
env_component,
|
|
381
367
|
component.model_dump(mode="json", by_alias=True),
|
|
382
368
|
)
|
|
383
369
|
|
|
384
|
-
|
|
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
|
-
**
|
|
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"]
|
|
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
|
|
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
|
-
|
|
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
|
|
|
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
|
|
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
|
|
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
|
|
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
|