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.
@@ -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
- elif (
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
- .dependencies(
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
- .dependencies([self._sanitize(node.name)])
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
- .dependencies([foreach_template_name])
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 this only for joins of foreaches, not static splits
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
- graph_dict[stepname] = {
28
- "type": node_to_type(step_info[stepname]["type"]),
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}