metaflow 2.15.20__py2.py3-none-any.whl → 2.16.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- metaflow/__init__.py +7 -1
- metaflow/cli.py +16 -1
- metaflow/cli_components/init_cmd.py +1 -0
- metaflow/cli_components/run_cmds.py +6 -2
- metaflow/client/core.py +22 -30
- metaflow/datastore/task_datastore.py +0 -1
- metaflow/debug.py +5 -0
- metaflow/decorators.py +230 -70
- metaflow/extension_support/__init__.py +15 -8
- metaflow/extension_support/_empty_file.py +2 -2
- metaflow/flowspec.py +80 -53
- metaflow/graph.py +24 -2
- metaflow/meta_files.py +13 -0
- metaflow/metadata_provider/metadata.py +7 -1
- metaflow/metaflow_config.py +5 -0
- metaflow/metaflow_environment.py +82 -25
- metaflow/metaflow_version.py +1 -1
- metaflow/package/__init__.py +664 -0
- metaflow/packaging_sys/__init__.py +870 -0
- metaflow/packaging_sys/backend.py +113 -0
- metaflow/packaging_sys/distribution_support.py +153 -0
- metaflow/packaging_sys/tar_backend.py +86 -0
- metaflow/packaging_sys/utils.py +91 -0
- metaflow/packaging_sys/v1.py +476 -0
- metaflow/plugins/airflow/airflow.py +5 -1
- metaflow/plugins/airflow/airflow_cli.py +15 -4
- metaflow/plugins/argo/argo_workflows.py +23 -17
- metaflow/plugins/argo/argo_workflows_cli.py +16 -4
- metaflow/plugins/aws/batch/batch.py +22 -3
- metaflow/plugins/aws/batch/batch_cli.py +3 -0
- metaflow/plugins/aws/batch/batch_decorator.py +13 -5
- metaflow/plugins/aws/step_functions/step_functions.py +4 -1
- metaflow/plugins/aws/step_functions/step_functions_cli.py +15 -4
- metaflow/plugins/cards/card_decorator.py +0 -5
- metaflow/plugins/kubernetes/kubernetes.py +8 -1
- metaflow/plugins/kubernetes/kubernetes_cli.py +3 -0
- metaflow/plugins/kubernetes/kubernetes_decorator.py +13 -5
- metaflow/plugins/package_cli.py +25 -23
- metaflow/plugins/parallel_decorator.py +4 -2
- metaflow/plugins/pypi/bootstrap.py +8 -2
- metaflow/plugins/pypi/conda_decorator.py +39 -82
- metaflow/plugins/pypi/conda_environment.py +6 -2
- metaflow/plugins/pypi/pypi_decorator.py +4 -4
- metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
- metaflow/plugins/timeout_decorator.py +0 -1
- metaflow/plugins/uv/bootstrap.py +11 -0
- metaflow/plugins/uv/uv_environment.py +4 -2
- metaflow/pylint_wrapper.py +5 -1
- metaflow/runner/click_api.py +5 -4
- metaflow/runner/subprocess_manager.py +14 -2
- metaflow/runtime.py +37 -11
- metaflow/task.py +91 -7
- metaflow/user_configs/config_options.py +13 -8
- metaflow/user_configs/config_parameters.py +0 -4
- metaflow/user_decorators/__init__.py +0 -0
- metaflow/user_decorators/common.py +144 -0
- metaflow/user_decorators/mutable_flow.py +499 -0
- metaflow/user_decorators/mutable_step.py +424 -0
- metaflow/user_decorators/user_flow_decorator.py +263 -0
- metaflow/user_decorators/user_step_decorator.py +712 -0
- metaflow/util.py +4 -1
- metaflow/version.py +1 -1
- {metaflow-2.15.20.dist-info → metaflow-2.16.0.dist-info}/METADATA +2 -2
- {metaflow-2.15.20.dist-info → metaflow-2.16.0.dist-info}/RECORD +71 -60
- metaflow/info_file.py +0 -25
- metaflow/package.py +0 -203
- metaflow/user_configs/config_decorators.py +0 -568
- {metaflow-2.15.20.data → metaflow-2.16.0.data}/data/share/metaflow/devtools/Makefile +0 -0
- {metaflow-2.15.20.data → metaflow-2.16.0.data}/data/share/metaflow/devtools/Tiltfile +0 -0
- {metaflow-2.15.20.data → metaflow-2.16.0.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
- {metaflow-2.15.20.dist-info → metaflow-2.16.0.dist-info}/WHEEL +0 -0
- {metaflow-2.15.20.dist-info → metaflow-2.16.0.dist-info}/entry_points.txt +0 -0
- {metaflow-2.15.20.dist-info → metaflow-2.16.0.dist-info}/licenses/LICENSE +0 -0
- {metaflow-2.15.20.dist-info → metaflow-2.16.0.dist-info}/top_level.txt +0 -0
metaflow/task.py
CHANGED
@@ -6,6 +6,7 @@ import os
|
|
6
6
|
import time
|
7
7
|
import traceback
|
8
8
|
|
9
|
+
|
9
10
|
from types import MethodType, FunctionType
|
10
11
|
|
11
12
|
from metaflow.sidecar import Message, MessageTypes
|
@@ -24,6 +25,7 @@ from .unbounded_foreach import UBF_CONTROL
|
|
24
25
|
from .util import all_equal, get_username, resolve_identity, unicode_type
|
25
26
|
from .clone_util import clone_task_helper
|
26
27
|
from .metaflow_current import current
|
28
|
+
from metaflow.user_configs.config_parameters import ConfigValue
|
27
29
|
from metaflow.system import _system_logger, _system_monitor
|
28
30
|
from metaflow.tracing import get_trace_id
|
29
31
|
from metaflow.tuple_util import ForeachFrame
|
@@ -57,11 +59,89 @@ class MetaflowTask(object):
|
|
57
59
|
self.monitor = monitor
|
58
60
|
self.ubf_context = ubf_context
|
59
61
|
|
60
|
-
def _exec_step_function(self, step_function, input_obj=None):
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
62
|
+
def _exec_step_function(self, step_function, orig_step_func, input_obj=None):
|
63
|
+
wrappers_stack = []
|
64
|
+
wrapped_func = None
|
65
|
+
do_next = False
|
66
|
+
raised_exception = None
|
67
|
+
# If we have wrappers w1, w2 and w3, we need to execute
|
68
|
+
# - w3_pre
|
69
|
+
# - w2_pre
|
70
|
+
# - w1_pre
|
71
|
+
# - step_function
|
72
|
+
# - w1_post
|
73
|
+
# - w2_post
|
74
|
+
# - w3_post
|
75
|
+
# in that order. We do this by maintaining a stack of generators.
|
76
|
+
# Note that if any of the pre functions returns a function, we execute that
|
77
|
+
# instead of the rest of the inside part. This is useful if you want to create
|
78
|
+
# no-op function for example.
|
79
|
+
for w in reversed(orig_step_func.wrappers):
|
80
|
+
wrapped_func = w.pre_step(orig_step_func.name, self.flow, input_obj)
|
81
|
+
wrappers_stack.append(w)
|
82
|
+
if w.skip_step:
|
83
|
+
# We have nothing to run
|
84
|
+
do_next = w.skip_step
|
85
|
+
break
|
86
|
+
if wrapped_func:
|
87
|
+
break # We have nothing left to do since we now execute the
|
88
|
+
# wrapped function
|
89
|
+
# Else, we continue down the list of wrappers
|
90
|
+
try:
|
91
|
+
if not do_next:
|
92
|
+
if input_obj is None:
|
93
|
+
if wrapped_func:
|
94
|
+
do_next = wrapped_func(self.flow)
|
95
|
+
if not do_next:
|
96
|
+
do_next = True
|
97
|
+
else:
|
98
|
+
step_function()
|
99
|
+
else:
|
100
|
+
if wrapped_func:
|
101
|
+
do_next = wrapped_func(self.flow, input_obj)
|
102
|
+
if not do_next:
|
103
|
+
do_next = True
|
104
|
+
else:
|
105
|
+
step_function(input_obj)
|
106
|
+
except Exception as ex:
|
107
|
+
raised_exception = ex
|
108
|
+
|
109
|
+
if do_next:
|
110
|
+
# If we are skipping the step, or executed a wrapped function,
|
111
|
+
# we need to set the transition variables
|
112
|
+
# properly. We call the next function as needed
|
113
|
+
graph_node = self.flow._graph[step_function.name]
|
114
|
+
out_funcs = [getattr(self.flow, f) for f in graph_node.out_funcs]
|
115
|
+
if out_funcs:
|
116
|
+
if isinstance(do_next, bool):
|
117
|
+
# We need to extract things from the self.next. This is not possible
|
118
|
+
# in the case where there was a num_parallel.
|
119
|
+
if graph_node.parallel_foreach:
|
120
|
+
raise RuntimeError(
|
121
|
+
"Skipping a parallel foreach step without providing "
|
122
|
+
"the arguments to the self.next call is not supported. "
|
123
|
+
)
|
124
|
+
if graph_node.foreach_param:
|
125
|
+
self.flow.next(*out_funcs, foreach=graph_node.foreach_param)
|
126
|
+
else:
|
127
|
+
self.flow.next(*out_funcs)
|
128
|
+
elif isinstance(do_next, dict):
|
129
|
+
# Here it is a dictionary so we just call the next method with
|
130
|
+
# those arguments
|
131
|
+
self.flow.next(*out_funcs, **do_next)
|
132
|
+
else:
|
133
|
+
raise RuntimeError(
|
134
|
+
"Invalid value passed to self.next; expected "
|
135
|
+
" bool of a dictionary; got: %s" % do_next
|
136
|
+
)
|
137
|
+
# We back out of the stack of generators
|
138
|
+
for w in reversed(wrappers_stack):
|
139
|
+
raised_exception = w.post_step(
|
140
|
+
step_function.name, self.flow, raised_exception
|
141
|
+
)
|
142
|
+
if raised_exception:
|
143
|
+
# We have an exception that we need to propagate
|
144
|
+
raise raised_exception
|
65
145
|
|
66
146
|
def _init_parameters(self, parameter_ds, passdown=True):
|
67
147
|
cls = self.flow.__class__
|
@@ -538,6 +618,9 @@ class MetaflowTask(object):
|
|
538
618
|
output.save_metadata(
|
539
619
|
{
|
540
620
|
"task_begin": {
|
621
|
+
"code_package_metadata": os.environ.get(
|
622
|
+
"METAFLOW_CODE_METADATA", ""
|
623
|
+
),
|
541
624
|
"code_package_sha": os.environ.get("METAFLOW_CODE_SHA"),
|
542
625
|
"code_package_ds": os.environ.get("METAFLOW_CODE_DS"),
|
543
626
|
"code_package_url": os.environ.get("METAFLOW_CODE_URL"),
|
@@ -651,6 +734,7 @@ class MetaflowTask(object):
|
|
651
734
|
inputs,
|
652
735
|
)
|
653
736
|
|
737
|
+
orig_step_func = step_func
|
654
738
|
for deco in decorators:
|
655
739
|
# decorators can actually decorate the step function,
|
656
740
|
# or they can replace it altogether. This functionality
|
@@ -667,9 +751,9 @@ class MetaflowTask(object):
|
|
667
751
|
)
|
668
752
|
|
669
753
|
if join_type:
|
670
|
-
self._exec_step_function(step_func, input_obj)
|
754
|
+
self._exec_step_function(step_func, orig_step_func, input_obj)
|
671
755
|
else:
|
672
|
-
self._exec_step_function(step_func)
|
756
|
+
self._exec_step_function(step_func, orig_step_func)
|
673
757
|
|
674
758
|
for deco in decorators:
|
675
759
|
deco.task_post_step(
|
@@ -7,8 +7,9 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
7
7
|
from metaflow._vendor import click
|
8
8
|
from metaflow.debug import debug
|
9
9
|
|
10
|
-
from .config_parameters import
|
10
|
+
from .config_parameters import ConfigValue
|
11
11
|
from ..exception import MetaflowException, MetaflowInternalError
|
12
|
+
from ..packaging_sys import MetaflowCodeContent
|
12
13
|
from ..parameters import DeployTimeField, ParameterContext, current_flow
|
13
14
|
from ..util import get_username
|
14
15
|
|
@@ -24,12 +25,16 @@ _CONVERTED_DEFAULT_NO_FILE = _CONVERTED_DEFAULT + _NO_FILE
|
|
24
25
|
|
25
26
|
def _load_config_values(info_file: Optional[str] = None) -> Optional[Dict[Any, Any]]:
|
26
27
|
if info_file is None:
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
config_content = MetaflowCodeContent.get_config()
|
29
|
+
else:
|
30
|
+
try:
|
31
|
+
with open(info_file, encoding="utf-8") as f:
|
32
|
+
config_content = json.load(f)
|
33
|
+
except IOError:
|
34
|
+
return None
|
35
|
+
if config_content:
|
36
|
+
return config_content.get("user_configs", {})
|
37
|
+
return None
|
33
38
|
|
34
39
|
|
35
40
|
class ConvertPath(click.Path):
|
@@ -437,7 +442,7 @@ class LocalFileInput(click.Path):
|
|
437
442
|
# Small wrapper around click.Path to set the value from which to read configuration
|
438
443
|
# values. This is set immediately upon processing the --local-config-file
|
439
444
|
# option and will therefore then be available when processing any of the other
|
440
|
-
# --config options (which will call ConfigInput.process_configs
|
445
|
+
# --config options (which will call ConfigInput.process_configs)
|
441
446
|
name = "LocalFileInput"
|
442
447
|
|
443
448
|
def convert(self, value, param, ctx):
|
@@ -49,10 +49,6 @@ if TYPE_CHECKING:
|
|
49
49
|
|
50
50
|
# return tracefunc_closure
|
51
51
|
|
52
|
-
CONFIG_FILE = os.path.join(
|
53
|
-
os.path.dirname(os.path.abspath(__file__)), "CONFIG_PARAMETERS"
|
54
|
-
)
|
55
|
-
|
56
52
|
ID_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
57
53
|
|
58
54
|
UNPACK_KEY = "_unpacked_delayed_"
|
File without changes
|
@@ -0,0 +1,144 @@
|
|
1
|
+
from typing import Dict, Optional, List, Tuple
|
2
|
+
|
3
|
+
|
4
|
+
class _TrieNode:
|
5
|
+
def __init__(
|
6
|
+
self, parent: Optional["_TrieNode"] = None, component: Optional[str] = None
|
7
|
+
):
|
8
|
+
self.parent = parent
|
9
|
+
self.component = component
|
10
|
+
self.children = {} # type: Dict[str, "_TrieNode"]
|
11
|
+
self.total_children = 0
|
12
|
+
self.value = None
|
13
|
+
self.end_value = None
|
14
|
+
|
15
|
+
def traverse(self, value: type) -> Optional["_TrieNode"]:
|
16
|
+
if self.total_children == 0:
|
17
|
+
self.end_value = value
|
18
|
+
else:
|
19
|
+
self.end_value = None
|
20
|
+
self.total_children += 1
|
21
|
+
|
22
|
+
def remove_child(self, child_name: str) -> bool:
|
23
|
+
if child_name in self.children:
|
24
|
+
del self.children[child_name]
|
25
|
+
self.total_children -= 1
|
26
|
+
return True
|
27
|
+
return False
|
28
|
+
|
29
|
+
|
30
|
+
class ClassPath_Trie:
|
31
|
+
def __init__(self):
|
32
|
+
self.root = _TrieNode(None, None)
|
33
|
+
self.inited = False
|
34
|
+
self._value_to_node = {} # type: Dict[type, _TrieNode]
|
35
|
+
|
36
|
+
def init(self, initial_nodes: Optional[List[Tuple[str, type]]] = None):
|
37
|
+
# We need to do this so we can delay import of STEP_DECORATORS
|
38
|
+
self.inited = True
|
39
|
+
for classpath_name, value in initial_nodes or []:
|
40
|
+
self.insert(classpath_name, value)
|
41
|
+
|
42
|
+
def insert(self, classpath_name: str, value: type):
|
43
|
+
node = self.root
|
44
|
+
components = reversed(classpath_name.split("."))
|
45
|
+
for c in components:
|
46
|
+
node = node.children.setdefault(c, _TrieNode(node, c))
|
47
|
+
node.traverse(value)
|
48
|
+
node.total_children -= (
|
49
|
+
1 # We do not count the last node as having itself as a child
|
50
|
+
)
|
51
|
+
node.value = value
|
52
|
+
self._value_to_node[value] = node
|
53
|
+
|
54
|
+
def search(self, classpath_name: str) -> Optional[type]:
|
55
|
+
node = self.root
|
56
|
+
components = reversed(classpath_name.split("."))
|
57
|
+
for c in components:
|
58
|
+
if c not in node.children:
|
59
|
+
return None
|
60
|
+
node = node.children[c]
|
61
|
+
return node.value
|
62
|
+
|
63
|
+
def remove(self, classpath_name: str):
|
64
|
+
components = list(reversed(classpath_name.split(".")))
|
65
|
+
|
66
|
+
def _remove(node: _TrieNode, components, depth):
|
67
|
+
if depth == len(components):
|
68
|
+
if node.value is not None:
|
69
|
+
del self._value_to_node[node.value]
|
70
|
+
node.value = None
|
71
|
+
return len(node.children) == 0
|
72
|
+
return False
|
73
|
+
c = components[depth]
|
74
|
+
if c not in node.children:
|
75
|
+
return False
|
76
|
+
did_delete_child = _remove(node.children[c], components, depth + 1)
|
77
|
+
if did_delete_child:
|
78
|
+
node.remove_child(c)
|
79
|
+
if node.total_children == 1:
|
80
|
+
# If we have one total child left, we have at least one
|
81
|
+
# child and that one has an end_value
|
82
|
+
for child in node.children.values():
|
83
|
+
assert (
|
84
|
+
child.end_value
|
85
|
+
), "Node with one child must have an end_value"
|
86
|
+
node.end_value = child.end_value
|
87
|
+
return node.total_children == 0
|
88
|
+
return False
|
89
|
+
|
90
|
+
_remove(self.root, components, 0)
|
91
|
+
|
92
|
+
def unique_prefix_value(self, classpath_name: str) -> Optional[type]:
|
93
|
+
node = self.root
|
94
|
+
components = reversed(classpath_name.split("."))
|
95
|
+
for c in components:
|
96
|
+
if c not in node.children:
|
97
|
+
return None
|
98
|
+
node = node.children[c]
|
99
|
+
# If we reach here, it means the classpath_name is a prefix.
|
100
|
+
# We check if it has only one path forward (end_value will be non None)
|
101
|
+
# If value is not None, we also consider this to be a unique "prefix"
|
102
|
+
# This happens since this trie is also filled with metaflow default decorators
|
103
|
+
return node.end_value or node.value
|
104
|
+
|
105
|
+
def unique_prefix_for_type(self, value: type) -> Optional[str]:
|
106
|
+
node = self._value_to_node.get(value, None)
|
107
|
+
if node is None:
|
108
|
+
return None
|
109
|
+
components = []
|
110
|
+
while node:
|
111
|
+
if node.end_value == value:
|
112
|
+
components = []
|
113
|
+
if node.component is not None:
|
114
|
+
components.append(node.component)
|
115
|
+
node = node.parent
|
116
|
+
return ".".join(components)
|
117
|
+
|
118
|
+
def get_unique_prefixes(self) -> Dict[str, type]:
|
119
|
+
"""
|
120
|
+
Get all unique prefixes in the trie.
|
121
|
+
|
122
|
+
Returns
|
123
|
+
-------
|
124
|
+
List[str]
|
125
|
+
A list of unique prefixes.
|
126
|
+
"""
|
127
|
+
to_return = {}
|
128
|
+
|
129
|
+
def _collect(node, current_prefix):
|
130
|
+
if node.end_value is not None:
|
131
|
+
to_return[current_prefix] = node.end_value
|
132
|
+
# We stop there and don't look further since we found the unique prefix
|
133
|
+
return
|
134
|
+
if node.value is not None:
|
135
|
+
to_return[current_prefix] = node.value
|
136
|
+
# We continue to look for more unique prefixes
|
137
|
+
for child_name, child_node in node.children.items():
|
138
|
+
_collect(
|
139
|
+
child_node,
|
140
|
+
f"{current_prefix}.{child_name}" if current_prefix else child_name,
|
141
|
+
)
|
142
|
+
|
143
|
+
_collect(self.root, "")
|
144
|
+
return {".".join(reversed(k.split("."))): v for k, v in to_return.items()}
|