metaflow 2.17.1__py2.py3-none-any.whl → 2.17.3__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.
- metaflow/cli_components/run_cmds.py +15 -0
- metaflow/flowspec.py +91 -1
- metaflow/graph.py +152 -13
- metaflow/lint.py +66 -3
- metaflow/plugins/argo/argo_workflows.py +233 -9
- metaflow/plugins/argo/argo_workflows_decorator.py +9 -0
- metaflow/plugins/argo/conditional_input_paths.py +21 -0
- metaflow/plugins/aws/step_functions/step_functions.py +6 -0
- metaflow/plugins/cards/card_modules/basic.py +14 -2
- metaflow/plugins/cards/card_modules/main.css +1 -0
- metaflow/plugins/cards/card_modules/main.js +28 -28
- metaflow/plugins/catch_decorator.py +9 -0
- metaflow/plugins/parallel_decorator.py +7 -0
- metaflow/runtime.py +57 -14
- metaflow/task.py +62 -34
- metaflow/user_decorators/user_step_decorator.py +24 -5
- metaflow/version.py +1 -1
- {metaflow-2.17.1.dist-info → metaflow-2.17.3.dist-info}/METADATA +2 -2
- {metaflow-2.17.1.dist-info → metaflow-2.17.3.dist-info}/RECORD +26 -24
- {metaflow-2.17.1.data → metaflow-2.17.3.data}/data/share/metaflow/devtools/Makefile +0 -0
- {metaflow-2.17.1.data → metaflow-2.17.3.data}/data/share/metaflow/devtools/Tiltfile +0 -0
- {metaflow-2.17.1.data → metaflow-2.17.3.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
- {metaflow-2.17.1.dist-info → metaflow-2.17.3.dist-info}/WHEEL +0 -0
- {metaflow-2.17.1.dist-info → metaflow-2.17.3.dist-info}/entry_points.txt +0 -0
- {metaflow-2.17.1.dist-info → metaflow-2.17.3.dist-info}/licenses/LICENSE +0 -0
- {metaflow-2.17.1.dist-info → metaflow-2.17.3.dist-info}/top_level.txt +0 -0
@@ -143,6 +143,7 @@ class ArgoWorkflows(object):
|
|
143
143
|
|
144
144
|
self.name = name
|
145
145
|
self.graph = graph
|
146
|
+
self._parse_conditional_branches()
|
146
147
|
self.flow = flow
|
147
148
|
self.code_package_metadata = code_package_metadata
|
148
149
|
self.code_package_sha = code_package_sha
|
@@ -920,6 +921,121 @@ class ArgoWorkflows(object):
|
|
920
921
|
)
|
921
922
|
)
|
922
923
|
|
924
|
+
# Visit every node and record information on conditional step structure
|
925
|
+
def _parse_conditional_branches(self):
|
926
|
+
self.conditional_nodes = set()
|
927
|
+
self.conditional_join_nodes = set()
|
928
|
+
self.matching_conditional_join_dict = {}
|
929
|
+
|
930
|
+
node_conditional_parents = {}
|
931
|
+
node_conditional_branches = {}
|
932
|
+
|
933
|
+
def _visit(node, seen, conditional_branch, conditional_parents=None):
|
934
|
+
if not node.type == "split-switch" and not (
|
935
|
+
conditional_branch and conditional_parents
|
936
|
+
):
|
937
|
+
# skip regular non-conditional nodes entirely
|
938
|
+
return
|
939
|
+
|
940
|
+
if node.type == "split-switch":
|
941
|
+
conditional_branch = conditional_branch + [node.name]
|
942
|
+
node_conditional_branches[node.name] = conditional_branch
|
943
|
+
|
944
|
+
conditional_parents = (
|
945
|
+
[node.name]
|
946
|
+
if not conditional_parents
|
947
|
+
else conditional_parents + [node.name]
|
948
|
+
)
|
949
|
+
node_conditional_parents[node.name] = conditional_parents
|
950
|
+
|
951
|
+
if conditional_parents and not node.type == "split-switch":
|
952
|
+
node_conditional_parents[node.name] = conditional_parents
|
953
|
+
conditional_branch = conditional_branch + [node.name]
|
954
|
+
node_conditional_branches[node.name] = conditional_branch
|
955
|
+
|
956
|
+
self.conditional_nodes.add(node.name)
|
957
|
+
|
958
|
+
if conditional_branch and conditional_parents:
|
959
|
+
for n in node.out_funcs:
|
960
|
+
child = self.graph[n]
|
961
|
+
if n not in seen:
|
962
|
+
_visit(
|
963
|
+
child, seen + [n], conditional_branch, conditional_parents
|
964
|
+
)
|
965
|
+
|
966
|
+
# First we visit all nodes to determine conditional parents and branches
|
967
|
+
for n in self.graph:
|
968
|
+
_visit(n, [], [])
|
969
|
+
|
970
|
+
# Then we traverse again in order to determine conditional join nodes, and matching conditional join info
|
971
|
+
for node in self.graph:
|
972
|
+
if node_conditional_parents.get(node.name, False):
|
973
|
+
# do the required postprocessing for anything requiring node.in_funcs
|
974
|
+
|
975
|
+
# check that in previous parsing we have not closed all conditional in_funcs.
|
976
|
+
# If so, this step can not be conditional either
|
977
|
+
is_conditional = any(
|
978
|
+
in_func in self.conditional_nodes
|
979
|
+
or self.graph[in_func].type == "split-switch"
|
980
|
+
for in_func in node.in_funcs
|
981
|
+
)
|
982
|
+
if is_conditional:
|
983
|
+
self.conditional_nodes.add(node.name)
|
984
|
+
else:
|
985
|
+
if node.name in self.conditional_nodes:
|
986
|
+
self.conditional_nodes.remove(node.name)
|
987
|
+
|
988
|
+
# does this node close the latest conditional parent branches?
|
989
|
+
conditional_in_funcs = [
|
990
|
+
in_func
|
991
|
+
for in_func in node.in_funcs
|
992
|
+
if node_conditional_branches.get(in_func, False)
|
993
|
+
]
|
994
|
+
closed_conditional_parents = []
|
995
|
+
for last_split_switch in node_conditional_parents.get(node.name, [])[
|
996
|
+
::-1
|
997
|
+
]:
|
998
|
+
last_conditional_split_nodes = self.graph[
|
999
|
+
last_split_switch
|
1000
|
+
].out_funcs
|
1001
|
+
# p needs to be in at least one conditional_branch for it to be closed.
|
1002
|
+
if all(
|
1003
|
+
any(
|
1004
|
+
p in node_conditional_branches.get(in_func, [])
|
1005
|
+
for in_func in conditional_in_funcs
|
1006
|
+
)
|
1007
|
+
for p in last_conditional_split_nodes
|
1008
|
+
):
|
1009
|
+
closed_conditional_parents.append(last_split_switch)
|
1010
|
+
|
1011
|
+
self.conditional_join_nodes.add(node.name)
|
1012
|
+
self.matching_conditional_join_dict[last_split_switch] = (
|
1013
|
+
node.name
|
1014
|
+
)
|
1015
|
+
|
1016
|
+
# Did we close all conditionals? Then this branch and all its children are not conditional anymore (unless a new conditional branch is encountered).
|
1017
|
+
if not [
|
1018
|
+
p
|
1019
|
+
for p in node_conditional_parents.get(node.name, [])
|
1020
|
+
if p not in closed_conditional_parents
|
1021
|
+
]:
|
1022
|
+
if node.name in self.conditional_nodes:
|
1023
|
+
self.conditional_nodes.remove(node.name)
|
1024
|
+
node_conditional_parents[node.name] = []
|
1025
|
+
for p in node.out_funcs:
|
1026
|
+
if p in self.conditional_nodes:
|
1027
|
+
self.conditional_nodes.remove(p)
|
1028
|
+
node_conditional_parents[p] = []
|
1029
|
+
|
1030
|
+
def _is_conditional_node(self, node):
|
1031
|
+
return node.name in self.conditional_nodes
|
1032
|
+
|
1033
|
+
def _is_conditional_join_node(self, node):
|
1034
|
+
return node.name in self.conditional_join_nodes
|
1035
|
+
|
1036
|
+
def _matching_conditional_join(self, node):
|
1037
|
+
return self.matching_conditional_join_dict.get(node.name, None)
|
1038
|
+
|
923
1039
|
# Visit every node and yield the uber DAGTemplate(s).
|
924
1040
|
def _dag_templates(self):
|
925
1041
|
def _visit(
|
@@ -941,6 +1057,7 @@ class ArgoWorkflows(object):
|
|
941
1057
|
dag_tasks = []
|
942
1058
|
if templates is None:
|
943
1059
|
templates = []
|
1060
|
+
|
944
1061
|
if exit_node is not None and exit_node is node.name:
|
945
1062
|
return templates, dag_tasks
|
946
1063
|
if node.name == "start":
|
@@ -948,7 +1065,7 @@ class ArgoWorkflows(object):
|
|
948
1065
|
dag_task = DAGTask(self._sanitize(node.name)).template(
|
949
1066
|
self._sanitize(node.name)
|
950
1067
|
)
|
951
|
-
|
1068
|
+
if (
|
952
1069
|
node.is_inside_foreach
|
953
1070
|
and self.graph[node.in_funcs[0]].type == "foreach"
|
954
1071
|
and not self.graph[node.in_funcs[0]].parallel_foreach
|
@@ -1082,15 +1199,43 @@ class ArgoWorkflows(object):
|
|
1082
1199
|
]
|
1083
1200
|
)
|
1084
1201
|
|
1202
|
+
conditional_deps = [
|
1203
|
+
"%s.Succeeded" % self._sanitize(in_func)
|
1204
|
+
for in_func in node.in_funcs
|
1205
|
+
if self._is_conditional_node(self.graph[in_func])
|
1206
|
+
]
|
1207
|
+
required_deps = [
|
1208
|
+
"%s.Succeeded" % self._sanitize(in_func)
|
1209
|
+
for in_func in node.in_funcs
|
1210
|
+
if not self._is_conditional_node(self.graph[in_func])
|
1211
|
+
]
|
1212
|
+
both_conditions = required_deps and conditional_deps
|
1213
|
+
|
1214
|
+
depends_str = "{required}{_and}{conditional}".format(
|
1215
|
+
required=("(%s)" if both_conditions else "%s")
|
1216
|
+
% " && ".join(required_deps),
|
1217
|
+
_and=" && " if both_conditions else "",
|
1218
|
+
conditional=("(%s)" if both_conditions else "%s")
|
1219
|
+
% " || ".join(conditional_deps),
|
1220
|
+
)
|
1085
1221
|
dag_task = (
|
1086
1222
|
DAGTask(self._sanitize(node.name))
|
1087
|
-
.
|
1088
|
-
[self._sanitize(in_func) for in_func in node.in_funcs]
|
1089
|
-
)
|
1223
|
+
.depends(depends_str)
|
1090
1224
|
.template(self._sanitize(node.name))
|
1091
1225
|
.arguments(Arguments().parameters(parameters))
|
1092
1226
|
)
|
1093
1227
|
|
1228
|
+
# Add conditional if this is the first step in a conditional branch
|
1229
|
+
if (
|
1230
|
+
self._is_conditional_node(node)
|
1231
|
+
and self.graph[node.in_funcs[0]].type == "split-switch"
|
1232
|
+
):
|
1233
|
+
in_func = node.in_funcs[0]
|
1234
|
+
dag_task.when(
|
1235
|
+
"{{tasks.%s.outputs.parameters.switch-step}}==%s"
|
1236
|
+
% (self._sanitize(in_func), node.name)
|
1237
|
+
)
|
1238
|
+
|
1094
1239
|
dag_tasks.append(dag_task)
|
1095
1240
|
# End the workflow if we have reached the end of the flow
|
1096
1241
|
if node.type == "end":
|
@@ -1116,6 +1261,23 @@ class ArgoWorkflows(object):
|
|
1116
1261
|
dag_tasks,
|
1117
1262
|
parent_foreach,
|
1118
1263
|
)
|
1264
|
+
elif node.type == "split-switch":
|
1265
|
+
for n in node.out_funcs:
|
1266
|
+
_visit(
|
1267
|
+
self.graph[n],
|
1268
|
+
self._matching_conditional_join(node),
|
1269
|
+
templates,
|
1270
|
+
dag_tasks,
|
1271
|
+
parent_foreach,
|
1272
|
+
)
|
1273
|
+
|
1274
|
+
return _visit(
|
1275
|
+
self.graph[self._matching_conditional_join(node)],
|
1276
|
+
exit_node,
|
1277
|
+
templates,
|
1278
|
+
dag_tasks,
|
1279
|
+
parent_foreach,
|
1280
|
+
)
|
1119
1281
|
# For foreach nodes generate a new sub DAGTemplate
|
1120
1282
|
# We do this for "regular" foreaches (ie. `self.next(self.a, foreach=)`)
|
1121
1283
|
elif node.type == "foreach":
|
@@ -1143,7 +1305,7 @@ class ArgoWorkflows(object):
|
|
1143
1305
|
#
|
1144
1306
|
foreach_task = (
|
1145
1307
|
DAGTask(foreach_template_name)
|
1146
|
-
.
|
1308
|
+
.depends(f"{self._sanitize(node.name)}.Succeeded")
|
1147
1309
|
.template(foreach_template_name)
|
1148
1310
|
.arguments(
|
1149
1311
|
Arguments().parameters(
|
@@ -1188,6 +1350,16 @@ class ArgoWorkflows(object):
|
|
1188
1350
|
% self._sanitize(node.name)
|
1189
1351
|
)
|
1190
1352
|
)
|
1353
|
+
# Add conditional if this is the first step in a conditional branch
|
1354
|
+
if self._is_conditional_node(node) and not any(
|
1355
|
+
self._is_conditional_node(self.graph[in_func])
|
1356
|
+
for in_func in node.in_funcs
|
1357
|
+
):
|
1358
|
+
in_func = node.in_funcs[0]
|
1359
|
+
foreach_task.when(
|
1360
|
+
"{{tasks.%s.outputs.parameters.switch-step}}==%s"
|
1361
|
+
% (self._sanitize(in_func), node.name)
|
1362
|
+
)
|
1191
1363
|
dag_tasks.append(foreach_task)
|
1192
1364
|
templates, dag_tasks_1 = _visit(
|
1193
1365
|
self.graph[node.out_funcs[0]],
|
@@ -1231,7 +1403,22 @@ class ArgoWorkflows(object):
|
|
1231
1403
|
self.graph[node.matching_join].in_funcs[0]
|
1232
1404
|
)
|
1233
1405
|
}
|
1234
|
-
|
1406
|
+
if not self._is_conditional_join_node(
|
1407
|
+
self.graph[node.matching_join]
|
1408
|
+
)
|
1409
|
+
else
|
1410
|
+
# Note: If the nodes leading to the join are conditional, then we need to use an expression to pick the outputs from the task that executed.
|
1411
|
+
# ref for operators: https://github.com/expr-lang/expr/blob/master/docs/language-definition.md
|
1412
|
+
{
|
1413
|
+
"expression": "get((%s)?.parameters, 'task-id')"
|
1414
|
+
% " ?? ".join(
|
1415
|
+
f"tasks['{self._sanitize(func)}']?.outputs"
|
1416
|
+
for func in self.graph[
|
1417
|
+
node.matching_join
|
1418
|
+
].in_funcs
|
1419
|
+
)
|
1420
|
+
}
|
1421
|
+
),
|
1235
1422
|
]
|
1236
1423
|
if not node.parallel_foreach
|
1237
1424
|
else [
|
@@ -1264,7 +1451,7 @@ class ArgoWorkflows(object):
|
|
1264
1451
|
join_foreach_task = (
|
1265
1452
|
DAGTask(self._sanitize(self.graph[node.matching_join].name))
|
1266
1453
|
.template(self._sanitize(self.graph[node.matching_join].name))
|
1267
|
-
.
|
1454
|
+
.depends(f"{foreach_template_name}.Succeeded")
|
1268
1455
|
.arguments(
|
1269
1456
|
Arguments().parameters(
|
1270
1457
|
(
|
@@ -1391,6 +1578,14 @@ class ArgoWorkflows(object):
|
|
1391
1578
|
input_paths_expr = (
|
1392
1579
|
"export INPUT_PATHS={{inputs.parameters.input-paths}}"
|
1393
1580
|
)
|
1581
|
+
if self._is_conditional_join_node(node):
|
1582
|
+
# NOTE: Argo template expressions that fail to resolve, output the expression itself as a value.
|
1583
|
+
# With conditional steps, some of the input-paths are therefore 'broken' due to containing a nil expression
|
1584
|
+
# e.g. "{{ tasks['A'].outputs.parameters.task-id }}" when task A never executed.
|
1585
|
+
# We base64 encode the input-paths in order to not pollute the execution environment with templating expressions.
|
1586
|
+
# NOTE: Adding conditionals that check if a key exists or not does not work either, due to an issue with how Argo
|
1587
|
+
# handles tasks in a nested foreach (withParam template) leading to all such expressions getting evaluated as false.
|
1588
|
+
input_paths_expr = "export INPUT_PATHS={{=toBase64(inputs.parameters['input-paths'])}}"
|
1394
1589
|
input_paths = "$(echo $INPUT_PATHS)"
|
1395
1590
|
if any(self.graph[n].type == "foreach" for n in node.in_funcs):
|
1396
1591
|
task_idx = "{{inputs.parameters.split-index}}"
|
@@ -1406,7 +1601,6 @@ class ArgoWorkflows(object):
|
|
1406
1601
|
# foreaches
|
1407
1602
|
task_idx = "{{inputs.parameters.split-index}}"
|
1408
1603
|
root_input = "{{inputs.parameters.root-input-path}}"
|
1409
|
-
|
1410
1604
|
# Task string to be hashed into an ID
|
1411
1605
|
task_str = "-".join(
|
1412
1606
|
[
|
@@ -1563,10 +1757,25 @@ class ArgoWorkflows(object):
|
|
1563
1757
|
]
|
1564
1758
|
)
|
1565
1759
|
input_paths = "%s/_parameters/%s" % (run_id, task_id_params)
|
1760
|
+
# Only for static joins and conditional_joins
|
1761
|
+
elif self._is_conditional_join_node(node) and not (
|
1762
|
+
node.type == "join"
|
1763
|
+
and self.graph[node.split_parents[-1]].type == "foreach"
|
1764
|
+
):
|
1765
|
+
input_paths = (
|
1766
|
+
"$(python -m metaflow.plugins.argo.conditional_input_paths %s)"
|
1767
|
+
% input_paths
|
1768
|
+
)
|
1566
1769
|
elif (
|
1567
1770
|
node.type == "join"
|
1568
1771
|
and self.graph[node.split_parents[-1]].type == "foreach"
|
1569
1772
|
):
|
1773
|
+
# foreach-joins straight out of conditional branches are not yet supported
|
1774
|
+
if self._is_conditional_join_node(node):
|
1775
|
+
raise ArgoWorkflowsException(
|
1776
|
+
"Conditionals steps that transition directly into a join step are not currently supported. "
|
1777
|
+
"As a workaround, you can add a normal step after the conditional steps that transitions to a join step."
|
1778
|
+
)
|
1570
1779
|
# Set aggregated input-paths for a for-each join
|
1571
1780
|
foreach_step = next(
|
1572
1781
|
n for n in node.in_funcs if self.graph[n].is_inside_foreach
|
@@ -1809,7 +2018,7 @@ class ArgoWorkflows(object):
|
|
1809
2018
|
[Parameter("num-parallel"), Parameter("task-id-entropy")]
|
1810
2019
|
)
|
1811
2020
|
else:
|
1812
|
-
# append
|
2021
|
+
# append these only for joins of foreaches, not static splits
|
1813
2022
|
inputs.append(Parameter("split-cardinality"))
|
1814
2023
|
# check if the node is a @parallel node.
|
1815
2024
|
elif node.parallel_step:
|
@@ -1844,6 +2053,13 @@ class ArgoWorkflows(object):
|
|
1844
2053
|
# are derived at runtime.
|
1845
2054
|
if not (node.name == "end" or node.parallel_step):
|
1846
2055
|
outputs = [Parameter("task-id").valueFrom({"path": "/mnt/out/task_id"})]
|
2056
|
+
|
2057
|
+
# If this step is a split-switch one, we need to output the switch step name
|
2058
|
+
if node.type == "split-switch":
|
2059
|
+
outputs.append(
|
2060
|
+
Parameter("switch-step").valueFrom({"path": "/mnt/out/switch_step"})
|
2061
|
+
)
|
2062
|
+
|
1847
2063
|
if node.type == "foreach":
|
1848
2064
|
# Emit split cardinality from foreach task
|
1849
2065
|
outputs.append(
|
@@ -3976,6 +4192,10 @@ class DAGTask(object):
|
|
3976
4192
|
self.payload["dependencies"] = dependencies
|
3977
4193
|
return self
|
3978
4194
|
|
4195
|
+
def depends(self, depends: str):
|
4196
|
+
self.payload["depends"] = depends
|
4197
|
+
return self
|
4198
|
+
|
3979
4199
|
def template(self, template):
|
3980
4200
|
# Template reference
|
3981
4201
|
self.payload["template"] = template
|
@@ -3987,6 +4207,10 @@ class DAGTask(object):
|
|
3987
4207
|
self.payload["inline"] = template.to_json()
|
3988
4208
|
return self
|
3989
4209
|
|
4210
|
+
def when(self, when: str):
|
4211
|
+
self.payload["when"] = when
|
4212
|
+
return self
|
4213
|
+
|
3990
4214
|
def with_param(self, with_param):
|
3991
4215
|
self.payload["withParam"] = with_param
|
3992
4216
|
return self
|
@@ -123,6 +123,15 @@ class ArgoWorkflowsInternalDecorator(StepDecorator):
|
|
123
123
|
with open("/mnt/out/split_cardinality", "w") as file:
|
124
124
|
json.dump(flow._foreach_num_splits, file)
|
125
125
|
|
126
|
+
# For conditional branches we need to record the value of the switch to disk, in order to pass it as an
|
127
|
+
# output from the switching step to be used further down the DAG
|
128
|
+
if graph[step_name].type == "split-switch":
|
129
|
+
# TODO: A nicer way to access the chosen step?
|
130
|
+
_out_funcs, _ = flow._transition
|
131
|
+
chosen_step = _out_funcs[0]
|
132
|
+
with open("/mnt/out/switch_step", "w") as file:
|
133
|
+
file.write(chosen_step)
|
134
|
+
|
126
135
|
# For steps that have a `@parallel` decorator set to them, we will be relying on Jobsets
|
127
136
|
# to run the task. In this case, we cannot set anything in the
|
128
137
|
# `/mnt/out` directory, since such form of output mounts are not available to Jobset executions.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from math import inf
|
2
|
+
import sys
|
3
|
+
from metaflow.util import decompress_list, compress_list
|
4
|
+
import base64
|
5
|
+
|
6
|
+
|
7
|
+
def generate_input_paths(input_paths):
|
8
|
+
# => run_id/step/:foo,bar
|
9
|
+
# input_paths are base64 encoded due to Argo shenanigans
|
10
|
+
decoded = base64.b64decode(input_paths).decode("utf-8")
|
11
|
+
paths = decompress_list(decoded)
|
12
|
+
|
13
|
+
# some of the paths are going to be malformed due to never having executed per conditional.
|
14
|
+
# strip these out of the list.
|
15
|
+
|
16
|
+
trimmed = [path for path in paths if not "{{" in path]
|
17
|
+
return compress_list(trimmed, zlibmin=inf)
|
18
|
+
|
19
|
+
|
20
|
+
if __name__ == "__main__":
|
21
|
+
print(generate_input_paths(sys.argv[1]))
|
@@ -317,6 +317,12 @@ class StepFunctions(object):
|
|
317
317
|
"to AWS Step Functions is not supported currently."
|
318
318
|
)
|
319
319
|
|
320
|
+
if node.type == "split-switch":
|
321
|
+
raise StepFunctionsException(
|
322
|
+
"Deploying flows with switch statement "
|
323
|
+
"to AWS Step Functions is not supported currently."
|
324
|
+
)
|
325
|
+
|
320
326
|
# Assign an AWS Batch job to the AWS Step Functions state
|
321
327
|
# and pass the intermediate state by exposing `JobId` and
|
322
328
|
# `Parameters` to the child job(s) as outputs. `Index` and
|
@@ -20,12 +20,15 @@ def transform_flow_graph(step_info):
|
|
20
20
|
return "split"
|
21
21
|
elif node_type == "split-parallel" or node_type == "split-foreach":
|
22
22
|
return "foreach"
|
23
|
+
elif node_type == "split-switch":
|
24
|
+
return "switch"
|
23
25
|
return "unknown" # Should never happen
|
24
26
|
|
25
27
|
graph_dict = {}
|
26
28
|
for stepname in step_info:
|
27
|
-
|
28
|
-
|
29
|
+
node_type = node_to_type(step_info[stepname]["type"])
|
30
|
+
node_info = {
|
31
|
+
"type": node_type,
|
29
32
|
"box_next": step_info[stepname]["type"] not in ("linear", "join"),
|
30
33
|
"box_ends": (
|
31
34
|
None
|
@@ -35,6 +38,15 @@ def transform_flow_graph(step_info):
|
|
35
38
|
"next": step_info[stepname]["next"],
|
36
39
|
"doc": step_info[stepname]["doc"],
|
37
40
|
}
|
41
|
+
|
42
|
+
if node_type == "switch":
|
43
|
+
if "condition" in step_info[stepname]:
|
44
|
+
node_info["condition"] = step_info[stepname]["condition"]
|
45
|
+
if "switch_cases" in step_info[stepname]:
|
46
|
+
node_info["switch_cases"] = step_info[stepname]["switch_cases"]
|
47
|
+
|
48
|
+
graph_dict[stepname] = node_info
|
49
|
+
|
38
50
|
return graph_dict
|
39
51
|
|
40
52
|
|
@@ -0,0 +1 @@
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap";code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:#ffffff80}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}:root{--bg: #ffffff;--black: #333;--blue: #0c66de;--dk-grey: #767676;--dk-primary: #ef863b;--dk-secondary: #13172d;--dk-tertiary: #0f426e;--error: #cf483e;--grey: rgba(0, 0, 0, .125);--highlight: #f8d9d8;--lt-blue: #4fa7ff;--lt-grey: #f3f3f3;--lt-lt-grey: #f9f9f9;--lt-primary: #ffcb8b;--lt-secondary: #434d81;--lt-tertiary: #4189c9;--primary: #faab4a;--quadrary: #f8d9d8;--secondary: #2e3454;--tertiary: #2a679d;--white: #ffffff;--component-spacer: 3rem;--aside-width: 20rem;--embed-card-min-height: 12rem;--mono-font: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace}html,body{margin:0;min-height:100vh;overflow-y:visible;padding:0;width:100%}.card_app{width:100%;min-height:100vh}.embed .card_app{min-height:var(--embed-card-min-height)}.mf-card *{box-sizing:border-box}.mf-card{background:var(--bg);color:var(--black);font-family:Roboto,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:14px;font-weight:400;line-height:1.5;text-size-adjust:100%;margin:0;min-height:100vh;overflow-y:visible;padding:0;text-align:left;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;width:100%}.embed .mf-card{min-height:var(--embed-card-min-height)}.mf-card :is(.mono,code.mono,pre.mono){font-family:var(--mono-font);font-weight:lighter}.mf-card :is(table,th,td){border-spacing:1px;text-align:center;color:var(--black)}.mf-card table{position:relative;min-width:100%;table-layout:inherit!important}.mf-card td{padding:.66rem 1.25rem;background:var(--lt-lt-grey);border:none}.mf-card th{border:none;color:var(--dk-grey);font-weight:400;padding:.5rem}.mf-card :is(h1,h2,h3,h4,h5){font-weight:700;margin:.5rem 0}.mf-card ul{margin:0;padding:0}.mf-card p{margin:0 0 1rem}.mf-card p:last-of-type{margin:0}.mf-card button{font-size:1rem}.mf-card .textButton{cursor:pointer;text-align:left;background:none;border:1px solid transparent;outline:none;padding:0}.mf-card :is(button.textButton:focus,a:focus,button.textButton:active){border:1px dashed var(--grey);background:transparent}.mf-card button.textButton:hover{color:var(--blue);text-decoration:none}.mf-card :is(:not(pre)>code[class*=language-],pre[class*=language-]){background:transparent!important;text-shadow:none;-webkit-user-select:auto;user-select:auto}aside.svelte-1okdv0e{display:none;line-height:2;text-align:left}@media (min-width: 60rem){aside.svelte-1okdv0e{display:flex;flex-direction:column;height:100vh;justify-content:space-between;padding:2.5rem 0 1.5rem 1.5rem;position:fixed;width:var(--aside-width)}}.embed aside{display:none}aside ul{list-style-type:none}aside a,aside button,aside a:visited{text-decoration:none;cursor:pointer;font-weight:700;color:var(--black)}aside a:hover,aside button:hover{text-decoration:underline}.logoContainer svg{width:100%;max-width:140px;margin-bottom:3.75rem;height:auto}.idCell.svelte-pt8vzv{font-weight:700;text-align:right;background:var(--lt-grey);width:12%}.codeCell.svelte-pt8vzv{text-align:left;-webkit-user-select:all;user-select:all}.container.svelte-ubs992{width:100%;overflow:auto}table.svelte-ubs992{width:100%}:root{--dag-border: #282828;--dag-bg-static: var(--lt-grey);--dag-bg-success: #a5d46a;--dag-bg-running: #ffdf80;--dag-bg-error: #ffa080;--dag-connector: #cccccc;--dag-gap: 5rem;--dag-step-height: 6.25rem;--dag-step-width: 11.25rem;--dag-selected: #ffd700}.connectorwrapper.svelte-1hyaq5f{transform-origin:0 0;position:absolute;z-index:0;min-width:var(--strokeWidth)}.flip.svelte-1hyaq5f{transform:scaleX(-1)}.path.svelte-1hyaq5f{--strokeWidth: .5rem;--strokeColor: var(--dag-connector);--borderRadius: 1.25rem;box-sizing:border-box}.straightLine.svelte-1hyaq5f{position:absolute;inset:0;border-left:var(--strokeWidth) solid var(--strokeColor)}.topLeft.svelte-1hyaq5f{position:absolute;top:0;left:0;right:50%;bottom:calc(var(--dag-gap) / 2 - var(--strokeWidth) / 2);border-radius:0 0 0 var(--borderRadius);border-left:var(--strokeWidth) solid var(--strokeColor);border-bottom:var(--strokeWidth) solid var(--strokeColor)}.bottomRight.svelte-1hyaq5f{position:absolute;top:calc(100% - (var(--dag-gap) / 2 + var(--strokeWidth) / 2));left:50%;right:0;bottom:0;border-radius:0 var(--borderRadius) 0 0;border-top:var(--strokeWidth) solid var(--strokeColor);border-right:var(--strokeWidth) solid var(--strokeColor)}.wrapper.svelte-117ceti{position:relative;z-index:1}.step.svelte-117ceti{font-size:.75rem;padding:.5rem;color:var(--dk-grey)}.rectangle.svelte-117ceti{background-color:var(--dag-bg-static);border:1px solid var(--dag-border);box-sizing:border-box;position:relative;height:var(--dag-step-height);width:var(--dag-step-width)}.rectangle.error.svelte-117ceti{background-color:var(--dag-bg-error)}.rectangle.success.svelte-117ceti{background-color:var(--dag-bg-success)}.rectangle.running.svelte-117ceti{background-color:var(--dag-bg-running)}.level.svelte-117ceti{z-index:-1;filter:contrast(.5);position:absolute}.inner.svelte-117ceti{position:relative;height:100%;width:100%}.name.svelte-117ceti{font-weight:700;overflow:hidden;text-overflow:ellipsis;display:block}.description.svelte-117ceti{position:absolute;max-height:4rem;bottom:0;left:0;right:0;overflow:hidden;-webkit-line-clamp:4;line-clamp:4;display:-webkit-box;-webkit-box-orient:vertical}.overflown.description.svelte-117ceti{cursor:help}.current.svelte-117ceti .rectangle:where(.svelte-117ceti){box-shadow:0 0 10px var(--dag-selected)}.levelstoshow.svelte-117ceti{position:absolute;bottom:100%;right:0;font-size:.75rem;font-weight:100;text-align:right}.stepwrapper.svelte-18aex7a{display:flex;align-items:center;flex-direction:column;width:100%;position:relative;min-width:var(--dag-step-width)}.childwrapper.svelte-18aex7a{display:flex;width:100%}.gap.svelte-18aex7a{height:var(--dag-gap)}.title.svelte-117s0ws{text-align:left}.subtitle.svelte-lu9pnn{font-size:1rem;text-align:left}header.svelte-1ugmt5d{margin-bottom:var(--component-spacer)}figure.svelte-1x96yvr{background:var(--lt-grey);padding:1rem;border-radius:5px;text-align:center;margin:0 auto var(--component-spacer)}@media (min-width: 60rem){figure.svelte-1x96yvr{margin-bottom:0}}img.svelte-1x96yvr{max-width:100%;max-height:500px}.label.svelte-1x96yvr{font-weight:700;margin:.5rem 0}.description.svelte-1x96yvr{font-size:.9rem;font-style:italic;text-align:center;margin:.5rem 0}.log.svelte-1jhmsu{background:var(--lt-grey)!important;font-size:.9rem;padding:2rem}.page.svelte-v7ihqd:last-of-type{margin-bottom:var(--component-spacer)}.page:last-of-type section:last-of-type hr{display:none}progress.svelte-ljrmzp::-webkit-progress-bar{background-color:#fff!important;min-width:100%}progress.svelte-ljrmzp{background-color:#fff;color:#326cded9!important}progress.svelte-ljrmzp::-moz-progress-bar{background-color:#326cde!important}table .container{background:transparent!important;font-size:10px!important;padding:0!important}table progress{height:4px!important}.container.svelte-ljrmzp{display:flex;align-items:center;justify-content:center;font-size:12px;border-radius:3px;background:#edf5ff;padding:3rem}.inner.svelte-ljrmzp{max-width:410px;width:100%;text-align:center}.info.svelte-ljrmzp{display:flex;justify-content:space-between}table .info{text-align:left;flex-direction:column}label.svelte-ljrmzp{font-weight:700}.labelValue.svelte-ljrmzp{border-left:1px solid rgba(0,0,0,.1);margin-left:.25rem;padding-left:.5rem}.details.svelte-ljrmzp{font-family:var(--mono-font);font-size:8px;color:#333433;line-height:18px;overflow:hidden;white-space:nowrap}progress.svelte-ljrmzp{width:100%;border:none;border-radius:5px;height:8px;background:#fff}.heading.svelte-17n0qr8{margin-bottom:1.5rem}.sectionItems.svelte-17n0qr8{display:block}.sectionItems .imageContainer{max-height:500px}.container.svelte-17n0qr8{scroll-margin:var(--component-spacer)}hr.svelte-17n0qr8{background:var(--grey);border:none;height:1px;margin:var(--component-spacer) 0;padding:0}@media (min-width: 60rem){.sectionItems.svelte-17n0qr8{display:grid;grid-gap:2rem}}td.svelte-gl9h79{text-align:left}td.labelColumn.svelte-gl9h79{text-align:right;background-color:var(--lt-grey);font-weight:700;width:12%;white-space:nowrap}.tableContainer.svelte-q3hq57{overflow:auto}th.svelte-q3hq57{position:sticky;top:-1px;z-index:2;white-space:nowrap;background:var(--white)}.mainContainer.svelte-mqeomk{max-width:110rem}main.svelte-mqeomk{flex:0 1 auto;max-width:100rem;padding:1.5rem}@media (min-width: 60rem){main.svelte-mqeomk{margin-left:var(--aside-width)}}.embed main{margin:0 auto;min-width:80%}.modal.svelte-1hhf5ym{align-items:center;background:#00000080;cursor:pointer;display:flex;height:100%;justify-content:center;inset:0;overflow:hidden;position:fixed;width:100%;z-index:100}.modalContainer>*{background-color:#fff;border-radius:5px;cursor:default;flex:0 1 auto;padding:1rem;position:relative}.modal img{max-height:80vh!important}.cancelButton.svelte-1hhf5ym{color:#fff;cursor:pointer;font-size:2rem;position:absolute;right:1rem;top:1rem}.cancelButton.svelte-1hhf5ym:hover{color:var(--blue)}.nav.svelte-1kdpgko{border-radius:0 0 5px;display:none;margin:0;top:0}ul.navList.svelte-1kdpgko{list-style-type:none}ul.navList.svelte-1kdpgko ul:where(.svelte-1kdpgko){margin:.5rem 1rem 2rem}.navList.svelte-1kdpgko li:where(.svelte-1kdpgko){display:block;margin:0}.navItem.svelte-1kdpgko li:where(.svelte-1kdpgko):hover{color:var(--blue)}.pageId.svelte-1kdpgko{display:block;border-bottom:1px solid var(--grey);padding:0 .5rem;margin-bottom:1rem}@media (min-width: 60rem){.nav.svelte-1kdpgko{display:block}ul.navList.svelte-1kdpgko{text-align:left}.navList.svelte-1kdpgko li:where(.svelte-1kdpgko){display:block;margin:.5rem 0}}.container.svelte-teyund{width:100%;display:flex;flex-direction:column;position:relative}
|