scalable-pypeline 2.0.10__py2.py3-none-any.whl → 2.1.0__py2.py3-none-any.whl
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.
- pypeline/__init__.py +1 -1
- pypeline/barrier.py +3 -0
- pypeline/dramatiq.py +26 -154
- pypeline/flask/api/pipelines.py +60 -4
- pypeline/flask/api/schedules.py +1 -3
- pypeline/pipeline_config_schema.py +91 -3
- pypeline/pipeline_settings_schema.py +334 -0
- pypeline/pipelines/__init__.py +0 -0
- pypeline/pipelines/composition/__init__.py +0 -0
- pypeline/pipelines/composition/pypeline_composition.py +188 -0
- pypeline/pipelines/factory.py +107 -0
- pypeline/pipelines/middleware/__init__.py +0 -0
- pypeline/pipelines/middleware/pypeline_middleware.py +188 -0
- pypeline/utils/dramatiq_utils.py +126 -0
- pypeline/utils/module_utils.py +27 -2
- pypeline/utils/pipeline_utils.py +22 -37
- pypeline/utils/schema_utils.py +24 -0
- {scalable_pypeline-2.0.10.dist-info → scalable_pypeline-2.1.0.dist-info}/METADATA +1 -1
- scalable_pypeline-2.1.0.dist-info/RECORD +36 -0
- scalable_pypeline-2.0.10.dist-info/RECORD +0 -27
- /pypeline/{composition.py → pipelines/composition/parallel_pipeline_composition.py} +0 -0
- /pypeline/{middleware.py → pipelines/middleware/parallel_pipeline_middleware.py} +0 -0
- {scalable_pypeline-2.0.10.dist-info → scalable_pypeline-2.1.0.dist-info}/LICENSE +0 -0
- {scalable_pypeline-2.0.10.dist-info → scalable_pypeline-2.1.0.dist-info}/WHEEL +0 -0
- {scalable_pypeline-2.0.10.dist-info → scalable_pypeline-2.1.0.dist-info}/entry_points.txt +0 -0
- {scalable_pypeline-2.0.10.dist-info → scalable_pypeline-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,334 @@
|
|
1
|
+
from marshmallow import Schema, fields, validate, ValidationError, validates_schema
|
2
|
+
|
3
|
+
|
4
|
+
class MissingSettingsException(Exception):
|
5
|
+
pass
|
6
|
+
|
7
|
+
|
8
|
+
def create_pipeline_settings_schema(pipeline_settings_schema_data):
|
9
|
+
"""
|
10
|
+
Dynamically create a schema to validate user data based on settings.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
pipeline_settings_schema_data (dict): The settings schema data containing
|
14
|
+
field configurations.
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
Schema: A dynamically created schema class for validating user data.
|
18
|
+
"""
|
19
|
+
|
20
|
+
# Dictionary to store dynamically generated fields
|
21
|
+
schema_fields = {}
|
22
|
+
|
23
|
+
for key, config in pipeline_settings_schema_data["properties"].items():
|
24
|
+
data_type = config.get("dataType")
|
25
|
+
input_type = config.get("inputType")
|
26
|
+
field_args = {}
|
27
|
+
|
28
|
+
# Map dataType to Marshmallow field type
|
29
|
+
field_type = {
|
30
|
+
"string": fields.String,
|
31
|
+
"int": fields.Integer,
|
32
|
+
"float": fields.Float,
|
33
|
+
"boolean": fields.Boolean,
|
34
|
+
"datetime": fields.DateTime,
|
35
|
+
}.get(data_type)
|
36
|
+
|
37
|
+
if not field_type:
|
38
|
+
raise ValidationError(f"Unsupported dataType `{data_type}` for `{key}`.")
|
39
|
+
|
40
|
+
# Handle range validation for numeric fields
|
41
|
+
if data_type in ["int", "float"]:
|
42
|
+
if "minimum" in config or "maximum" in config:
|
43
|
+
field_args["validate"] = validate.Range(
|
44
|
+
min=config.get("minimum"), max=config.get("maximum")
|
45
|
+
)
|
46
|
+
|
47
|
+
# Handle dropdown or radio input options
|
48
|
+
if input_type in ["dropdown", "radio"] and "options" in config:
|
49
|
+
allowed_values = [option["value"] for option in config["options"]]
|
50
|
+
field_args["validate"] = validate.OneOf(allowed_values)
|
51
|
+
|
52
|
+
# Mark the field as required if specified
|
53
|
+
if key in pipeline_settings_schema_data.get("required", []):
|
54
|
+
field_args["required"] = True
|
55
|
+
|
56
|
+
# Create the field and add to the schema fields dictionary
|
57
|
+
schema_fields[key] = field_type(**field_args)
|
58
|
+
|
59
|
+
# Dynamically create a schema class with the generated fields
|
60
|
+
DynamicPipelineSettingsSchema = type(
|
61
|
+
"DynamicPipelineSettingsSchema", (Schema,), schema_fields
|
62
|
+
)
|
63
|
+
|
64
|
+
return DynamicPipelineSettingsSchema()
|
65
|
+
|
66
|
+
|
67
|
+
class OptionSchema(Schema):
|
68
|
+
label = fields.String(
|
69
|
+
required=True,
|
70
|
+
metadata={"description": "The display label for the option"},
|
71
|
+
)
|
72
|
+
value = fields.Raw(
|
73
|
+
required=True,
|
74
|
+
metadata={"description": "The value corresponding to the option"},
|
75
|
+
)
|
76
|
+
|
77
|
+
|
78
|
+
def validate_min_max(data):
|
79
|
+
"""Custom validator to ensure min/max match the dataType."""
|
80
|
+
data_type = data.get("dataType")
|
81
|
+
minimum = data.get("minimum")
|
82
|
+
maximum = data.get("maximum")
|
83
|
+
|
84
|
+
if data_type in ["int", "float"]:
|
85
|
+
if minimum is not None and not isinstance(
|
86
|
+
minimum, (int if data_type == "int" else float)
|
87
|
+
):
|
88
|
+
raise ValidationError(f"`minimum` must be of type {data_type}.")
|
89
|
+
if maximum is not None and not isinstance(
|
90
|
+
maximum, (int if data_type == "int" else float)
|
91
|
+
):
|
92
|
+
raise ValidationError(f"`maximum` must be of type {data_type}.")
|
93
|
+
if minimum is not None and maximum is not None and minimum > maximum:
|
94
|
+
raise ValidationError("`minimum` must be less than or equal to `maximum`.")
|
95
|
+
elif data_type not in ["int", "float"] and (
|
96
|
+
minimum is not None or maximum is not None
|
97
|
+
):
|
98
|
+
raise ValidationError(
|
99
|
+
"`minimum` and `maximum` are only valid for numeric types (`int`, `float`)."
|
100
|
+
)
|
101
|
+
|
102
|
+
|
103
|
+
class SettingSchema(Schema):
|
104
|
+
dataType = fields.String(
|
105
|
+
required=True,
|
106
|
+
validate=validate.OneOf(["string", "int", "float", "boolean", "datetime"]),
|
107
|
+
metadata={"description": "The underlying data type of the setting"},
|
108
|
+
)
|
109
|
+
inputType = fields.String(
|
110
|
+
required=True,
|
111
|
+
validate=validate.OneOf(
|
112
|
+
["text", "dropdown", "radio", "checkbox", "searchable"]
|
113
|
+
),
|
114
|
+
metadata={"description": "The type of input UI element"},
|
115
|
+
)
|
116
|
+
label = fields.String(
|
117
|
+
required=True,
|
118
|
+
metadata={"description": "The display label for the field"},
|
119
|
+
)
|
120
|
+
placeholder = fields.String(
|
121
|
+
metadata={"description": "Placeholder text for text input fields"}
|
122
|
+
)
|
123
|
+
minimum = fields.Raw(
|
124
|
+
metadata={"description": "Minimum value for numeric data types"}
|
125
|
+
)
|
126
|
+
maximum = fields.Raw(
|
127
|
+
metadata={"description": "Maximum value for numeric data types"}
|
128
|
+
)
|
129
|
+
options = fields.List(
|
130
|
+
fields.Nested(OptionSchema),
|
131
|
+
metadata={"description": "Options for dropdown or radio input types"},
|
132
|
+
)
|
133
|
+
searchEndpoint = fields.String(
|
134
|
+
metadata={"description": "Endpoint for searchable fields"}
|
135
|
+
)
|
136
|
+
|
137
|
+
class Meta:
|
138
|
+
ordered = True
|
139
|
+
|
140
|
+
@validates_schema
|
141
|
+
def validate_min_max(self, data, **kwargs):
|
142
|
+
validate_min_max(data)
|
143
|
+
|
144
|
+
@validates_schema
|
145
|
+
def validate_options(self, data, **kwargs):
|
146
|
+
"""Ensure options are provided for dropdown or radio input types and validate value types."""
|
147
|
+
input_type = data.get("inputType")
|
148
|
+
options = data.get("options")
|
149
|
+
data_type = data.get("dataType")
|
150
|
+
|
151
|
+
if input_type in ["dropdown", "radio"]:
|
152
|
+
if not options:
|
153
|
+
raise ValidationError(
|
154
|
+
"`options` are required for dropdown and radio input types.",
|
155
|
+
field_name="options",
|
156
|
+
)
|
157
|
+
|
158
|
+
for option in options:
|
159
|
+
value = option.get("value")
|
160
|
+
if data_type == "int" and not isinstance(value, int):
|
161
|
+
raise ValidationError(
|
162
|
+
f"Option value `{value}` must be of type `int`."
|
163
|
+
)
|
164
|
+
elif data_type == "float" and not isinstance(value, float):
|
165
|
+
raise ValidationError(
|
166
|
+
f"Option value `{value}` must be of type `float`."
|
167
|
+
)
|
168
|
+
elif data_type == "boolean" and not isinstance(value, bool):
|
169
|
+
raise ValidationError(
|
170
|
+
f"Option value `{value}` must be of type `boolean`."
|
171
|
+
)
|
172
|
+
elif data_type == "string" and not isinstance(value, str):
|
173
|
+
raise ValidationError(
|
174
|
+
f"Option value `{value}` must be of type `string`."
|
175
|
+
)
|
176
|
+
elif data_type == "datetime" and not isinstance(
|
177
|
+
value, str
|
178
|
+
): # Assuming ISO 8601 strings
|
179
|
+
raise ValidationError(
|
180
|
+
f"Option value `{value}` must be an ISO 8601 string for `datetime`."
|
181
|
+
)
|
182
|
+
|
183
|
+
@validates_schema
|
184
|
+
def validate_search_endpoint(self, data, **kwargs):
|
185
|
+
"""Ensure searchEndpoint is provided only for 'searchable' input types."""
|
186
|
+
input_type = data.get("inputType")
|
187
|
+
search_endpoint = data.get("searchEndpoint")
|
188
|
+
|
189
|
+
if input_type == "searchable" and not search_endpoint:
|
190
|
+
raise ValidationError(
|
191
|
+
"`searchEndpoint` is required for `searchable` input types.",
|
192
|
+
field_name="searchEndpoint",
|
193
|
+
)
|
194
|
+
elif input_type != "searchable" and search_endpoint:
|
195
|
+
raise ValidationError(
|
196
|
+
"`searchEndpoint` is not allowed for non-searchable input types.",
|
197
|
+
field_name="searchEndpoint",
|
198
|
+
)
|
199
|
+
|
200
|
+
|
201
|
+
class PipelineSettingsSchema(Schema):
|
202
|
+
properties = fields.Dict(
|
203
|
+
keys=fields.String(),
|
204
|
+
values=fields.Nested(SettingSchema),
|
205
|
+
required=True,
|
206
|
+
metadata={"description": "A dictionary of settings with their configurations"},
|
207
|
+
)
|
208
|
+
required = fields.List(
|
209
|
+
fields.String(), required=True, description="List of required settings"
|
210
|
+
)
|
211
|
+
scenarioSettings = fields.List(
|
212
|
+
fields.String(),
|
213
|
+
required=False,
|
214
|
+
description="List of settings that can be overriding for different pipeline scenarios.",
|
215
|
+
)
|
216
|
+
|
217
|
+
@validates_schema
|
218
|
+
def validate_scenario_settings(self, data, **kwargs):
|
219
|
+
"""Ensure scenarioSettings only contains keys defined in properties."""
|
220
|
+
properties = data.get("properties", {})
|
221
|
+
scenario_settings = data.get("scenarioSettings", [])
|
222
|
+
|
223
|
+
invalid_settings = [
|
224
|
+
setting for setting in scenario_settings if setting not in properties
|
225
|
+
]
|
226
|
+
if invalid_settings:
|
227
|
+
raise ValidationError(
|
228
|
+
{
|
229
|
+
"scenario_settings": (
|
230
|
+
f"The following settings in scenarioSettings are not defined "
|
231
|
+
f"in properties: {', '.join(invalid_settings)}"
|
232
|
+
)
|
233
|
+
}
|
234
|
+
)
|
235
|
+
|
236
|
+
|
237
|
+
class PipelineScenarioSchema(Schema):
|
238
|
+
settings = fields.Dict(
|
239
|
+
required=True,
|
240
|
+
metadata={
|
241
|
+
"description": "Settings to be used for a given scenario. Should match the pypeline.yaml settings schema"
|
242
|
+
},
|
243
|
+
)
|
244
|
+
taskReplacements = fields.Dict(
|
245
|
+
keys=fields.String(),
|
246
|
+
values=fields.Integer(),
|
247
|
+
required=False,
|
248
|
+
metadata={
|
249
|
+
"description": "Tasks that should be replaced in a given scenario. "
|
250
|
+
"The key corresponds to the task definition in the pypeline.yaml and the value corresponds "
|
251
|
+
"to the index of the task handlers where 0 is the default and first task. Eg: {'a': 1}. In this case "
|
252
|
+
"if we have a task definition 'a' with 3 handlers fn_1, fn_2, fn_3 respectively then the handler to run "
|
253
|
+
"for 'a' is fn_2."
|
254
|
+
},
|
255
|
+
)
|
256
|
+
|
257
|
+
taskReruns = fields.List(
|
258
|
+
fields.String(),
|
259
|
+
required=False,
|
260
|
+
metadata={
|
261
|
+
"description": "List of task definitions that need to be run again for a given scenario. Here "
|
262
|
+
"the scenario's pipeline settings will be injected in the task being run again which could be used to "
|
263
|
+
"produce alternative calculations and or results."
|
264
|
+
},
|
265
|
+
)
|
266
|
+
|
267
|
+
|
268
|
+
class PipelineScenariosSchema(Schema):
|
269
|
+
required = fields.List(
|
270
|
+
fields.Nested(PipelineScenarioSchema),
|
271
|
+
metadata={"description": "List of scenarios to run for a given pipeline"},
|
272
|
+
)
|
273
|
+
|
274
|
+
|
275
|
+
# Example usage
|
276
|
+
if __name__ == "__main__":
|
277
|
+
pipeline_settings = {"param1": "test", "param2": 1}
|
278
|
+
|
279
|
+
yaml_data = {
|
280
|
+
"properties": {
|
281
|
+
"param1": {
|
282
|
+
"dataType": "string",
|
283
|
+
"inputType": "text",
|
284
|
+
"label": "Parameter 1",
|
285
|
+
"placeholder": "Enter a string",
|
286
|
+
},
|
287
|
+
"param2": {
|
288
|
+
"dataType": "int",
|
289
|
+
"inputType": "text",
|
290
|
+
"label": "Parameter 2",
|
291
|
+
"minimum": 1,
|
292
|
+
"maximum": -1,
|
293
|
+
},
|
294
|
+
"param3": {
|
295
|
+
"dataType": "boolean",
|
296
|
+
"inputType": "checkbox",
|
297
|
+
"label": "Enable Feature",
|
298
|
+
},
|
299
|
+
"param4": {
|
300
|
+
"dataType": "float",
|
301
|
+
"inputType": "dropdown",
|
302
|
+
"label": "Choose an Option",
|
303
|
+
"minimum": 0.5,
|
304
|
+
"maximum": 2.5,
|
305
|
+
"options": [
|
306
|
+
{"label": "Option 1", "value": 0.5},
|
307
|
+
{"label": "Option 2", "value": 1.5},
|
308
|
+
],
|
309
|
+
},
|
310
|
+
"param5": {
|
311
|
+
"dataType": "int",
|
312
|
+
"inputType": "radio",
|
313
|
+
"label": "Select a Mode",
|
314
|
+
"options": [
|
315
|
+
{"label": "Mode A", "value": 1},
|
316
|
+
{"label": "Mode B", "value": 2},
|
317
|
+
],
|
318
|
+
},
|
319
|
+
"param6": {
|
320
|
+
"dataType": "string",
|
321
|
+
"inputType": "searchable",
|
322
|
+
"label": "Select Pipeline",
|
323
|
+
"searchEndpoint": "/api/pipelines",
|
324
|
+
},
|
325
|
+
},
|
326
|
+
"required": ["param1", "param2", "param4"],
|
327
|
+
}
|
328
|
+
|
329
|
+
schema = PipelineSettingsSchema()
|
330
|
+
errors = schema.validate(yaml_data)
|
331
|
+
if errors:
|
332
|
+
print("Validation errors:", errors)
|
333
|
+
else:
|
334
|
+
print("Validation successful!")
|
File without changes
|
File without changes
|
@@ -0,0 +1,188 @@
|
|
1
|
+
import json
|
2
|
+
import typing
|
3
|
+
from copy import copy
|
4
|
+
from uuid import uuid4
|
5
|
+
|
6
|
+
import networkx as nx
|
7
|
+
from dramatiq import get_broker
|
8
|
+
|
9
|
+
from pypeline.barrier import LockingParallelBarrier
|
10
|
+
from pypeline.constants import REDIS_URL, PARALLEL_PIPELINE_CALLBACK_BARRIER_TTL
|
11
|
+
from pypeline.utils.dramatiq_utils import register_lazy_actor
|
12
|
+
from pypeline.utils.module_utils import get_callable
|
13
|
+
from pypeline.utils.pipeline_utils import get_execution_graph
|
14
|
+
|
15
|
+
|
16
|
+
class Pypeline:
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
pipeline: dict,
|
20
|
+
pipeline_settings: dict = None,
|
21
|
+
task_replacements: dict = {},
|
22
|
+
scenarios: dict = {},
|
23
|
+
broker=None,
|
24
|
+
execution_id=None,
|
25
|
+
):
|
26
|
+
# Construct initial properties
|
27
|
+
self.pipeline = pipeline
|
28
|
+
self.broker = broker or get_broker()
|
29
|
+
self.execution_id = execution_id or str(uuid4())
|
30
|
+
self._starting_messages = []
|
31
|
+
self.scenarios = scenarios
|
32
|
+
self.pipeline_settings = pipeline_settings
|
33
|
+
self.task_replacements = task_replacements
|
34
|
+
|
35
|
+
# Get pipeline dag graph and find first task
|
36
|
+
pipeline_config = pipeline["config"]
|
37
|
+
self.graph = get_execution_graph(pipeline_config)
|
38
|
+
self.number_of_tasks = len(self.graph.nodes)
|
39
|
+
task_definitions = pipeline_config["taskDefinitions"]
|
40
|
+
first_task = list(pipeline_config["dagAdjacency"].keys())[0]
|
41
|
+
|
42
|
+
# Process the scenarios one by one
|
43
|
+
for scenario in self.scenarios:
|
44
|
+
tasks_in_reruns = scenario["taskReruns"]
|
45
|
+
|
46
|
+
# Find any tasks that have replacements for this scenario
|
47
|
+
tasks_in_replacements = list(scenario["taskReplacements"].keys())
|
48
|
+
|
49
|
+
distinct_scenario_tasks = list(set(tasks_in_reruns + tasks_in_replacements))
|
50
|
+
tasks_to_be_rerun_in_scenario = distinct_scenario_tasks
|
51
|
+
|
52
|
+
tasks_to_be_rerun_in_scenario = list(
|
53
|
+
set(
|
54
|
+
task
|
55
|
+
for task in distinct_scenario_tasks
|
56
|
+
for task in nx.descendants(self.graph, task)
|
57
|
+
)
|
58
|
+
| set(tasks_to_be_rerun_in_scenario)
|
59
|
+
)
|
60
|
+
|
61
|
+
self.number_of_tasks = self.number_of_tasks + len(
|
62
|
+
tasks_to_be_rerun_in_scenario
|
63
|
+
)
|
64
|
+
scenario["tasksToRunInScenario"] = tasks_to_be_rerun_in_scenario
|
65
|
+
scenario["execution_id"] = scenario.get("execution_id", None) or str(
|
66
|
+
uuid4()
|
67
|
+
)
|
68
|
+
|
69
|
+
# Check if any of the scenarios need to be kicked off now
|
70
|
+
if first_task in tasks_to_be_rerun_in_scenario:
|
71
|
+
handler = task_definitions[first_task]["handlers"][
|
72
|
+
scenario["taskReplacements"].get(first_task, 0)
|
73
|
+
]
|
74
|
+
lazy_actor = register_lazy_actor(
|
75
|
+
self.broker,
|
76
|
+
get_callable(handler),
|
77
|
+
pipeline_config["metadata"],
|
78
|
+
)
|
79
|
+
message = lazy_actor.message()
|
80
|
+
message.options["pipeline"] = pipeline
|
81
|
+
message.options["task_replacements"] = self.task_replacements
|
82
|
+
message.options["execution_id"] = scenario["execution_id"]
|
83
|
+
message.options["task_name"] = first_task
|
84
|
+
message.options["root_execution_id"] = self.execution_id
|
85
|
+
if self.pipeline_settings:
|
86
|
+
message.kwargs["settings"] = copy(self.pipeline_settings)
|
87
|
+
message.kwargs["settings"]["execution_id"] = scenario[
|
88
|
+
"execution_id"
|
89
|
+
]
|
90
|
+
self._starting_messages.append(message)
|
91
|
+
|
92
|
+
for m in self._starting_messages:
|
93
|
+
m.options["scenarios"] = self.scenarios
|
94
|
+
|
95
|
+
handler = task_definitions[first_task]["handlers"][
|
96
|
+
self.task_replacements.get(first_task, 0)
|
97
|
+
]
|
98
|
+
lazy_actor = register_lazy_actor(
|
99
|
+
self.broker,
|
100
|
+
get_callable(handler),
|
101
|
+
pipeline_config["metadata"],
|
102
|
+
)
|
103
|
+
message = lazy_actor.message()
|
104
|
+
message.options["pipeline"] = pipeline
|
105
|
+
message.options["task_replacements"] = self.task_replacements
|
106
|
+
message.options["execution_id"] = self.execution_id
|
107
|
+
message.options["task_name"] = first_task
|
108
|
+
message.options["scenarios"] = self.scenarios
|
109
|
+
message.options["root_execution_id"] = self.execution_id
|
110
|
+
|
111
|
+
if self.pipeline_settings:
|
112
|
+
message.kwargs["settings"] = copy(self.pipeline_settings)
|
113
|
+
message.kwargs["settings"]["execution_id"] = self.execution_id
|
114
|
+
|
115
|
+
self._starting_messages.append(message)
|
116
|
+
|
117
|
+
def run(self, *, delay=None):
|
118
|
+
for message in self._starting_messages:
|
119
|
+
task_key = (
|
120
|
+
f"{message.options['execution_id']}-{message.options['task_name']}"
|
121
|
+
)
|
122
|
+
locking_parallel_barrier = LockingParallelBarrier(
|
123
|
+
REDIS_URL, task_key=task_key, lock_key=f"{self.execution_id}-lock"
|
124
|
+
)
|
125
|
+
locking_parallel_barrier.set_task_count(1)
|
126
|
+
self.broker.enqueue(message, delay=delay)
|
127
|
+
|
128
|
+
return self
|
129
|
+
|
130
|
+
def __len__(self):
|
131
|
+
return self.number_of_tasks
|
132
|
+
|
133
|
+
def completed(self):
|
134
|
+
redis_task_keys = [
|
135
|
+
f"{self.execution_id}-{node}" for node in list(self.graph.nodes)
|
136
|
+
]
|
137
|
+
redis_lock_key = f"{self.execution_id}-lock"
|
138
|
+
for scenario in self.scenarios:
|
139
|
+
scenario_task_keys = [
|
140
|
+
f"{scenario['execution_id']}-{task}"
|
141
|
+
for task in scenario["tasksToRunInScenario"]
|
142
|
+
]
|
143
|
+
redis_task_keys = redis_task_keys + scenario_task_keys
|
144
|
+
|
145
|
+
for task_key in redis_task_keys:
|
146
|
+
locking_parallel_barrier = LockingParallelBarrier(
|
147
|
+
REDIS_URL, task_key=task_key, lock_key=redis_lock_key
|
148
|
+
)
|
149
|
+
try:
|
150
|
+
locking_parallel_barrier.acquire_lock(
|
151
|
+
timeout=PARALLEL_PIPELINE_CALLBACK_BARRIER_TTL
|
152
|
+
)
|
153
|
+
task_complete = True
|
154
|
+
if locking_parallel_barrier.task_exists():
|
155
|
+
remaining_tasks = locking_parallel_barrier.get_task_count()
|
156
|
+
if remaining_tasks >= 1:
|
157
|
+
task_complete = False
|
158
|
+
else:
|
159
|
+
task_complete = False
|
160
|
+
finally:
|
161
|
+
locking_parallel_barrier.release_lock()
|
162
|
+
if not task_complete:
|
163
|
+
return task_complete
|
164
|
+
|
165
|
+
return True
|
166
|
+
|
167
|
+
def to_json(self) -> str:
|
168
|
+
return json.dumps(
|
169
|
+
{
|
170
|
+
"pipeline": self.pipeline,
|
171
|
+
"pipeline_settings": self.pipeline_settings,
|
172
|
+
"task_replacements": self.task_replacements,
|
173
|
+
"scenarios": self.scenarios,
|
174
|
+
"execution_id": self.execution_id,
|
175
|
+
}
|
176
|
+
)
|
177
|
+
|
178
|
+
@classmethod
|
179
|
+
def from_json(cls, json_data: str) -> typing.Type["Pypeline"]:
|
180
|
+
data = json.loads(json_data)
|
181
|
+
|
182
|
+
return cls(
|
183
|
+
data["pipeline"],
|
184
|
+
pipeline_settings=data["pipeline_settings"],
|
185
|
+
task_replacements=data["task_replacements"],
|
186
|
+
scenarios=data["scenarios"],
|
187
|
+
execution_id=data["execution_id"],
|
188
|
+
)
|
@@ -0,0 +1,107 @@
|
|
1
|
+
import typing
|
2
|
+
from dramatiq import get_broker, Message
|
3
|
+
from pypeline.pipelines.composition.parallel_pipeline_composition import (
|
4
|
+
parallel_pipeline,
|
5
|
+
)
|
6
|
+
from pypeline.dramatiq import LazyActor
|
7
|
+
from pypeline.utils.dramatiq_utils import register_lazy_actor
|
8
|
+
from pypeline.pipeline_settings_schema import (
|
9
|
+
MissingSettingsException,
|
10
|
+
create_pipeline_settings_schema,
|
11
|
+
PipelineScenarioSchema,
|
12
|
+
)
|
13
|
+
from pypeline.pipelines.composition.pypeline_composition import Pypeline
|
14
|
+
from pypeline.utils.config_utils import retrieve_latest_pipeline_config
|
15
|
+
from pypeline.utils.module_utils import get_callable
|
16
|
+
from pypeline.utils.pipeline_utils import (
|
17
|
+
get_execution_graph,
|
18
|
+
topological_sort_with_parallelism,
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
def dag_generator(
|
23
|
+
pipeline_id: str,
|
24
|
+
task_replacements: dict = {},
|
25
|
+
scenarios: typing.List[typing.Dict] = [],
|
26
|
+
*args,
|
27
|
+
**kwargs
|
28
|
+
) -> typing.Union[parallel_pipeline, Pypeline]:
|
29
|
+
"""Generates a pipeline dag from a pre-defined pipeline yaml
|
30
|
+
|
31
|
+
:param pipeline_id: Id of the pipeline to generate
|
32
|
+
:param task_replacements: A dictionary of task names and handler index to run. E.g. {"a": 1} would run the handler
|
33
|
+
in the second index position.
|
34
|
+
:param scenarios:
|
35
|
+
:param args:
|
36
|
+
:param kwargs:
|
37
|
+
:return: Returns a parallel_pipeline object which can be run
|
38
|
+
"""
|
39
|
+
pipeline = retrieve_latest_pipeline_config(pipeline_id=pipeline_id)
|
40
|
+
|
41
|
+
pipeline_config = pipeline["config"]
|
42
|
+
broker = get_broker()
|
43
|
+
broker.actors.clear()
|
44
|
+
|
45
|
+
if pipeline["schemaVersion"] == 2:
|
46
|
+
# If the pipeline_config expects settings ensure we have them
|
47
|
+
if (
|
48
|
+
"settings" in pipeline_config
|
49
|
+
and len(pipeline_config["settings"]["required"]) > 0
|
50
|
+
and "settings" not in kwargs
|
51
|
+
):
|
52
|
+
raise MissingSettingsException()
|
53
|
+
|
54
|
+
# If we're here we expect to have settings. Pop them out of kwargs to validate
|
55
|
+
inputted_settings = kwargs.pop("settings", {})
|
56
|
+
if "settings" in pipeline_config:
|
57
|
+
supplied_pipeline_settings_schema = create_pipeline_settings_schema(
|
58
|
+
pipeline_config["settings"]
|
59
|
+
)
|
60
|
+
|
61
|
+
# Validate scenarios settings to make sure they look okay
|
62
|
+
validated_scenarios = PipelineScenarioSchema(many=True).load(scenarios)
|
63
|
+
|
64
|
+
for scenario in validated_scenarios:
|
65
|
+
supplied_pipeline_settings_schema.load(scenario["settings"])
|
66
|
+
|
67
|
+
validated_settings = supplied_pipeline_settings_schema.load(
|
68
|
+
inputted_settings
|
69
|
+
)
|
70
|
+
p = Pypeline(
|
71
|
+
pipeline,
|
72
|
+
pipeline_settings=validated_settings,
|
73
|
+
task_replacements=task_replacements,
|
74
|
+
scenarios=scenarios,
|
75
|
+
broker=broker,
|
76
|
+
)
|
77
|
+
else:
|
78
|
+
p = Pypeline(pipeline, task_replacements=task_replacements, broker=broker)
|
79
|
+
return p
|
80
|
+
graph = get_execution_graph(pipeline_config)
|
81
|
+
optimal_execution_graph = topological_sort_with_parallelism(graph.copy())
|
82
|
+
registered_actors: typing.Dict[str, LazyActor] = {}
|
83
|
+
|
84
|
+
messages: typing.List[typing.List[Message]] = []
|
85
|
+
|
86
|
+
task_definitions = pipeline_config["taskDefinitions"]
|
87
|
+
for task_group in optimal_execution_graph:
|
88
|
+
message_group = []
|
89
|
+
for task in task_group:
|
90
|
+
module_path = task_definitions[task]["handler"]
|
91
|
+
tmp_handler = get_callable(module_path)
|
92
|
+
lazy_actor = register_lazy_actor(
|
93
|
+
broker, tmp_handler, pipeline_config["metadata"]
|
94
|
+
)
|
95
|
+
registered_actors[task] = lazy_actor
|
96
|
+
if args and not kwargs:
|
97
|
+
message_group.append(registered_actors[task].message(*args))
|
98
|
+
elif kwargs and not args:
|
99
|
+
message_group.append(registered_actors[task].message(**kwargs))
|
100
|
+
elif args and kwargs:
|
101
|
+
message_group.append(registered_actors[task].message(*args, **kwargs))
|
102
|
+
else:
|
103
|
+
message_group.append(registered_actors[task].message())
|
104
|
+
messages.append(message_group)
|
105
|
+
p = parallel_pipeline(messages)
|
106
|
+
|
107
|
+
return p
|
File without changes
|