metaflow 2.12.18__py2.py3-none-any.whl → 2.12.20__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.
@@ -4,7 +4,9 @@ from typing import Any, Optional, TYPE_CHECKING
4
4
 
5
5
  from metaflow.metaflow_config import TEMPDIR
6
6
 
7
- Parallel = namedtuple("Parallel", ["main_ip", "num_nodes", "node_index"])
7
+ Parallel = namedtuple(
8
+ "Parallel", ["main_ip", "num_nodes", "node_index", "control_task_id"]
9
+ )
8
10
 
9
11
  if TYPE_CHECKING:
10
12
  import metaflow
@@ -1905,6 +1905,12 @@ class ArgoWorkflows(object):
1905
1905
  jobset.environment_variable(
1906
1906
  "MF_WORLD_SIZE", "{{inputs.parameters.num-parallel}}"
1907
1907
  )
1908
+ # We need this task-id set so that all the nodes are aware of the control
1909
+ # task's task-id. These "MF_" variables populate the `current.parallel` namedtuple
1910
+ jobset.environment_variable(
1911
+ "MF_PARALLEL_CONTROL_TASK_ID",
1912
+ "control-{{inputs.parameters.task-id-entropy}}-0",
1913
+ )
1908
1914
  # for k, v in .items():
1909
1915
  jobset.environment_variables_from_selectors(
1910
1916
  {
@@ -2552,8 +2558,8 @@ class ArgoWorkflows(object):
2552
2558
  cmd_str = " && ".join([init_cmds, heartbeat_cmds])
2553
2559
  cmds = shlex.split('bash -c "%s"' % cmd_str)
2554
2560
 
2555
- # TODO: Check that this is the minimal env.
2556
2561
  # Env required for sending heartbeats to the metadata service, nothing extra.
2562
+ # prod token / runtime info is required to correctly register flow branches
2557
2563
  env = {
2558
2564
  # These values are needed by Metaflow to set it's internal
2559
2565
  # state appropriately.
@@ -2565,7 +2571,10 @@ class ArgoWorkflows(object):
2565
2571
  "METAFLOW_USER": "argo-workflows",
2566
2572
  "METAFLOW_DEFAULT_DATASTORE": self.flow_datastore.TYPE,
2567
2573
  "METAFLOW_DEFAULT_METADATA": DEFAULT_METADATA,
2574
+ "METAFLOW_KUBERNETES_WORKLOAD": 1,
2575
+ "METAFLOW_RUNTIME_ENVIRONMENT": "kubernetes",
2568
2576
  "METAFLOW_OWNER": self.username,
2577
+ "METAFLOW_PRODUCTION_TOKEN": self.production_token,
2569
2578
  }
2570
2579
  # support Metaflow sandboxes
2571
2580
  env["METAFLOW_INIT_SCRIPT"] = KUBERNETES_SANDBOX_INIT_SCRIPT
@@ -89,6 +89,9 @@ class BatchJob(object):
89
89
  # Multinode
90
90
  if getattr(self, "num_parallel", 0) >= 1:
91
91
  num_nodes = self.num_parallel
92
+ # We need this task-id set so that all the nodes are aware of the control
93
+ # task's task-id. These "MF_" variables populate the `current.parallel` namedtuple
94
+ self.environment_variable("MF_PARALLEL_CONTROL_TASK_ID", self._task_id)
92
95
  main_task_override = copy.deepcopy(self.payload["containerOverrides"])
93
96
 
94
97
  # main
@@ -0,0 +1,25 @@
1
+ from metaflow.exception import CommandException
2
+ from metaflow.util import get_username, get_latest_run_id
3
+
4
+
5
+ def parse_cli_options(flow_name, run_id, user, my_runs, echo):
6
+ if user and my_runs:
7
+ raise CommandException("--user and --my-runs are mutually exclusive.")
8
+
9
+ if run_id and my_runs:
10
+ raise CommandException("--run_id and --my-runs are mutually exclusive.")
11
+
12
+ if my_runs:
13
+ user = get_username()
14
+
15
+ latest_run = True
16
+
17
+ if user and not run_id:
18
+ latest_run = False
19
+
20
+ if not run_id and latest_run:
21
+ run_id = get_latest_run_id(echo, flow_name)
22
+ if run_id is None:
23
+ raise CommandException("A previous run id was not found. Specify --run-id.")
24
+
25
+ return flow_name, run_id, user
@@ -401,6 +401,9 @@ class Kubernetes(object):
401
401
  .label("app.kubernetes.io/name", "metaflow-task")
402
402
  .label("app.kubernetes.io/part-of", "metaflow")
403
403
  )
404
+ # We need this task-id set so that all the nodes are aware of the control
405
+ # task's task-id. These "MF_" variables populate the `current.parallel` namedtuple
406
+ jobset.environment_variable("MF_PARALLEL_CONTROL_TASK_ID", str(task_id))
404
407
 
405
408
  ## ----------- control/worker specific values START here -----------
406
409
  # We will now set the appropriate command for the control/worker job
@@ -698,7 +701,7 @@ class Kubernetes(object):
698
701
  t = time.time()
699
702
  time.sleep(update_delay(time.time() - start_time))
700
703
 
701
- _make_prefix = lambda: b"[%s] " % util.to_bytes(self._job.id)
704
+ prefix = lambda: b"[%s] " % util.to_bytes(self._job.id)
702
705
 
703
706
  stdout_tail = get_log_tailer(stdout_location, self._datastore.TYPE)
704
707
  stderr_tail = get_log_tailer(stderr_location, self._datastore.TYPE)
@@ -708,7 +711,7 @@ class Kubernetes(object):
708
711
 
709
712
  # 2) Tail logs until the job has finished
710
713
  tail_logs(
711
- prefix=_make_prefix(),
714
+ prefix=prefix(),
712
715
  stdout_tail=stdout_tail,
713
716
  stderr_tail=stderr_tail,
714
717
  echo=echo,
@@ -3,10 +3,12 @@ import sys
3
3
  import time
4
4
  import traceback
5
5
 
6
+ from metaflow.plugins.kubernetes.kube_utils import parse_cli_options
7
+ from metaflow.plugins.kubernetes.kubernetes_client import KubernetesClient
6
8
  import metaflow.tracing as tracing
7
9
  from metaflow import JSONTypeClass, util
8
10
  from metaflow._vendor import click
9
- from metaflow.exception import METAFLOW_EXIT_DISALLOW_RETRY, CommandException
11
+ from metaflow.exception import METAFLOW_EXIT_DISALLOW_RETRY, MetaflowException
10
12
  from metaflow.metadata.util import sync_local_metadata_from_datastore
11
13
  from metaflow.metaflow_config import DATASTORE_LOCAL_DIR, KUBERNETES_LABELS
12
14
  from metaflow.mflog import TASK_LOG_SOURCE
@@ -305,3 +307,84 @@ def step(
305
307
  sys.exit(METAFLOW_EXIT_DISALLOW_RETRY)
306
308
  finally:
307
309
  _sync_metadata()
310
+
311
+
312
+ @kubernetes.command(help="List unfinished Kubernetes tasks of this flow.")
313
+ @click.option(
314
+ "--my-runs",
315
+ default=False,
316
+ is_flag=True,
317
+ help="List all my unfinished tasks.",
318
+ )
319
+ @click.option("--user", default=None, help="List unfinished tasks for the given user.")
320
+ @click.option(
321
+ "--run-id",
322
+ default=None,
323
+ help="List unfinished tasks corresponding to the run id.",
324
+ )
325
+ @click.pass_obj
326
+ def list(obj, run_id, user, my_runs):
327
+ flow_name, run_id, user = parse_cli_options(
328
+ obj.flow.name, run_id, user, my_runs, obj.echo
329
+ )
330
+ kube_client = KubernetesClient()
331
+ pods = kube_client.list(obj.flow.name, run_id, user)
332
+
333
+ def format_timestamp(timestamp=None):
334
+ if timestamp is None:
335
+ return "-"
336
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
337
+
338
+ for pod in pods:
339
+ obj.echo(
340
+ "Run: *{run_id}* "
341
+ "Pod: *{pod_id}* "
342
+ "Started At: {startedAt} "
343
+ "Status: *{status}*".format(
344
+ run_id=pod.metadata.annotations.get(
345
+ "metaflow/run_id",
346
+ pod.metadata.labels.get("workflows.argoproj.io/workflow"),
347
+ ),
348
+ pod_id=pod.metadata.name,
349
+ startedAt=format_timestamp(pod.status.start_time),
350
+ status=pod.status.phase,
351
+ )
352
+ )
353
+
354
+ if not pods:
355
+ obj.echo("No active Kubernetes pods found.")
356
+
357
+
358
+ @kubernetes.command(
359
+ help="Terminate unfinished Kubernetes tasks of this flow. Killed pods may result in newer attempts when using @retry."
360
+ )
361
+ @click.option(
362
+ "--my-runs",
363
+ default=False,
364
+ is_flag=True,
365
+ help="Kill all my unfinished tasks.",
366
+ )
367
+ @click.option(
368
+ "--user",
369
+ default=None,
370
+ help="Terminate unfinished tasks for the given user.",
371
+ )
372
+ @click.option(
373
+ "--run-id",
374
+ default=None,
375
+ help="Terminate unfinished tasks corresponding to the run id.",
376
+ )
377
+ @click.pass_obj
378
+ def kill(obj, run_id, user, my_runs):
379
+ flow_name, run_id, user = parse_cli_options(
380
+ obj.flow.name, run_id, user, my_runs, obj.echo
381
+ )
382
+
383
+ if run_id is not None and run_id.startswith("argo-") or user == "argo-workflows":
384
+ raise MetaflowException(
385
+ "Killing pods launched by Argo Workflows is not supported. "
386
+ "Use *argo-workflows terminate* instead."
387
+ )
388
+
389
+ kube_client = KubernetesClient()
390
+ kube_client.kill_pods(flow_name, run_id, user, obj.echo)
@@ -1,8 +1,10 @@
1
+ from concurrent.futures import ThreadPoolExecutor
1
2
  import os
2
3
  import sys
3
4
  import time
4
5
 
5
6
  from metaflow.exception import MetaflowException
7
+ from metaflow.metaflow_config import KUBERNETES_NAMESPACE
6
8
 
7
9
  from .kubernetes_job import KubernetesJob, KubernetesJobSet
8
10
 
@@ -28,6 +30,7 @@ class KubernetesClient(object):
28
30
  % sys.executable
29
31
  )
30
32
  self._refresh_client()
33
+ self._namespace = KUBERNETES_NAMESPACE
31
34
 
32
35
  def _refresh_client(self):
33
36
  from kubernetes import client, config
@@ -60,6 +63,100 @@ class KubernetesClient(object):
60
63
 
61
64
  return self._client
62
65
 
66
+ def _find_active_pods(self, flow_name, run_id=None, user=None):
67
+ def _request(_continue=None):
68
+ # handle paginated responses
69
+ return self._client.CoreV1Api().list_namespaced_pod(
70
+ namespace=self._namespace,
71
+ # limited selector support for K8S api. We want to cover multiple statuses: Running / Pending / Unknown
72
+ field_selector="status.phase!=Succeeded,status.phase!=Failed",
73
+ limit=1000,
74
+ _continue=_continue,
75
+ )
76
+
77
+ results = _request()
78
+
79
+ if run_id is not None:
80
+ # handle argo prefixes in run_id
81
+ run_id = run_id[run_id.startswith("argo-") and len("argo-") :]
82
+
83
+ while results.metadata._continue or results.items:
84
+ for pod in results.items:
85
+ match = (
86
+ # arbitrary pods might have no annotations at all.
87
+ pod.metadata.annotations
88
+ and pod.metadata.labels
89
+ and (
90
+ run_id is None
91
+ or (pod.metadata.annotations.get("metaflow/run_id") == run_id)
92
+ # we want to also match pods launched by argo-workflows
93
+ or (
94
+ pod.metadata.labels.get("workflows.argoproj.io/workflow")
95
+ == run_id
96
+ )
97
+ )
98
+ and (
99
+ user is None
100
+ or pod.metadata.annotations.get("metaflow/user") == user
101
+ )
102
+ and (
103
+ pod.metadata.annotations.get("metaflow/flow_name") == flow_name
104
+ )
105
+ )
106
+ if match:
107
+ yield pod
108
+ if not results.metadata._continue:
109
+ break
110
+ results = _request(results.metadata._continue)
111
+
112
+ def list(self, flow_name, run_id, user):
113
+ results = self._find_active_pods(flow_name, run_id, user)
114
+
115
+ return list(results)
116
+
117
+ def kill_pods(self, flow_name, run_id, user, echo):
118
+ from kubernetes.stream import stream
119
+
120
+ api_instance = self._client.CoreV1Api()
121
+ job_api = self._client.BatchV1Api()
122
+ pods = self._find_active_pods(flow_name, run_id, user)
123
+
124
+ def _kill_pod(pod):
125
+ echo("Killing Kubernetes pod %s\n" % pod.metadata.name)
126
+ try:
127
+ stream(
128
+ api_instance.connect_get_namespaced_pod_exec,
129
+ name=pod.metadata.name,
130
+ namespace=pod.metadata.namespace,
131
+ command=[
132
+ "/bin/sh",
133
+ "-c",
134
+ "/sbin/killall5",
135
+ ],
136
+ stderr=True,
137
+ stdin=False,
138
+ stdout=True,
139
+ tty=False,
140
+ )
141
+ except Exception:
142
+ # best effort kill for pod can fail.
143
+ try:
144
+ job_name = pod.metadata.labels.get("job-name", None)
145
+ if job_name is None:
146
+ raise Exception("Could not determine job name")
147
+
148
+ job_api.patch_namespaced_job(
149
+ name=job_name,
150
+ namespace=pod.metadata.namespace,
151
+ field_manager="metaflow",
152
+ body={"spec": {"parallelism": 0}},
153
+ )
154
+ except Exception as e:
155
+ echo("failed to kill pod %s - %s" % (pod.metadata.name, str(e)))
156
+
157
+ with ThreadPoolExecutor() as executor:
158
+ executor.map(_kill_pod, list(pods))
159
+
63
160
  def jobset(self, **kwargs):
64
161
  return KubernetesJobSet(self, **kwargs)
65
162
 
@@ -70,6 +70,10 @@ class KubernetesDecorator(StepDecorator):
70
70
  Kubernetes secrets to use when launching pod in Kubernetes. These
71
71
  secrets are in addition to the ones defined in `METAFLOW_KUBERNETES_SECRETS`
72
72
  in Metaflow configuration.
73
+ node_selector: Union[Dict[str,str], str], optional, default None
74
+ Kubernetes node selector(s) to apply to the pod running the task.
75
+ Can be passed in as a comma separated string of values e.g. "kubernetes.io/os=linux,kubernetes.io/arch=amd64"
76
+ or as a dictionary {"kubernetes.io/os": "linux", "kubernetes.io/arch": "amd64"}
73
77
  namespace : str, default METAFLOW_KUBERNETES_NAMESPACE
74
78
  Kubernetes namespace to use when launching pod in Kubernetes.
75
79
  gpu : int, optional, default None
@@ -571,10 +571,8 @@ class JobSetSpec(object):
571
571
  namespace=self._kwargs["namespace"],
572
572
  ),
573
573
  spec=client.V1PodSpec(
574
- ## --- jobset require podspec deets start----
575
574
  subdomain=self._kwargs["subdomain"],
576
575
  set_hostname_as_fqdn=True,
577
- ## --- jobset require podspec deets end ----
578
576
  # Timeout is set on the pod and not the job (important!)
579
577
  active_deadline_seconds=self._kwargs[
580
578
  "timeout_in_seconds"
@@ -24,6 +24,8 @@ class ParallelDecorator(StepDecorator):
24
24
  The total number of tasks created by @parallel
25
25
  - node_index : int
26
26
  The index of the current task in all the @parallel tasks.
27
+ - control_task_id : Optional[str]
28
+ The task ID of the control task. Available to all tasks.
27
29
 
28
30
  is_parallel -> bool
29
31
  True if the current step is a @parallel step.
@@ -67,6 +69,7 @@ class ParallelDecorator(StepDecorator):
67
69
  main_ip=os.environ.get("MF_PARALLEL_MAIN_IP", "127.0.0.1"),
68
70
  num_nodes=int(os.environ.get("MF_PARALLEL_NUM_NODES", "1")),
69
71
  node_index=int(os.environ.get("MF_PARALLEL_NODE_INDEX", "0")),
72
+ control_task_id=os.environ.get("MF_PARALLEL_CONTROL_TASK_ID", None),
70
73
  )
71
74
  ),
72
75
  )
