metaflow 2.12.19__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.
- metaflow/metaflow_current.py +3 -1
- metaflow/plugins/argo/argo_workflows.py +10 -1
- metaflow/plugins/aws/batch/batch_client.py +3 -0
- metaflow/plugins/kubernetes/kube_utils.py +25 -0
- metaflow/plugins/kubernetes/kubernetes.py +3 -0
- metaflow/plugins/kubernetes/kubernetes_cli.py +84 -1
- metaflow/plugins/kubernetes/kubernetes_client.py +97 -0
- metaflow/plugins/kubernetes/kubernetes_decorator.py +4 -0
- metaflow/plugins/parallel_decorator.py +4 -0
- metaflow/plugins/pypi/bootstrap.py +2 -0
- metaflow/plugins/pypi/conda_decorator.py +6 -0
- metaflow/runner/click_api.py +13 -1
- metaflow/runner/deployer.py +9 -2
- metaflow/runner/metaflow_runner.py +4 -2
- metaflow/runner/subprocess_manager.py +8 -3
- metaflow/runner/utils.py +19 -2
- metaflow/version.py +1 -1
- {metaflow-2.12.19.dist-info → metaflow-2.12.20.dist-info}/METADATA +2 -2
- {metaflow-2.12.19.dist-info → metaflow-2.12.20.dist-info}/RECORD +23 -22
- {metaflow-2.12.19.dist-info → metaflow-2.12.20.dist-info}/WHEEL +1 -1
- {metaflow-2.12.19.dist-info → metaflow-2.12.20.dist-info}/LICENSE +0 -0
- {metaflow-2.12.19.dist-info → metaflow-2.12.20.dist-info}/entry_points.txt +0 -0
- {metaflow-2.12.19.dist-info → metaflow-2.12.20.dist-info}/top_level.txt +0 -0
metaflow/metaflow_current.py
CHANGED
@@ -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(
|
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
|
@@ -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,
|
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
|
@@ -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
|
)
|
@@ -353,6 +353,12 @@ class CondaFlowDecorator(FlowDecorator):
|
|
353
353
|
def flow_init(
|
354
354
|
self, flow, graph, environment, flow_datastore, metadata, logger, echo, options
|
355
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
|
+
|
356
362
|
# @conda uses a conda environment to create a virtual environment.
|
357
363
|
# The conda environment can be created through micromamba.
|
358
364
|
_supported_virtual_envs = ["conda"]
|
metaflow/runner/click_api.py
CHANGED
@@ -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:
|
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]
|
metaflow/runner/deployer.py
CHANGED
@@ -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 =
|
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
|
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
|
-
|
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(
|
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.
|
1
|
+
metaflow_version = "2.12.20"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: metaflow
|
3
|
-
Version: 2.12.
|
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.
|
29
|
+
Requires-Dist: metaflow-stubs==2.12.20; extra == "stubs"
|
30
30
|
|
31
31
|

|
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=
|
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=
|
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=
|
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=
|
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=
|
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,18 +279,19 @@ 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/
|
283
|
-
metaflow/plugins/kubernetes/
|
284
|
-
metaflow/plugins/kubernetes/
|
285
|
-
metaflow/plugins/kubernetes/
|
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
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=
|
293
|
-
metaflow/plugins/pypi/conda_decorator.py,sha256=
|
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
|
@@ -301,13 +302,13 @@ metaflow/plugins/secrets/__init__.py,sha256=mhJaN2eMS_ZZVewAMR2E-JdP5i0t3v9e6Dcw
|
|
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=
|
305
|
-
metaflow/runner/deployer.py,sha256=
|
306
|
-
metaflow/runner/metaflow_runner.py,sha256=
|
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=
|
310
|
-
metaflow/runner/utils.py,sha256=
|
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.
|
348
|
-
metaflow-2.12.
|
349
|
-
metaflow-2.12.
|
350
|
-
metaflow-2.12.
|
351
|
-
metaflow-2.12.
|
352
|
-
metaflow-2.12.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|