metaflow 2.17.3__py2.py3-none-any.whl → 2.17.5__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/datastore/task_datastore.py +3 -0
- metaflow/graph.py +3 -1
- metaflow/lint.py +6 -0
- metaflow/plugins/argo/argo_workflows.py +132 -2
- metaflow/plugins/cards/card_modules/main.js +29 -29
- metaflow/runtime.py +166 -26
- metaflow/task.py +67 -2
- metaflow/user_decorators/user_step_decorator.py +7 -1
- metaflow/version.py +1 -1
- {metaflow-2.17.3.dist-info → metaflow-2.17.5.dist-info}/METADATA +2 -2
- {metaflow-2.17.3.dist-info → metaflow-2.17.5.dist-info}/RECORD +18 -18
- {metaflow-2.17.3.data → metaflow-2.17.5.data}/data/share/metaflow/devtools/Makefile +0 -0
- {metaflow-2.17.3.data → metaflow-2.17.5.data}/data/share/metaflow/devtools/Tiltfile +0 -0
- {metaflow-2.17.3.data → metaflow-2.17.5.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
- {metaflow-2.17.3.dist-info → metaflow-2.17.5.dist-info}/WHEEL +0 -0
- {metaflow-2.17.3.dist-info → metaflow-2.17.5.dist-info}/entry_points.txt +0 -0
- {metaflow-2.17.3.dist-info → metaflow-2.17.5.dist-info}/licenses/LICENSE +0 -0
- {metaflow-2.17.3.dist-info → metaflow-2.17.5.dist-info}/top_level.txt +0 -0
metaflow/runtime.py
CHANGED
@@ -15,11 +15,13 @@ import tempfile
|
|
15
15
|
import time
|
16
16
|
import subprocess
|
17
17
|
from datetime import datetime
|
18
|
+
from enum import Enum
|
18
19
|
from io import BytesIO
|
19
20
|
from itertools import chain
|
20
21
|
from functools import partial
|
21
22
|
from concurrent import futures
|
22
23
|
|
24
|
+
from typing import Dict, Tuple
|
23
25
|
from metaflow.datastore.exceptions import DataException
|
24
26
|
from contextlib import contextmanager
|
25
27
|
|
@@ -60,6 +62,7 @@ PROGRESS_INTERVAL = 300 # s
|
|
60
62
|
# leveraging the TaskDataStoreSet.
|
61
63
|
PREFETCH_DATA_ARTIFACTS = [
|
62
64
|
"_foreach_stack",
|
65
|
+
"_iteration_stack",
|
63
66
|
"_task_ok",
|
64
67
|
"_transition",
|
65
68
|
"_control_mapper_tasks",
|
@@ -67,6 +70,14 @@ PREFETCH_DATA_ARTIFACTS = [
|
|
67
70
|
]
|
68
71
|
RESUME_POLL_SECONDS = 60
|
69
72
|
|
73
|
+
|
74
|
+
class LoopBehavior(Enum):
|
75
|
+
NONE = "none"
|
76
|
+
ENTERING = "entering"
|
77
|
+
EXITING = "exiting"
|
78
|
+
LOOPING = "looping"
|
79
|
+
|
80
|
+
|
70
81
|
# Runtime must use logsource=RUNTIME_LOG_SOURCE for all loglines that it
|
71
82
|
# formats according to mflog. See a comment in mflog.__init__
|
72
83
|
mflog_msg = partial(mflog.decorate, RUNTIME_LOG_SOURCE)
|
@@ -290,6 +301,7 @@ class NativeRuntime(object):
|
|
290
301
|
pathspec_index,
|
291
302
|
cloned_task_pathspec_index,
|
292
303
|
finished_tuple,
|
304
|
+
iteration_tuple,
|
293
305
|
ubf_context,
|
294
306
|
generate_task_obj,
|
295
307
|
verbose=False,
|
@@ -334,7 +346,7 @@ class NativeRuntime(object):
|
|
334
346
|
self._metadata,
|
335
347
|
origin_ds_set=self._origin_ds_set,
|
336
348
|
)
|
337
|
-
self._finished[(step_name, finished_tuple)] = task_pathspec
|
349
|
+
self._finished[(step_name, finished_tuple, iteration_tuple)] = task_pathspec
|
338
350
|
self._is_cloned[task_pathspec] = True
|
339
351
|
except Exception as e:
|
340
352
|
self._logger(
|
@@ -415,6 +427,7 @@ class NativeRuntime(object):
|
|
415
427
|
finished_tuple = tuple(
|
416
428
|
[s._replace(value=0) for s in task_ds.get("_foreach_stack", ())]
|
417
429
|
)
|
430
|
+
iteration_tuple = tuple(task_ds.get("_iteration_stack", ()))
|
418
431
|
cloned_task_pathspec_index = pathspec_index.split("/")[1]
|
419
432
|
if task_ds.get("_control_task_is_mapper_zero", False):
|
420
433
|
# Replace None with index 0 for control task as it is part of the
|
@@ -440,6 +453,7 @@ class NativeRuntime(object):
|
|
440
453
|
pathspec_index,
|
441
454
|
cloned_task_pathspec_index,
|
442
455
|
finished_tuple,
|
456
|
+
iteration_tuple,
|
443
457
|
is_ubf_mapper_task,
|
444
458
|
ubf_context,
|
445
459
|
)
|
@@ -454,6 +468,7 @@ class NativeRuntime(object):
|
|
454
468
|
pathspec_index,
|
455
469
|
cloned_task_pathspec_index,
|
456
470
|
finished_tuple,
|
471
|
+
iteration_tuple,
|
457
472
|
ubf_context=ubf_context,
|
458
473
|
generate_task_obj=generate_task_obj and (not is_ubf_mapper_task),
|
459
474
|
verbose=verbose,
|
@@ -464,6 +479,7 @@ class NativeRuntime(object):
|
|
464
479
|
pathspec_index,
|
465
480
|
cloned_task_pathspec_index,
|
466
481
|
finished_tuple,
|
482
|
+
iteration_tuple,
|
467
483
|
is_ubf_mapper_task,
|
468
484
|
ubf_context,
|
469
485
|
) in inputs
|
@@ -484,6 +500,7 @@ class NativeRuntime(object):
|
|
484
500
|
self._queue_push("start", {"input_paths": [self._params_task.path]})
|
485
501
|
else:
|
486
502
|
self._queue_push("start", {})
|
503
|
+
|
487
504
|
progress_tstamp = time.time()
|
488
505
|
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as config_file:
|
489
506
|
# Configurations are passed through a file to avoid overloading the
|
@@ -504,7 +521,74 @@ class NativeRuntime(object):
|
|
504
521
|
):
|
505
522
|
# 1. are any of the current workers finished?
|
506
523
|
if self._cloned_tasks:
|
507
|
-
finished_tasks =
|
524
|
+
finished_tasks = []
|
525
|
+
|
526
|
+
# For loops (right now just recursive steps), we need to find
|
527
|
+
# the exact frontier because if we queue all "successors" to all
|
528
|
+
# the finished iterations, we would incorrectly launch multiple
|
529
|
+
# successors. We therefore have to strip out all non-last
|
530
|
+
# iterations *per* foreach branch.
|
531
|
+
idx_per_finished_id = (
|
532
|
+
{}
|
533
|
+
) # type: Dict[Tuple[str, Tuple[int, ...], Tuple[int, Tuple[int, ...]]]]
|
534
|
+
for task in self._cloned_tasks:
|
535
|
+
step_name, foreach_stack, iteration_stack = task.finished_id
|
536
|
+
existing_task_idx = idx_per_finished_id.get(
|
537
|
+
(step_name, foreach_stack), None
|
538
|
+
)
|
539
|
+
if existing_task_idx is not None:
|
540
|
+
len_diff = len(iteration_stack) - len(
|
541
|
+
existing_task_idx[1]
|
542
|
+
)
|
543
|
+
# In this case, we need to keep only the latest iteration
|
544
|
+
if (
|
545
|
+
len_diff == 0
|
546
|
+
and iteration_stack > existing_task_idx[1]
|
547
|
+
) or len_diff == -1:
|
548
|
+
# We remove the one we currently have and replace
|
549
|
+
# by this one. The second option means that we are
|
550
|
+
# adding the finished iteration marker.
|
551
|
+
existing_task = finished_tasks[existing_task_idx[0]]
|
552
|
+
# These are the first two lines of _queue_tasks
|
553
|
+
# We still consider the tasks finished so we need
|
554
|
+
# to update state to be clean.
|
555
|
+
self._finished[existing_task.finished_id] = (
|
556
|
+
existing_task.path
|
557
|
+
)
|
558
|
+
self._is_cloned[existing_task.path] = (
|
559
|
+
existing_task.is_cloned
|
560
|
+
)
|
561
|
+
|
562
|
+
finished_tasks[existing_task_idx[0]] = task
|
563
|
+
idx_per_finished_id[(step_name, foreach_stack)] = (
|
564
|
+
existing_task_idx[0],
|
565
|
+
iteration_stack,
|
566
|
+
)
|
567
|
+
elif (
|
568
|
+
len_diff == 0
|
569
|
+
and iteration_stack < existing_task_idx[1]
|
570
|
+
) or len_diff == 1:
|
571
|
+
# The second option is when we have already marked
|
572
|
+
# the end of the iteration in self._finished and
|
573
|
+
# are now seeing a previous iteration.
|
574
|
+
# We just mark the task as finished but we don't
|
575
|
+
# put it in the finished_tasks list to pass to
|
576
|
+
# the _queue_tasks function
|
577
|
+
self._finished[task.finished_id] = task.path
|
578
|
+
self._is_cloned[task.path] = task.is_cloned
|
579
|
+
else:
|
580
|
+
raise MetaflowInternalError(
|
581
|
+
"Unexpected recursive cloned tasks -- "
|
582
|
+
"this is a bug, please report it."
|
583
|
+
)
|
584
|
+
else:
|
585
|
+
# New entry
|
586
|
+
finished_tasks.append(task)
|
587
|
+
idx_per_finished_id[(step_name, foreach_stack)] = (
|
588
|
+
len(finished_tasks) - 1,
|
589
|
+
iteration_stack,
|
590
|
+
)
|
591
|
+
|
508
592
|
# reset the list of cloned tasks and let poll_workers handle
|
509
593
|
# the remaining transition
|
510
594
|
self._cloned_tasks = []
|
@@ -578,7 +662,7 @@ class NativeRuntime(object):
|
|
578
662
|
self._run_exit_hooks()
|
579
663
|
|
580
664
|
# assert that end was executed and it was successful
|
581
|
-
if ("end", ()) in self._finished:
|
665
|
+
if ("end", (), ()) in self._finished:
|
582
666
|
if self._run_url:
|
583
667
|
self._logger(
|
584
668
|
"Done! See the run in the UI at %s" % self._run_url,
|
@@ -604,7 +688,7 @@ class NativeRuntime(object):
|
|
604
688
|
if not exit_hook_decos:
|
605
689
|
return
|
606
690
|
|
607
|
-
successful = ("end", ()) in self._finished or self._clone_only
|
691
|
+
successful = ("end", (), ()) in self._finished or self._clone_only
|
608
692
|
pathspec = f"{self._graph.name}/{self._run_id}"
|
609
693
|
flow_file = self._environment.get_environment_info()["script"]
|
610
694
|
|
@@ -672,29 +756,60 @@ class NativeRuntime(object):
|
|
672
756
|
|
673
757
|
# Given the current task information (task_index), the type of transition,
|
674
758
|
# and the split index, return the new task index.
|
675
|
-
def _translate_index(
|
676
|
-
|
759
|
+
def _translate_index(
|
760
|
+
self, task, next_step, type, split_index=None, loop_mode=LoopBehavior.NONE
|
761
|
+
):
|
762
|
+
match = re.match(r"^(.+)\[(.*)\]\[(.*)\]$", task.task_index)
|
677
763
|
if match:
|
678
|
-
_, foreach_index = match.groups()
|
764
|
+
_, foreach_index, iteration_index = match.groups()
|
679
765
|
# Convert foreach_index to a list of integers
|
680
766
|
if len(foreach_index) > 0:
|
681
767
|
foreach_index = foreach_index.split(",")
|
682
768
|
else:
|
683
769
|
foreach_index = []
|
770
|
+
# Ditto for iteration_index
|
771
|
+
if len(iteration_index) > 0:
|
772
|
+
iteration_index = iteration_index.split(",")
|
773
|
+
else:
|
774
|
+
iteration_index = []
|
684
775
|
else:
|
685
776
|
raise ValueError(
|
686
|
-
"Index not in the format of {run_id}/{step_name}[{foreach_index}]"
|
777
|
+
"Index not in the format of {run_id}/{step_name}[{foreach_index}][{iteration_index}]"
|
687
778
|
)
|
779
|
+
if loop_mode == LoopBehavior.NONE:
|
780
|
+
# Check if we are entering a looping construct. Right now, only recursive
|
781
|
+
# steps are looping constructs
|
782
|
+
next_step_node = self._graph[next_step]
|
783
|
+
if (
|
784
|
+
next_step_node.type == "split-switch"
|
785
|
+
and next_step in next_step_node.out_funcs
|
786
|
+
):
|
787
|
+
loop_mode = LoopBehavior.ENTERING
|
788
|
+
|
789
|
+
# Update iteration_index
|
790
|
+
if loop_mode == LoopBehavior.ENTERING:
|
791
|
+
# We are entering a loop, so we add a new iteration level
|
792
|
+
iteration_index.append("0")
|
793
|
+
elif loop_mode == LoopBehavior.EXITING:
|
794
|
+
iteration_index = iteration_index[:-1]
|
795
|
+
elif loop_mode == LoopBehavior.LOOPING:
|
796
|
+
if len(iteration_index) == 0:
|
797
|
+
raise MetaflowInternalError(
|
798
|
+
"In looping mode but there is no iteration index"
|
799
|
+
)
|
800
|
+
iteration_index[-1] = str(int(iteration_index[-1]) + 1)
|
801
|
+
iteration_index = ",".join(iteration_index)
|
802
|
+
|
688
803
|
if type == "linear":
|
689
|
-
return "%s[%s]" % (next_step, ",".join(foreach_index))
|
804
|
+
return "%s[%s][%s]" % (next_step, ",".join(foreach_index), iteration_index)
|
690
805
|
elif type == "join":
|
691
806
|
indices = []
|
692
807
|
if len(foreach_index) > 0:
|
693
808
|
indices = foreach_index[:-1]
|
694
|
-
return "%s[%s]" % (next_step, ",".join(indices))
|
809
|
+
return "%s[%s][%s]" % (next_step, ",".join(indices), iteration_index)
|
695
810
|
elif type == "split":
|
696
811
|
foreach_index.append(str(split_index))
|
697
|
-
return "%s[%s]" % (next_step, ",".join(foreach_index))
|
812
|
+
return "%s[%s][%s]" % (next_step, ",".join(foreach_index), iteration_index)
|
698
813
|
|
699
814
|
# Store the parameters needed for task creation, so that pushing on items
|
700
815
|
# onto the run_queue is an inexpensive operation.
|
@@ -778,17 +893,19 @@ class NativeRuntime(object):
|
|
778
893
|
# tasks is incorrect and contains the pathspec of the *cloned* run
|
779
894
|
# but we don't use it for anything. We could look to clean it up though
|
780
895
|
if not task.is_cloned:
|
781
|
-
_, foreach_stack = task.finished_id
|
896
|
+
_, foreach_stack, iteration_stack = task.finished_id
|
782
897
|
top = foreach_stack[-1]
|
783
898
|
bottom = list(foreach_stack[:-1])
|
784
899
|
for i in range(num_splits):
|
785
900
|
s = tuple(bottom + [top._replace(index=i)])
|
786
|
-
self._finished[(task.step, s)] = mapper_tasks[
|
901
|
+
self._finished[(task.step, s, iteration_stack)] = mapper_tasks[
|
902
|
+
i
|
903
|
+
]
|
787
904
|
self._is_cloned[mapper_tasks[i]] = False
|
788
905
|
|
789
906
|
# Find and check status of control task and retrieve its pathspec
|
790
907
|
# for retrieving unbounded foreach cardinality.
|
791
|
-
_, foreach_stack = task.finished_id
|
908
|
+
_, foreach_stack, iteration_stack = task.finished_id
|
792
909
|
top = foreach_stack[-1]
|
793
910
|
bottom = list(foreach_stack[:-1])
|
794
911
|
s = tuple(bottom + [top._replace(index=None)])
|
@@ -797,7 +914,7 @@ class NativeRuntime(object):
|
|
797
914
|
# it will have index=0 instead of index=None.
|
798
915
|
if task.results.get("_control_task_is_mapper_zero", False):
|
799
916
|
s = tuple(bottom + [top._replace(index=0)])
|
800
|
-
control_path = self._finished.get((task.step, s))
|
917
|
+
control_path = self._finished.get((task.step, s, iteration_stack))
|
801
918
|
if control_path:
|
802
919
|
# Control task was successful.
|
803
920
|
# Additionally check the state of (sibling) mapper tasks as well
|
@@ -806,7 +923,9 @@ class NativeRuntime(object):
|
|
806
923
|
required_tasks = []
|
807
924
|
for i in range(num_splits):
|
808
925
|
s = tuple(bottom + [top._replace(index=i)])
|
809
|
-
required_tasks.append(
|
926
|
+
required_tasks.append(
|
927
|
+
self._finished.get((task.step, s, iteration_stack))
|
928
|
+
)
|
810
929
|
|
811
930
|
if all(required_tasks):
|
812
931
|
index = self._translate_index(task, next_step, "join")
|
@@ -819,7 +938,7 @@ class NativeRuntime(object):
|
|
819
938
|
else:
|
820
939
|
# matching_split is the split-parent of the finished task
|
821
940
|
matching_split = self._graph[self._graph[next_step].split_parents[-1]]
|
822
|
-
_, foreach_stack = task.finished_id
|
941
|
+
_, foreach_stack, iteration_stack = task.finished_id
|
823
942
|
|
824
943
|
direct_parents = set(self._graph[next_step].in_funcs)
|
825
944
|
|
@@ -837,7 +956,7 @@ class NativeRuntime(object):
|
|
837
956
|
filter(
|
838
957
|
lambda x: x is not None,
|
839
958
|
[
|
840
|
-
self._finished.get((p, s))
|
959
|
+
self._finished.get((p, s, iteration_stack))
|
841
960
|
for p in direct_parents
|
842
961
|
for s in siblings(foreach_stack)
|
843
962
|
],
|
@@ -852,11 +971,12 @@ class NativeRuntime(object):
|
|
852
971
|
filter(
|
853
972
|
lambda x: x is not None,
|
854
973
|
[
|
855
|
-
self._finished.get((p, foreach_stack))
|
974
|
+
self._finished.get((p, foreach_stack, iteration_stack))
|
856
975
|
for p in direct_parents
|
857
976
|
],
|
858
977
|
)
|
859
978
|
)
|
979
|
+
|
860
980
|
required_count = len(matching_split.out_funcs)
|
861
981
|
join_type = "linear"
|
862
982
|
index = self._translate_index(task, next_step, "linear")
|
@@ -868,9 +988,18 @@ class NativeRuntime(object):
|
|
868
988
|
index,
|
869
989
|
)
|
870
990
|
|
871
|
-
def _queue_task_switch(self, task, next_steps):
|
991
|
+
def _queue_task_switch(self, task, next_steps, is_recursive):
|
872
992
|
chosen_step = next_steps[0]
|
873
|
-
|
993
|
+
|
994
|
+
loop_mode = LoopBehavior.NONE
|
995
|
+
if is_recursive:
|
996
|
+
if chosen_step != task.step:
|
997
|
+
# We are exiting a loop
|
998
|
+
loop_mode = LoopBehavior.EXITING
|
999
|
+
else:
|
1000
|
+
# We are staying in the loop
|
1001
|
+
loop_mode = LoopBehavior.LOOPING
|
1002
|
+
index = self._translate_index(task, chosen_step, "linear", None, loop_mode)
|
874
1003
|
self._queue_push(chosen_step, {"input_paths": [task.path]}, index)
|
875
1004
|
|
876
1005
|
def _queue_task_foreach(self, task, next_steps):
|
@@ -949,7 +1078,9 @@ class NativeRuntime(object):
|
|
949
1078
|
next_steps = []
|
950
1079
|
foreach = None
|
951
1080
|
expected = self._graph[task.step].out_funcs
|
1081
|
+
|
952
1082
|
if self._graph[task.step].type == "split-switch":
|
1083
|
+
is_recursive = task.step in self._graph[task.step].out_funcs
|
953
1084
|
if len(next_steps) != 1:
|
954
1085
|
msg = (
|
955
1086
|
"Switch step *{step}* should transition to exactly "
|
@@ -970,6 +1101,15 @@ class NativeRuntime(object):
|
|
970
1101
|
expected=", ".join(expected),
|
971
1102
|
)
|
972
1103
|
)
|
1104
|
+
# When exiting a recursive loop, we mark that the loop itself has
|
1105
|
+
# finished by adding a special entry in self._finished which has
|
1106
|
+
# an iteration stack that is shorter (ie: we are out of the loop) so
|
1107
|
+
# that we can then find it when looking at successor tasks to launch.
|
1108
|
+
if is_recursive and next_steps[0] != task.step:
|
1109
|
+
step_name, finished_tuple, iteration_tuple = task.finished_id
|
1110
|
+
self._finished[
|
1111
|
+
(step_name, finished_tuple, iteration_tuple[:-1])
|
1112
|
+
] = task.path
|
973
1113
|
elif next_steps != expected:
|
974
1114
|
msg = (
|
975
1115
|
"Based on static analysis of the code, step *{step}* "
|
@@ -995,8 +1135,8 @@ class NativeRuntime(object):
|
|
995
1135
|
# Next step is a foreach child
|
996
1136
|
self._queue_task_foreach(task, next_steps)
|
997
1137
|
elif self._graph[task.step].type == "split-switch":
|
998
|
-
#
|
999
|
-
self._queue_task_switch(task, next_steps)
|
1138
|
+
# Current step is switch - queue the chosen step
|
1139
|
+
self._queue_task_switch(task, next_steps, is_recursive)
|
1000
1140
|
else:
|
1001
1141
|
# Next steps are normal linear steps
|
1002
1142
|
for step in next_steps:
|
@@ -1537,13 +1677,13 @@ class Task(object):
|
|
1537
1677
|
@property
|
1538
1678
|
def finished_id(self):
|
1539
1679
|
# note: id is not available before the task has finished.
|
1540
|
-
# Index already identifies the task within the foreach
|
1541
|
-
#
|
1680
|
+
# Index already identifies the task within the foreach and loop.
|
1681
|
+
# We will remove foreach value so that it is easier to
|
1542
1682
|
# identify siblings within a foreach.
|
1543
1683
|
foreach_stack_tuple = tuple(
|
1544
1684
|
[s._replace(value=0) for s in self.results["_foreach_stack"]]
|
1545
1685
|
)
|
1546
|
-
return (self.step, foreach_stack_tuple)
|
1686
|
+
return (self.step, foreach_stack_tuple, tuple(self.results["_iteration_stack"]))
|
1547
1687
|
|
1548
1688
|
@property
|
1549
1689
|
def is_cloned(self):
|
metaflow/task.py
CHANGED
@@ -117,11 +117,19 @@ class MetaflowTask(object):
|
|
117
117
|
|
118
118
|
# We back out of the stack of generators
|
119
119
|
for w in reversed(wrappers_stack):
|
120
|
-
|
120
|
+
try:
|
121
|
+
r = w.post_step(orig_step_func.name, self.flow, raised_exception)
|
122
|
+
except Exception as ex:
|
123
|
+
r = ex
|
121
124
|
if r is None or isinstance(r, Exception):
|
122
125
|
raised_exception = None
|
123
126
|
elif isinstance(r, tuple):
|
124
|
-
|
127
|
+
if len(r) == 2:
|
128
|
+
raised_exception, fake_next_call_args = r
|
129
|
+
else:
|
130
|
+
# The last argument is an exception to be re-raised. Used in
|
131
|
+
# user_step_decorator's post_step
|
132
|
+
raise r[2]
|
125
133
|
else:
|
126
134
|
raise RuntimeError(
|
127
135
|
"Invalid return value from a UserStepDecorator. Expected an"
|
@@ -239,6 +247,7 @@ class MetaflowTask(object):
|
|
239
247
|
# Prefetch 'foreach' related artifacts to improve time taken by
|
240
248
|
# _init_foreach.
|
241
249
|
prefetch_data_artifacts = [
|
250
|
+
"_iteration_stack",
|
242
251
|
"_foreach_stack",
|
243
252
|
"_foreach_num_splits",
|
244
253
|
"_foreach_var",
|
@@ -375,6 +384,56 @@ class MetaflowTask(object):
|
|
375
384
|
elif "_foreach_stack" in inputs[0]:
|
376
385
|
self.flow._foreach_stack = inputs[0]["_foreach_stack"]
|
377
386
|
|
387
|
+
def _init_iteration(self, step_name, inputs, is_recursive_step):
|
388
|
+
# We track the iteration "stack" for loops. At this time, we
|
389
|
+
# only support one type of "looping" which is a recursive step but
|
390
|
+
# this can generalize to arbitrary well-scoped loops in the future.
|
391
|
+
|
392
|
+
# _iteration_stack will contain the iteration count for each loop
|
393
|
+
# level. Currently, there will be only no elements (no loops) or
|
394
|
+
# a single element (a single recursive step).
|
395
|
+
|
396
|
+
# We just need to determine the rules to add a new looping level,
|
397
|
+
# increment the looping level or pop the looping level. In our
|
398
|
+
# current support for only recursive steps, this is pretty straightforward:
|
399
|
+
# 1) if is_recursive_step:
|
400
|
+
# - we are entering a loop -- we are either entering for the first time
|
401
|
+
# or we are continuing the loop. Note that a recursive step CANNOT
|
402
|
+
# be a join step so there is always a single input
|
403
|
+
# 1a) If inputs[0]["_iteration_stack"] contains an element, we are looping
|
404
|
+
# so we increment the count
|
405
|
+
# 1b) If inputs[0]["_iteration_stack"] is empty, this is the first time we
|
406
|
+
# are entering the loop so we set the iteration count to 0
|
407
|
+
# 2) if it is not a recursive step, we need to determine if this is the step
|
408
|
+
# *after* the recursive step. The easiest way to determine that is to
|
409
|
+
# look at all inputs (there can be multiple in case of a join) and pop
|
410
|
+
# _iteration_stack if it is set. However, since we know that non recursive
|
411
|
+
# steps are *never* part of an iteration, we can simplify and just set it
|
412
|
+
# to [] without even checking anything. We will have to revisit this if/when
|
413
|
+
# more complex loop structures are supported.
|
414
|
+
|
415
|
+
# Note that just like _foreach_stack, we need to set _iteration_stack to *something*
|
416
|
+
# so that it doesn't get clobbered weirdly by merge_artifacts.
|
417
|
+
|
418
|
+
if is_recursive_step:
|
419
|
+
# Case 1)
|
420
|
+
if len(inputs) != 1:
|
421
|
+
raise MetaflowInternalError(
|
422
|
+
"Step *%s* is a recursive step but got multiple inputs." % step_name
|
423
|
+
)
|
424
|
+
inp = inputs[0]
|
425
|
+
if "_iteration_stack" not in inp or not inp["_iteration_stack"]:
|
426
|
+
# Case 1b)
|
427
|
+
self.flow._iteration_stack = [0]
|
428
|
+
else:
|
429
|
+
# Case 1a)
|
430
|
+
stack = inp["_iteration_stack"]
|
431
|
+
stack[-1] += 1
|
432
|
+
self.flow._iteration_stack = stack
|
433
|
+
else:
|
434
|
+
# Case 2)
|
435
|
+
self.flow._iteration_stack = []
|
436
|
+
|
378
437
|
def _clone_flow(self, datastore):
|
379
438
|
x = self.flow.__class__(use_cli=False)
|
380
439
|
x._set_datastore(datastore)
|
@@ -563,6 +622,12 @@ class MetaflowTask(object):
|
|
563
622
|
# 3. initialize foreach state
|
564
623
|
self._init_foreach(step_name, join_type, inputs, split_index)
|
565
624
|
|
625
|
+
# 4. initialize the iteration state
|
626
|
+
is_recursive_step = (
|
627
|
+
node.type == "split-switch" and step_name in node.out_funcs
|
628
|
+
)
|
629
|
+
self._init_iteration(step_name, inputs, is_recursive_step)
|
630
|
+
|
566
631
|
# Add foreach stack to metadata of the task
|
567
632
|
|
568
633
|
foreach_stack = (
|
@@ -658,8 +658,14 @@ def user_step_decorator(*args, **kwargs):
|
|
658
658
|
self._generator.send(None)
|
659
659
|
except StopIteration as e:
|
660
660
|
to_return = e.value
|
661
|
+
except Exception as e:
|
662
|
+
return e
|
661
663
|
else:
|
662
|
-
|
664
|
+
return (
|
665
|
+
None,
|
666
|
+
None,
|
667
|
+
MetaflowException(" %s should only yield once" % self),
|
668
|
+
)
|
663
669
|
return to_return
|
664
670
|
|
665
671
|
return WrapClass
|
metaflow/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
metaflow_version = "2.17.
|
1
|
+
metaflow_version = "2.17.5"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: metaflow
|
3
|
-
Version: 2.17.
|
3
|
+
Version: 2.17.5
|
4
4
|
Summary: Metaflow: More AI and ML, Less Engineering
|
5
5
|
Author: Metaflow Developers
|
6
6
|
Author-email: help@metaflow.org
|
@@ -26,7 +26,7 @@ License-File: LICENSE
|
|
26
26
|
Requires-Dist: requests
|
27
27
|
Requires-Dist: boto3
|
28
28
|
Provides-Extra: stubs
|
29
|
-
Requires-Dist: metaflow-stubs==2.17.
|
29
|
+
Requires-Dist: metaflow-stubs==2.17.5; extra == "stubs"
|
30
30
|
Dynamic: author
|
31
31
|
Dynamic: author-email
|
32
32
|
Dynamic: classifier
|
@@ -11,10 +11,10 @@ metaflow/event_logger.py,sha256=joTVRqZPL87nvah4ZOwtqWX8NeraM_CXKXXGVpKGD8o,780
|
|
11
11
|
metaflow/events.py,sha256=ahjzkSbSnRCK9RZ-9vTfUviz_6gMvSO9DGkJ86X80-k,5300
|
12
12
|
metaflow/exception.py,sha256=_m9ZBJM0cooHRslDqfxCPQmkChqaTh6fGxp7HvISnYI,5161
|
13
13
|
metaflow/flowspec.py,sha256=9wsO2_QoO_VHKusKdpslfbwQREOwf0fAzF-DSEA0iZ8,41968
|
14
|
-
metaflow/graph.py,sha256=
|
14
|
+
metaflow/graph.py,sha256=UOeClj-JeORRlZWOQMI1CirkrCpTvVWvRcSwODCajMg,19263
|
15
15
|
metaflow/includefile.py,sha256=RtISGl1V48qjkJBakUZ9yPpHV102h7pOIFiKP8PLHpc,20927
|
16
16
|
metaflow/integrations.py,sha256=LlsaoePRg03DjENnmLxZDYto3NwWc9z_PtU6nJxLldg,1480
|
17
|
-
metaflow/lint.py,sha256=
|
17
|
+
metaflow/lint.py,sha256=dvgpMZoF506yJerCHHUu7A-nTVry95Fmr8Q3bPEaJCs,14374
|
18
18
|
metaflow/meta_files.py,sha256=vlgJHI8GJUKzXoxdrVoH8yyCF5bhFgwYemUgnyd1wgM,342
|
19
19
|
metaflow/metaflow_config.py,sha256=lsdfr_1eS4cP6g1OgLWePKzUIypUGVjNxffRhKlFWfA,24102
|
20
20
|
metaflow/metaflow_config_funcs.py,sha256=5GlvoafV6SxykwfL8D12WXSfwjBN_NsyuKE_Q3gjGVE,6738
|
@@ -29,14 +29,14 @@ metaflow/parameters.py,sha256=b3rS6P-TeEj2JgPEKaiy0ys1B_JtOGJ6XM0KkY2layc,18649
|
|
29
29
|
metaflow/procpoll.py,sha256=U2tE4iK_Mwj2WDyVTx_Uglh6xZ-jixQOo4wrM9OOhxg,2859
|
30
30
|
metaflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
31
|
metaflow/pylint_wrapper.py,sha256=tJjmdsgtbHTCqg_oA6fV6SbWq_3V5XUgE9xH0zJ1CGU,3004
|
32
|
-
metaflow/runtime.py,sha256=
|
32
|
+
metaflow/runtime.py,sha256=3R44AFI04LWahbfa2v0yDYIWHIz6sau26p1SnzYFNew,86888
|
33
33
|
metaflow/tagging_util.py,sha256=ctyf0Q1gBi0RyZX6J0e9DQGNkNHblV_CITfy66axXB4,2346
|
34
|
-
metaflow/task.py,sha256=
|
34
|
+
metaflow/task.py,sha256=keErPaNH5gNko6V1saDT-qsICKu4EROau9DiCu4riHU,39192
|
35
35
|
metaflow/tuple_util.py,sha256=_G5YIEhuugwJ_f6rrZoelMFak3DqAR2tt_5CapS1XTY,830
|
36
36
|
metaflow/unbounded_foreach.py,sha256=p184WMbrMJ3xKYHwewj27ZhRUsSj_kw1jlye5gA9xJk,387
|
37
37
|
metaflow/util.py,sha256=g2SOU_CRzJLgDM_UGF9QDMANMAIHAsDRXE6S76_YzsY,14594
|
38
38
|
metaflow/vendor.py,sha256=A82CGHfStZGDP5pQ5XzRjFkbN1ZC-vFmghXIrzMDDNg,5868
|
39
|
-
metaflow/version.py,sha256=
|
39
|
+
metaflow/version.py,sha256=YrG_eZXjqECyWWPibAnZKm-dNf2obxHEkt8n9fUg9zs,28
|
40
40
|
metaflow/_vendor/__init__.py,sha256=y_CiwUD3l4eAKvTVDZeqgVujMy31cAM1qjAB-HfI-9s,353
|
41
41
|
metaflow/_vendor/typing_extensions.py,sha256=q9zxWa6p6CzF1zZvSkygSlklduHf_b3K7MCxGz7MJRc,134519
|
42
42
|
metaflow/_vendor/zipp.py,sha256=ajztOH-9I7KA_4wqDYygtHa6xUBVZgFpmZ8FE74HHHI,8425
|
@@ -176,7 +176,7 @@ metaflow/datastore/datastore_storage.py,sha256=7V43QuiWDQ_Q4oHw9y7Z7X9lYj3GI-LV1
|
|
176
176
|
metaflow/datastore/exceptions.py,sha256=r7Ab5FvHIzyFh6kwiptA1lO5nLqWg0xRBoeYGefvapA,373
|
177
177
|
metaflow/datastore/flow_datastore.py,sha256=rDMEHdYwub1PwLp2uaK-8CHdd8hiwxqeELXzsUfuqZs,10250
|
178
178
|
metaflow/datastore/inputs.py,sha256=i43dXr2xvgtsgKMO9allgCR18bk80GeayeQFyUTH36w,449
|
179
|
-
metaflow/datastore/task_datastore.py,sha256=
|
179
|
+
metaflow/datastore/task_datastore.py,sha256=XZKC4YJ2kyFTDqwTARNavjQ-oSosjUJbNH777e9OF0c,35262
|
180
180
|
metaflow/extension_support/__init__.py,sha256=xLkhh0IzQ70IfF9j6MopMY02SMpEVI_eguksIOEXbDs,52522
|
181
181
|
metaflow/extension_support/_empty_file.py,sha256=vz61sSExf5DZH3JCqdfwkp7l_NrJR8HV175kG82yUas,133
|
182
182
|
metaflow/extension_support/cmd.py,sha256=hk8iBUUINqvKCDxInKgWpum8ThiRZtHSJP7qBASHzl8,5711
|
@@ -230,7 +230,7 @@ metaflow/plugins/airflow/sensors/s3_sensor.py,sha256=iDReG-7FKnumrtQg-HY6cCUAAqN
|
|
230
230
|
metaflow/plugins/argo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
231
231
|
metaflow/plugins/argo/argo_client.py,sha256=jLz0FjCTBvFLZt-8lZcMQhDcInhgEcGdPrU2Gvh67zA,17080
|
232
232
|
metaflow/plugins/argo/argo_events.py,sha256=_C1KWztVqgi3zuH57pInaE9OzABc2NnncC-zdwOMZ-w,5909
|
233
|
-
metaflow/plugins/argo/argo_workflows.py,sha256=
|
233
|
+
metaflow/plugins/argo/argo_workflows.py,sha256=5AAPsMZuJ_yhQoE6Xx4VwtSRJyrSXqANqpAeog6XZjQ,207490
|
234
234
|
metaflow/plugins/argo/argo_workflows_cli.py,sha256=L5KwcT6Vd4HqAXFCPGmHUONgM2eOCTbvxdoIs6CQchw,51877
|
235
235
|
metaflow/plugins/argo/argo_workflows_decorator.py,sha256=CLSjPqFTGucZ2_dSQGAYkoWWUZBQ9TCBXul4rxhDj3w,8282
|
236
236
|
metaflow/plugins/argo/argo_workflows_deployer.py,sha256=6kHxEnYXJwzNCM9swI8-0AckxtPWqwhZLerYkX8fxUM,4444
|
@@ -289,7 +289,7 @@ metaflow/plugins/cards/card_modules/card.py,sha256=6sbqP5mwf7QWvQvX2N_bC78H9ixuI
|
|
289
289
|
metaflow/plugins/cards/card_modules/components.py,sha256=Y-Yo7UgSOyTB3zs-wDfUqUKl0PI_BzeY1QGcy2fqE2M,26752
|
290
290
|
metaflow/plugins/cards/card_modules/convert_to_native_type.py,sha256=Vcjqn5rfC0kVMdhqDwsYEjknXTbkG_ppraQrQGaQY_E,16245
|
291
291
|
metaflow/plugins/cards/card_modules/main.css,sha256=avu7BTB9qj0M8LvqNLUhikUFQhmAJhQQ7REcUgh9zMw,11725
|
292
|
-
metaflow/plugins/cards/card_modules/main.js,sha256=
|
292
|
+
metaflow/plugins/cards/card_modules/main.js,sha256=1p26spBKCwglJgioKfmAAPgh4pP2Cdp0bq29fWyg58A,1045517
|
293
293
|
metaflow/plugins/cards/card_modules/renderer_tools.py,sha256=uiTdKHWFInWgtfWArOUSQLnTwqVd4hAw49jfOfg8rGA,1642
|
294
294
|
metaflow/plugins/cards/card_modules/test_cards.py,sha256=t2PZ3QnjCN_KUlDKxAt2FfK1BX2dp8rqK6TjtkCNyew,5876
|
295
295
|
metaflow/plugins/cards/card_modules/chevron/__init__.py,sha256=SicpH_1eT76HUTA8aMuiGLRNKTqgYD1Qfutt2NdTL0E,156
|
@@ -427,13 +427,13 @@ metaflow/user_decorators/common.py,sha256=0u9NRLQ95TfJCjWVEOGT4MJ9WoQgAMBM9kJ2gJ
|
|
427
427
|
metaflow/user_decorators/mutable_flow.py,sha256=EywKTN3cnXPQF_s62wQaC4a4aH14j8oeqzD3yZaiDxw,19467
|
428
428
|
metaflow/user_decorators/mutable_step.py,sha256=-BY0UDXf_RCAEnC5JlLzEXGdiw1KD9oSrSxS_SWaB9Y,16791
|
429
429
|
metaflow/user_decorators/user_flow_decorator.py,sha256=2yDwZq9QGv9W-7kEuKwa8o4ZkTvuHJ5ESz7VVrGViAI,9890
|
430
|
-
metaflow/user_decorators/user_step_decorator.py,sha256=
|
431
|
-
metaflow-2.17.
|
432
|
-
metaflow-2.17.
|
433
|
-
metaflow-2.17.
|
434
|
-
metaflow-2.17.
|
435
|
-
metaflow-2.17.
|
436
|
-
metaflow-2.17.
|
437
|
-
metaflow-2.17.
|
438
|
-
metaflow-2.17.
|
439
|
-
metaflow-2.17.
|
430
|
+
metaflow/user_decorators/user_step_decorator.py,sha256=4558NR8RJtN22OyTwCXO80bAMhMTaRGMoX12b1GMcPc,27232
|
431
|
+
metaflow-2.17.5.data/data/share/metaflow/devtools/Makefile,sha256=TT4TCq8ALSfqYyGqDPocN5oPcZe2FqoCZxmGO1LmyCc,13760
|
432
|
+
metaflow-2.17.5.data/data/share/metaflow/devtools/Tiltfile,sha256=Ty5p6AD3MwJAcAnOGv4yMz8fExAsnNQ11r8whK6uzzw,21381
|
433
|
+
metaflow-2.17.5.data/data/share/metaflow/devtools/pick_services.sh,sha256=DCnrMXwtApfx3B4S-YiZESMyAFHbXa3VuNL0MxPLyiE,2196
|
434
|
+
metaflow-2.17.5.dist-info/licenses/LICENSE,sha256=nl_Lt5v9VvJ-5lWJDT4ddKAG-VZ-2IaLmbzpgYDz2hU,11343
|
435
|
+
metaflow-2.17.5.dist-info/METADATA,sha256=8p1TxoX8SU5a2LIpAPlSNgyMsqfgIPk4FDA01CI4ZfA,6740
|
436
|
+
metaflow-2.17.5.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
437
|
+
metaflow-2.17.5.dist-info/entry_points.txt,sha256=RvEq8VFlgGe_FfqGOZi0D7ze1hLD0pAtXeNyGfzc_Yc,103
|
438
|
+
metaflow-2.17.5.dist-info/top_level.txt,sha256=v1pDHoWaSaKeuc5fKTRSfsXCKSdW1zvNVmvA-i0if3o,9
|
439
|
+
metaflow-2.17.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|