@@ -177,6 +180,7 @@ def _local_multinode_control_task_step_func(
177
180
  num_parallel = foreach_iter.num_parallel
178
181
  os.environ["MF_PARALLEL_NUM_NODES"] = str(num_parallel)
179
182
  os.environ["MF_PARALLEL_MAIN_IP"] = "127.0.0.1"
183
+ os.environ["MF_PARALLEL_CONTROL_TASK_ID"] = str(current.task_id)
180
184
 
181
185
  run_id = current.run_id
182
186
  step_name = current.step_name
@@ -103,6 +103,7 @@ if __name__ == "__main__":
103
103
  echo "@EXPLICIT" > "$tmpfile";
104
104
  ls -d {conda_pkgs_dir}/*/* >> "$tmpfile";
105
105
  export PATH=$PATH:$(pwd)/micromamba;
106
+ export CONDA_PKGS_DIRS=$(pwd)/micromamba/pkgs;
106
107
  micromamba create --yes --offline --no-deps --safety-checks=disabled --no-extra-safety-checks --prefix {prefix} --file "$tmpfile";
107
108
  rm "$tmpfile"''',
108
109
  ]
@@ -123,6 +124,7 @@ if __name__ == "__main__":
123
124
  [
124
125
  f"""set -e;
125
126
  export PATH=$PATH:$(pwd)/micromamba;
127
+ export CONDA_PKGS_DIRS=$(pwd)/micromamba/pkgs;
126
128
  micromamba run --prefix {prefix} python -m pip --disable-pip-version-check install --root-user-action=ignore --no-compile {pypi_pkgs_dir}/*.whl --no-user"""
127
129
  ]
128
130
  )
@@ -50,6 +50,9 @@ class CondaStepDecorator(StepDecorator):
50
50
  # conda channels, users can specify channel::package as the package name.
51
51
 
52
52
  def __init__(self, attributes=None, statically_defined=False):
53
+ self._user_defined_attributes = (
54
+ attributes.copy() if attributes is not None else {}
55
+ )
53
56
  super(CondaStepDecorator, self).__init__(attributes, statically_defined)
54
57
 
55
58
  # Support legacy 'libraries=' attribute for the decorator.
@@ -59,6 +62,9 @@ class CondaStepDecorator(StepDecorator):
59
62
  }
60
63
  del self.attributes["libraries"]
61
64
 
65
+ def is_attribute_user_defined(self, name):
66
+ return name in self._user_defined_attributes
67
+
62
68
  def step_init(self, flow, graph, step, decos, environment, flow_datastore, logger):
63
69
  # The init_environment hook for Environment creates the relevant virtual
64
70
  # environments. The step_init hook sets up the relevant state for that hook to
@@ -71,11 +77,16 @@ class CondaStepDecorator(StepDecorator):
71
77
 
72
78
  # Support flow-level decorator.
73
79
  if "conda_base" in self.flow._flow_decorators:
74
- super_attributes = self.flow._flow_decorators["conda_base"][0].attributes
80
+ conda_base = self.flow._flow_decorators["conda_base"][0]
81
+ super_attributes = conda_base.attributes
75
82
  self.attributes["packages"] = {
76
83
  **super_attributes["packages"],
77
84
  **self.attributes["packages"],
78
85
  }
86
+ self._user_defined_attributes = {
87
+ **self._user_defined_attributes,
88
+ **conda_base._user_defined_attributes,
89
+ }
79
90
  self.attributes["python"] = (
80
91
  self.attributes["python"] or super_attributes["python"]
81
92
  )
@@ -322,6 +333,9 @@ class CondaFlowDecorator(FlowDecorator):
322
333
  }
323
334
 
324
335
  def __init__(self, attributes=None, statically_defined=False):
336
+ self._user_defined_attributes = (
337
+ attributes.copy() if attributes is not None else {}
338
+ )
325
339
  super(CondaFlowDecorator, self).__init__(attributes, statically_defined)
326
340
 
327
341
  # Support legacy 'libraries=' attribute for the decorator.
@@ -333,9 +347,18 @@ class CondaFlowDecorator(FlowDecorator):
333
347
  if self.attributes["python"]:
334
348
  self.attributes["python"] = str(self.attributes["python"])
335
349
 
350
+ def is_attribute_user_defined(self, name):
351
+ return name in self._user_defined_attributes
352
+
336
353
  def flow_init(
337
354
  self, flow, graph, environment, flow_datastore, metadata, logger, echo, options
338
355
  ):
356
+ # NOTE: Important for extensions implementing custom virtual environments.
357
+ # Without this steps will not have an implicit conda step decorator on them unless the environment adds one in its decospecs.
358
+ from metaflow import decorators
359
+
360
+ decorators._attach_decorators(flow, ["conda"])
361
+
339
362
  # @conda uses a conda environment to create a virtual environment.
340
363
  # The conda environment can be created through micromamba.
341
364
  _supported_virtual_envs = ["conda"]
@@ -40,7 +40,12 @@ class PyPIStepDecorator(StepDecorator):
40
40
 
41
41
  # Support flow-level decorator
42
42
  if "pypi_base" in self.flow._flow_decorators:
43
- super_attributes = self.flow._flow_decorators["pypi_base"][0].attributes
43
+ pypi_base = self.flow._flow_decorators["pypi_base"][0]
44
+ super_attributes = pypi_base.attributes
45
+ self._user_defined_attributes = {
46
+ **self._user_defined_attributes,
47
+ **pypi_base._user_defined_attributes,
48
+ }
44
49
  self.attributes["packages"] = {
45
50
  **super_attributes["packages"],
46
51
  **self.attributes["packages"],
@@ -93,6 +98,12 @@ class PyPIStepDecorator(StepDecorator):
93
98
  ),
94
99
  )
95
100
  )
101
+ # TODO: This code snippet can be done away with by altering the constructor of
102
+ # MetaflowEnvironment. A good first-task exercise.
103
+ # Avoid circular import
104
+ from metaflow.plugins.datastores.local_storage import LocalStorage
105
+
106
+ environment.set_local_root(LocalStorage.get_datastore_root_from_config(logger))
96
107
 
97
108
  def is_attribute_user_defined(self, name):
98
109
  return name in self._user_defined_attributes
@@ -117,6 +128,12 @@ class PyPIFlowDecorator(FlowDecorator):
117
128
  name = "pypi_base"
118
129
  defaults = {"packages": {}, "python": None, "disabled": None}
119
130
 
131
+ def __init__(self, attributes=None, statically_defined=False):
132
+ self._user_defined_attributes = (
133
+ attributes.copy() if attributes is not None else {}
134
+ )
135
+ super().__init__(attributes, statically_defined)
136
+
120
137
  def flow_init(
121
138
  self, flow, graph, environment, flow_datastore, metadata, logger, echo, options
122
139
  ):
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import sys
2
3
 
3
4
  if sys.version_info < (3, 7):
@@ -12,6 +13,7 @@ import importlib
12
13
  import inspect
13
14
  import itertools
14
15
  import uuid
16
+ import json
15
17
  from collections import OrderedDict
16
18
  from typing import Any, Callable, Dict, List, Optional
17
19
  from typing import OrderedDict as TOrderedDict
@@ -37,6 +39,9 @@ from metaflow.exception import MetaflowException
37
39
  from metaflow.includefile import FilePathClass
38
40
  from metaflow.parameters import JSONTypeClass, flow_context
39
41
 
42
+ # Define a recursive type alias for JSON
43
+ JSON = Union[Dict[str, "JSON"], List["JSON"], str, int, float, bool, None]
44
+
40
45
  click_to_python_types = {
41
46
  StringParamType: str,
42
47
  IntParamType: int,
@@ -48,7 +53,7 @@ click_to_python_types = {
48
53
  Tuple: tuple,
49
54
  Choice: str,
50
55
  File: str,
51
- JSONTypeClass: str,
56
+ JSONTypeClass: JSON,
52
57
  FilePathClass: str,
53
58
  }
54
59
 
@@ -82,6 +87,11 @@ def _method_sanity_check(
82
87
  % (supplied_k, annotations[supplied_k], defaults[supplied_k])
83
88
  )
84
89
 
90
+ # because Click expects stringified JSON..
91
+ supplied_v = (
92
+ json.dumps(supplied_v) if annotations[supplied_k] == JSON else supplied_v
93
+ )
94
+
85
95
  if supplied_k in possible_arg_params:
86
96
  cli_name = possible_arg_params[supplied_k].opts[0].strip("-")
87
97
  method_params["args"][cli_name] = supplied_v
@@ -142,6 +152,8 @@ loaded_modules = {}
142
152
 
143
153
 
144
154
  def extract_flow_class_from_file(flow_file: str) -> FlowSpec:
155
+ if not os.path.exists(flow_file):
156
+ raise FileNotFoundError("Flow file not present at '%s'" % flow_file)
145
157
  # Check if the module has already been loaded
146
158
  if flow_file in loaded_modules:
147
159
  module = loaded_modules[flow_file]
@@ -5,6 +5,8 @@ import time
5
5
  import importlib
6
6
  import functools
7
7
  import tempfile
8
+
9
+ from subprocess import CalledProcessError
8
10
  from typing import Optional, Dict, ClassVar
9
11
 
10
12
  from metaflow.exception import MetaflowNotFound
@@ -25,6 +27,8 @@ def handle_timeout(
25
27
  Temporary file that stores runner attribute data.
26
28
  command_obj : CommandManager
27
29
  Command manager object that encapsulates the running command details.
30
+ file_read_timeout : int
31
+ Timeout for reading the file.
28
32
 
29
33
  Returns
30
34
  -------
@@ -39,10 +43,10 @@ def handle_timeout(
39
43
  """
40
44
  try:
41
45
  content = read_from_file_when_ready(
42
- tfp_runner_attribute.name, timeout=file_read_timeout
46
+ tfp_runner_attribute.name, command_obj, timeout=file_read_timeout
43
47
  )
44
48
  return content
45
- except TimeoutError as e:
49
+ except (CalledProcessError, TimeoutError) as e:
46
50
  stdout_log = open(command_obj.log_files["stdout"]).read()
47
51
  stderr_log = open(command_obj.log_files["stderr"]).read()
48
52
  command = " ".join(command_obj.command)
@@ -397,6 +401,9 @@ class DeployerImpl(object):
397
401
  self.name = content.get("name")
398
402
  self.flow_name = content.get("flow_name")
399
403
  self.metadata = content.get("metadata")
404
+ # Additional info is used to pass additional deployer specific information.
405
+ # It is used in non-OSS deployers (extensions).
406
+ self.additional_info = content.get("additional_info", {})
400
407
 
401
408
  if command_obj.process.returncode == 0:
402
409
  deployed_flow = DeployedFlow(deployer=self)
@@ -2,6 +2,8 @@ import importlib
2
2
  import os
3
3
  import sys
4
4
  import tempfile
5
+
6
+ from subprocess import CalledProcessError
5
7
  from typing import Dict, Iterator, Optional, Tuple
6
8
 
7
9
  from metaflow import Run, metadata
@@ -275,13 +277,13 @@ class Runner(object):
275
277
 
276
278
  # Set the correct metadata from the runner_attribute file corresponding to this run.
277
279
  content = read_from_file_when_ready(
278
- tfp_runner_attribute.name, timeout=self.file_read_timeout
280
+ tfp_runner_attribute.name, command_obj, timeout=self.file_read_timeout
279
281
  )
280
282
  metadata_for_flow, pathspec = content.rsplit(":", maxsplit=1)
281
283
  metadata(metadata_for_flow)
282
284
  run_object = Run(pathspec, _namespace_check=False)
283
285
  return ExecutingRun(self, command_obj, run_object)
284
- except TimeoutError as e:
286
+ except (CalledProcessError, TimeoutError) as e:
285
287
  stdout_log = open(command_obj.log_files["stdout"]).read()
286
288
  stderr_log = open(command_obj.log_files["stderr"]).read()
287
289
  command = " ".join(command_obj.command)
@@ -1,18 +1,22 @@
1
1
  import asyncio
2
2
  import os
3
+ import time
3
4
  import shutil
4
5
  import signal
5
6
  import subprocess
6
7
  import sys
7
8
  import tempfile
8
9
  import threading
9
- import time
10
10
  from typing import Callable, Dict, Iterator, List, Optional, Tuple
11
11
 
12
12
 
13
13
  def kill_process_and_descendants(pid, termination_timeout):
14
+ # TODO: there's a race condition that new descendants might
15
+ # spawn b/w the invocations of 'pkill' and 'kill'.
16
+ # Needs to be fixed in future.
14
17
  try:
15
18
  subprocess.check_call(["pkill", "-TERM", "-P", str(pid)])
19
+ subprocess.check_call(["kill", "-TERM", str(pid)])
16
20
  except subprocess.CalledProcessError:
17
21
  pass
18
22
 
@@ -20,6 +24,7 @@ def kill_process_and_descendants(pid, termination_timeout):
20
24
 
21
25
  try:
22
26
  subprocess.check_call(["pkill", "-KILL", "-P", str(pid)])
27
+ subprocess.check_call(["kill", "-KILL", str(pid)])
23
28
  except subprocess.CalledProcessError:
24
29
  pass
25
30
 
@@ -436,13 +441,13 @@ class CommandManager(object):
436
441
  if self.run_called:
437
442
  shutil.rmtree(self.temp_dir, ignore_errors=True)
438
443
 
439
- async def kill(self, termination_timeout: float = 1):
444
+ async def kill(self, termination_timeout: float = 5):
440
445
  """
441
446
  Kill the subprocess and its descendants.
442
447
 
443
448
  Parameters
444
449
  ----------
445
- termination_timeout : float, default 1
450
+ termination_timeout : float, default 5
446
451
  The time to wait after sending a SIGTERM to the process and its descendants
447
452
  before sending a SIGKILL.
448
453
  """
metaflow/runner/utils.py CHANGED
@@ -1,7 +1,12 @@
1
1
  import os
2
2
  import ast
3
3
  import time
4
- from typing import Dict
4
+
5
+ from subprocess import CalledProcessError
6
+ from typing import Dict, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from .subprocess_manager import CommandManager
5
10
 
6
11
 
7
12
  def get_current_cell(ipython):
@@ -35,11 +40,23 @@ def clear_and_set_os_environ(env: Dict):
35
40
  os.environ.update(env)
36
41
 
37
42
 
38
- def read_from_file_when_ready(file_path: str, timeout: float = 5):
43
+ def read_from_file_when_ready(
44
+ file_path: str, command_obj: "CommandManager", timeout: float = 5
45
+ ):
39
46
  start_time = time.time()
40
47
  with open(file_path, "r", encoding="utf-8") as file_pointer:
41
48
  content = file_pointer.read()
42
49
  while not content:
50
+ if command_obj.process.poll() is not None:
51
+ # Check to make sure the file hasn't been read yet to avoid a race
52
+ # where the file is written between the end of this while loop and the
53
+ # poll call above.
54
+ content = file_pointer.read()
55
+ if content:
56
+ break
57
+ raise CalledProcessError(
58
+ command_obj.process.returncode, command_obj.command
59
+ )
43
60
  if time.time() - start_time > timeout:
44
61
  raise TimeoutError(
45
62
  "Timeout while waiting for file content from '%s'" % file_path
metaflow/version.py CHANGED
@@ -1 +1 @@
1
- metaflow_version = "2.12.18"
1
+ metaflow_version = "2.12.20"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: metaflow
3
- Version: 2.12.18
3
+ Version: 2.12.20
4
4
  Summary: Metaflow: More Data Science, 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.12.18; extra == "stubs"
29
+ Requires-Dist: metaflow-stubs==2.12.20; extra == "stubs"
30
30
 
31
31
  ![Metaflow_Logo_Horizontal_FullColor_Ribbon_Dark_RGB](https://user-images.githubusercontent.com/763451/89453116-96a57e00-d713-11ea-9fa6-82b29d4d6eff.png)
32
32
 
@@ -17,7 +17,7 @@ metaflow/integrations.py,sha256=LlsaoePRg03DjENnmLxZDYto3NwWc9z_PtU6nJxLldg,1480
17
17
  metaflow/lint.py,sha256=5rj1MlpluxyPTSINjtMoJ7viotyNzfjtBJSAihlAwMU,10870
18
18
  metaflow/metaflow_config.py,sha256=4PUd2-JWJs35SaobDgMg4RdpZaKjhEqPUFh2f0pjqnU,22853
19
19
  metaflow/metaflow_config_funcs.py,sha256=5GlvoafV6SxykwfL8D12WXSfwjBN_NsyuKE_Q3gjGVE,6738
20
- metaflow/metaflow_current.py,sha256=5Kri7fzj-rtIJVr5xh5kPKwZ0T73_4egZybzlDR-fgc,7136
20
+ metaflow/metaflow_current.py,sha256=pC-EMnAsnvBLvLd61W6MvfiCKcboryeui9f6r8z_sg8,7161
21
21
  metaflow/metaflow_environment.py,sha256=D7zHYqe8aLZVwJ20nx19vmsNW29Kf3PVE8hbBjVQin8,8115
22
22
  metaflow/metaflow_profile.py,sha256=jKPEW-hmAQO-htSxb9hXaeloLacAh41A35rMZH6G8pA,418
23
23
  metaflow/metaflow_version.py,sha256=mPQ6g_3XjNdi0NrxDzwlW8ZH0nMyYpwqmJ04P7TIdP0,4774
@@ -35,7 +35,7 @@ metaflow/tuple_util.py,sha256=_G5YIEhuugwJ_f6rrZoelMFak3DqAR2tt_5CapS1XTY,830
35
35
  metaflow/unbounded_foreach.py,sha256=p184WMbrMJ3xKYHwewj27ZhRUsSj_kw1jlye5gA9xJk,387
36
36
  metaflow/util.py,sha256=olAvJK3y1it_k99MhLulTaAJo7OFVt5rnrD-ulIFLCU,13616
37
37
  metaflow/vendor.py,sha256=FchtA9tH22JM-eEtJ2c9FpUdMn8sSb1VHuQS56EcdZk,5139
38
- metaflow/version.py,sha256=ENRBNUxFkqFdAswpvc-TIKbgracGkrn4SmFnZmnIlPI,29
38
+ metaflow/version.py,sha256=kleNZLjxgidWFnFZyqIbSBKOHUU3wMG2-4RRKGiPktc,29
39
39
  metaflow/_vendor/__init__.py,sha256=y_CiwUD3l4eAKvTVDZeqgVujMy31cAM1qjAB-HfI-9s,353
40
40
  metaflow/_vendor/typing_extensions.py,sha256=0nUs5p1A_UrZigrAVBoOEM6TxU37zzPDUtiij1ZwpNc,110417
41
41
  metaflow/_vendor/zipp.py,sha256=ajztOH-9I7KA_4wqDYygtHa6xUBVZgFpmZ8FE74HHHI,8425
@@ -150,7 +150,7 @@ metaflow/plugins/environment_decorator.py,sha256=6m9j2B77d-Ja_l_9CTJ__0O6aB2a8Qt
150
150
  metaflow/plugins/events_decorator.py,sha256=c2GcH6Mspbey3wBkjM5lqxaNByFOzYDQdllLpXzRNv8,18283
151
151
  metaflow/plugins/logs_cli.py,sha256=77W5UNagU2mOKSMMvrQxQmBLRzvmjK-c8dWxd-Ygbqs,11410
152
152
  metaflow/plugins/package_cli.py,sha256=-J6D4cupHfWSZ4GEFo2yy9Je9oL3owRWm5pEJwaiqd4,1649
153
- metaflow/plugins/parallel_decorator.py,sha256=wXBmEnnOmWI46-Lcry_CArkujj6YOrfzAwVVw1kgpPI,8591
153
+ metaflow/plugins/parallel_decorator.py,sha256=hRYTdw3iJjxBEws2rtrZaCHojOaDoO0Pu08ckl-Z5bU,8876
154
154
  metaflow/plugins/project_decorator.py,sha256=eJOe0Ea7CbUCReEhR_XQvRkhV6jyRqDxM72oZI7EMCk,5336
155
155
  metaflow/plugins/resources_decorator.py,sha256=3MMZ7uptDf99795_RcSOq4h0N3OFlKpd3ahIEsozBBs,1333
156
156
  metaflow/plugins/retry_decorator.py,sha256=tz_2Tq6GLg3vjDBZp0KKVTk3ADlCvqaWTSf7blmFdUw,1548
@@ -174,7 +174,7 @@ metaflow/plugins/airflow/sensors/s3_sensor.py,sha256=iDReG-7FKnumrtQg-HY6cCUAAqN
174
174
  metaflow/plugins/argo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
175
175
  metaflow/plugins/argo/argo_client.py,sha256=MKKhMCbWOPzf6z5zQQiyDRHHkAXcO7ipboDZDqAAvOk,15849
176
176
  metaflow/plugins/argo/argo_events.py,sha256=_C1KWztVqgi3zuH57pInaE9OzABc2NnncC-zdwOMZ-w,5909
177
- metaflow/plugins/argo/argo_workflows.py,sha256=6xUkz1LKdCLbl-O-D83Y2G5mCKYcIciKts3x1PNAzCk,170173
177
+ metaflow/plugins/argo/argo_workflows.py,sha256=8YajGVnvBjGC22L308p8zMXzvfJP0fI9RsxGl46nO1k,170748
178
178
  metaflow/plugins/argo/argo_workflows_cli.py,sha256=X2j_F0xF8-K30ebM4dSLOTteDKXbr-jMN18oMpl5S6Y,36313
179
179
  metaflow/plugins/argo/argo_workflows_decorator.py,sha256=yprszMdbE3rBTcEA9VR0IEnPjTprUauZBc4SBb-Q7sA,7878
180
180
  metaflow/plugins/argo/argo_workflows_deployer.py,sha256=wSSZtThn_VPvE_Wu6NB1L0Q86LmBJh9g009v_lpvBPM,8125
@@ -188,7 +188,7 @@ metaflow/plugins/aws/aws_utils.py,sha256=dk92IRZ2QTF3PicBOtZMMOmS_FIncFqZPeL9EbC
188
188
  metaflow/plugins/aws/batch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
189
189
  metaflow/plugins/aws/batch/batch.py,sha256=e9ssahWM18GnipPK2sqYB-ztx9w7Eoo7YtWyEtufYxs,17787
190
190
  metaflow/plugins/aws/batch/batch_cli.py,sha256=6PTbyajRgdy0XmjyJLBTdKdiOB84dcovQQ8sFXlJqko,11749
191
- metaflow/plugins/aws/batch/batch_client.py,sha256=s9ZHhxQPPoBQijLUgn6_16QOaD4-22U_44uJbp-yLkI,28565
191
+ metaflow/plugins/aws/batch/batch_client.py,sha256=ddlGG0Vk1mkO7tcvJjDvNAVsVLOlqddF7MA1kKfHSqM,28830
192
192
  metaflow/plugins/aws/batch/batch_decorator.py,sha256=kwgxEPCEoI6eZIpU5PuL442Ohg4_BfvwowoYgAnCzKE,17520
193
193
  metaflow/plugins/aws/secrets_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
194
194
  metaflow/plugins/aws/secrets_manager/aws_secrets_manager_secrets_provider.py,sha256=JtFUVu00Cg0FzAizgrPLXmrMqsT7YeQMkQlgeivUxcE,7986
@@ -279,35 +279,36 @@ metaflow/plugins/gcp/gs_tail.py,sha256=Jl_wvnzU7dub07A-DOAuP5FeccNIrPM-CeL1xKFs1
279
279
  metaflow/plugins/gcp/gs_utils.py,sha256=ZmIGFse1qYyvAVrwga23PQUzF6dXEDLLsZ2F-YRmvow,2030
280
280
  metaflow/plugins/gcp/includefile_support.py,sha256=vIDeR-MiJuUh-2S2pV7Z7FBkhIWwtHXaRrj76MWGRiY,3869
281
281
  metaflow/plugins/kubernetes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
282
- metaflow/plugins/kubernetes/kubernetes.py,sha256=cr3TheUasxIBEwFZ3GEVbctaf8gW57BM5BDk80ikjPI,31063
283
- metaflow/plugins/kubernetes/kubernetes_cli.py,sha256=qBDdr1Lvtt-RO9pB-9_HTOPdzAmDvvJ0aiQ1OoCcrMU,10892
284
- metaflow/plugins/kubernetes/kubernetes_client.py,sha256=GKg-gT3qhXMRQV-sG1YyoOf3Z32NXr_wwEN2ytMVSEg,2471
285
- metaflow/plugins/kubernetes/kubernetes_decorator.py,sha256=CXStYHomuJJK_Yocpdo6OJadEQv5hDfSpO7GPL61ltw,25322
282
+ metaflow/plugins/kubernetes/kube_utils.py,sha256=fYDlvqi8jYPsWijDwT6Z2qhQswyFqv7tiwtic_I80Vg,749
283
+ metaflow/plugins/kubernetes/kubernetes.py,sha256=bKBqgZXnIDkoa4xKtKoV6InPtYQy4CujfvcbQ3Pvsbc,31305
284
+ metaflow/plugins/kubernetes/kubernetes_cli.py,sha256=sFZ9Zrjef85vCO0MGpUF-em8Pw3dePFb3hbX3PtAH4I,13463
285
+ metaflow/plugins/kubernetes/kubernetes_client.py,sha256=LuKX9rejRySIajnfmDoNkQ6HrOMjwIMbHAOT7wgF9i8,6362
286
+ metaflow/plugins/kubernetes/kubernetes_decorator.py,sha256=MDW7AJozpFavrFEZAzFlxbF0vIxU0KWRpbvAtjcRIi4,25671
286
287
  metaflow/plugins/kubernetes/kubernetes_job.py,sha256=Cfkee8LbXC17jSXWoeNdomQRvF_8YSeXNg1gvxm6E_M,31806
287
- metaflow/plugins/kubernetes/kubernetes_jobsets.py,sha256=OBmLtX-ZUDQdCCfftUmRMernfmTNMwdTxPoCAp_NmwE,40957
288
+ metaflow/plugins/kubernetes/kubernetes_jobsets.py,sha256=sCVuy-PRN60s5HWpnRvFmnljS41k70sBoPO3nOtl2ZE,40800
288
289
  metaflow/plugins/metadata/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
289
290
  metaflow/plugins/metadata/local.py,sha256=YhLJC5zjVJrvQFIyQ92ZBByiUmhCC762RUX7ITX12O8,22428
290
291
  metaflow/plugins/metadata/service.py,sha256=ihq5F7KQZlxvYwzH_-jyP2aWN_I96i2vp92j_d697s8,20204
291
292
  metaflow/plugins/pypi/__init__.py,sha256=0YFZpXvX7HCkyBFglatual7XGifdA1RwC3U4kcizyak,1037
292
- metaflow/plugins/pypi/bootstrap.py,sha256=Tvc4_QKIx-A8j5Aq8ccWZrrxNM8csN40rK8HmxDx-Z8,5106
293
- metaflow/plugins/pypi/conda_decorator.py,sha256=fTJVbEfgOUtsDXIfnfsNk46sKeA9uTuTqGey9OFs9Ig,14738
293
+ metaflow/plugins/pypi/bootstrap.py,sha256=LSRjpDhtpvYqCC0lDPIWvZsrHW7PXx626NDzdYjFeVM,5224
294
+ metaflow/plugins/pypi/conda_decorator.py,sha256=sIAfvkjhUfTMK5yLrPOkqxeLUhlC5Buqmc9aITaf22E,15686
294
295
  metaflow/plugins/pypi/conda_environment.py,sha256=--q-8lypKupCdGsASpqABNpNqRxtQi6UCDgq8iHDFe4,19476
295
296
  metaflow/plugins/pypi/micromamba.py,sha256=67FiIZZz0Kig9EcN7bZLObsE6Z1MFyo4Dp93fd3Grcc,12178
296
297
  metaflow/plugins/pypi/pip.py,sha256=7B06mPOs5MvY33xbzPVYZlBr1iKMYaN-n8uulL9zSVg,13649
297
- metaflow/plugins/pypi/pypi_decorator.py,sha256=h5cAnxkWjmj4Ad4q0AkABKwhHQHYfeexy12yMaaLgXQ,6443
298
+ metaflow/plugins/pypi/pypi_decorator.py,sha256=rDMbHl7r81Ye7-TuIlKAVJ_CDnfjl9jV44ZPws-UsTY,7229
298
299
  metaflow/plugins/pypi/pypi_environment.py,sha256=FYMg8kF3lXqcLfRYWD83a9zpVjcoo_TARqMGZ763rRk,230
299
300
  metaflow/plugins/pypi/utils.py,sha256=ds1Mnv_DaxGnLAYp7ozg_K6oyguGyNhvHfE-75Ia1YA,2836
300
301
  metaflow/plugins/secrets/__init__.py,sha256=mhJaN2eMS_ZZVewAMR2E-JdP5i0t3v9e6Dcwd-WpruE,310
301
302
  metaflow/plugins/secrets/inline_secrets_provider.py,sha256=EChmoBGA1i7qM3jtYwPpLZDBybXLergiDlN63E0u3x8,294
302
303
  metaflow/plugins/secrets/secrets_decorator.py,sha256=s-sFzPWOjahhpr5fMj-ZEaHkDYAPTO0isYXGvaUwlG8,11273
303
304
  metaflow/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
304
- metaflow/runner/click_api.py,sha256=vrrIb5DtVYhCW_hB7wPgj6Q0o-h1bBSWiYnKTOyC454,13452
305
- metaflow/runner/deployer.py,sha256=nArjnErc0rOaZW612VRKDOT5594jwzeu86w5zW1LX6U,13558
306
- metaflow/runner/metaflow_runner.py,sha256=AO9nwr5qUbZWmsbFdjkUJrvFlaylz7WvxslvHsIqDYc,15371
305
+ metaflow/runner/click_api.py,sha256=Qfg4BOz5K2LaXTYBsi1y4zTfNIsGGHBVF3UkorX_-o8,13878
306
+ metaflow/runner/deployer.py,sha256=ddYv2UFE7lOFD3EPg4REzMdeLqg5WQhAXkdgMD5bUY8,13920
307
+ metaflow/runner/metaflow_runner.py,sha256=zRlnM2j5QLBh9OboPUuaQ4_WpwFaqrCedKBOpjjtxDw,15449
307
308
  metaflow/runner/nbdeploy.py,sha256=fP1s_5MeiDyT_igP82pB5EUqX9rOy2s06Hyc-OUbOvQ,4115
308
309
  metaflow/runner/nbrun.py,sha256=lmvhzMCz7iC9LSPGRijifW1wMXxa4RW_jVmpdjQi22E,7261
309
- metaflow/runner/subprocess_manager.py,sha256=0knxWZYJx8srMv6wTPYKOC6tn4-airnyI7Vbqfb3iXY,19567
310
- metaflow/runner/utils.py,sha256=FibdEj8CDnx1a-Je5KUQTwHuNbtkFm1unXGarj0D8ok,1394
310
+ metaflow/runner/subprocess_manager.py,sha256=FJh2C6wJlNnvwE00wyK6zxjf8R49PRRHeVcnPvlO4-0,19839
311
+ metaflow/runner/utils.py,sha256=jFlQf9fVvNRd4c6VKv0KwQ8kez_re7kC2VCRgHVMSsM,2051
311
312
  metaflow/sidecar/__init__.py,sha256=1mmNpmQ5puZCpRmmYlCOeieZ4108Su9XQ4_EqF1FGOU,131
312
313
  metaflow/sidecar/sidecar.py,sha256=EspKXvPPNiyRToaUZ51PS5TT_PzrBNAurn_wbFnmGr0,1334
313
314
  metaflow/sidecar/sidecar_messages.py,sha256=zPsCoYgDIcDkkvdC9MEpJTJ3y6TSGm2JWkRc4vxjbFA,1071
@@ -344,9 +345,9 @@ metaflow/tutorials/07-worldview/README.md,sha256=5vQTrFqulJ7rWN6r20dhot9lI2sVj9W
344
345
  metaflow/tutorials/07-worldview/worldview.ipynb,sha256=ztPZPI9BXxvW1QdS2Tfe7LBuVzvFvv0AToDnsDJhLdE,2237
345
346
  metaflow/tutorials/08-autopilot/README.md,sha256=GnePFp_q76jPs991lMUqfIIh5zSorIeWznyiUxzeUVE,1039
346
347
  metaflow/tutorials/08-autopilot/autopilot.ipynb,sha256=DQoJlILV7Mq9vfPBGW-QV_kNhWPjS5n6SJLqePjFYLY,3191
347
- metaflow-2.12.18.dist-info/LICENSE,sha256=nl_Lt5v9VvJ-5lWJDT4ddKAG-VZ-2IaLmbzpgYDz2hU,11343
348
- metaflow-2.12.18.dist-info/METADATA,sha256=YnoqO6tzhh5iHKOGK8jA5DzSl73EcXrjibG238O4I3M,5906
349
- metaflow-2.12.18.dist-info/WHEEL,sha256=WDDPHYzpiOIm6GP1C2_8y8W6q16ICddAgOHlhTje9Qc,109
350
- metaflow-2.12.18.dist-info/entry_points.txt,sha256=IKwTN1T3I5eJL3uo_vnkyxVffcgnRdFbKwlghZfn27k,57
351
- metaflow-2.12.18.dist-info/top_level.txt,sha256=v1pDHoWaSaKeuc5fKTRSfsXCKSdW1zvNVmvA-i0if3o,9
352
- metaflow-2.12.18.dist-info/RECORD,,
348
+ metaflow-2.12.20.dist-info/LICENSE,sha256=nl_Lt5v9VvJ-5lWJDT4ddKAG-VZ-2IaLmbzpgYDz2hU,11343
349
+ metaflow-2.12.20.dist-info/METADATA,sha256=oHu5racxyaljUQCl6cYXqYOHkI02VB5mR298txO9lAk,5906
350
+ metaflow-2.12.20.dist-info/WHEEL,sha256=AHX6tWk3qWuce7vKLrj7lnulVHEdWoltgauo8bgCXgU,109
351
+ metaflow-2.12.20.dist-info/entry_points.txt,sha256=IKwTN1T3I5eJL3uo_vnkyxVffcgnRdFbKwlghZfn27k,57
352
+ metaflow-2.12.20.dist-info/top_level.txt,sha256=v1pDHoWaSaKeuc5fKTRSfsXCKSdW1zvNVmvA-i0if3o,9
353
+ metaflow-2.12.20.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.0.0)
2
+ Generator: setuptools (75.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any