ob-metaflow 2.9.10.1__py2.py3-none-any.whl → 2.10.2.6__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.
Potentially problematic release.
This version of ob-metaflow might be problematic. Click here for more details.
- metaflow/_vendor/packaging/__init__.py +15 -0
- metaflow/_vendor/packaging/_elffile.py +108 -0
- metaflow/_vendor/packaging/_manylinux.py +238 -0
- metaflow/_vendor/packaging/_musllinux.py +80 -0
- metaflow/_vendor/packaging/_parser.py +328 -0
- metaflow/_vendor/packaging/_structures.py +61 -0
- metaflow/_vendor/packaging/_tokenizer.py +188 -0
- metaflow/_vendor/packaging/markers.py +245 -0
- metaflow/_vendor/packaging/requirements.py +95 -0
- metaflow/_vendor/packaging/specifiers.py +1005 -0
- metaflow/_vendor/packaging/tags.py +546 -0
- metaflow/_vendor/packaging/utils.py +141 -0
- metaflow/_vendor/packaging/version.py +563 -0
- metaflow/_vendor/v3_7/__init__.py +1 -0
- metaflow/_vendor/v3_7/zipp.py +329 -0
- metaflow/metaflow_config.py +2 -1
- metaflow/metaflow_environment.py +3 -1
- metaflow/mflog/mflog.py +7 -1
- metaflow/multicore_utils.py +12 -2
- metaflow/plugins/__init__.py +8 -3
- metaflow/plugins/airflow/airflow.py +13 -0
- metaflow/plugins/argo/argo_client.py +16 -0
- metaflow/plugins/argo/argo_events.py +7 -1
- metaflow/plugins/argo/argo_workflows.py +62 -0
- metaflow/plugins/argo/argo_workflows_cli.py +15 -0
- metaflow/plugins/aws/batch/batch.py +10 -0
- metaflow/plugins/aws/batch/batch_cli.py +1 -2
- metaflow/plugins/aws/batch/batch_decorator.py +2 -9
- metaflow/plugins/datatools/s3/s3.py +4 -0
- metaflow/plugins/env_escape/client.py +24 -3
- metaflow/plugins/env_escape/stub.py +2 -8
- metaflow/plugins/kubernetes/kubernetes.py +13 -0
- metaflow/plugins/kubernetes/kubernetes_cli.py +1 -2
- metaflow/plugins/kubernetes/kubernetes_decorator.py +9 -2
- metaflow/plugins/pypi/__init__.py +29 -0
- metaflow/plugins/pypi/bootstrap.py +131 -0
- metaflow/plugins/pypi/conda_decorator.py +335 -0
- metaflow/plugins/pypi/conda_environment.py +414 -0
- metaflow/plugins/pypi/micromamba.py +294 -0
- metaflow/plugins/pypi/pip.py +205 -0
- metaflow/plugins/pypi/pypi_decorator.py +130 -0
- metaflow/plugins/pypi/pypi_environment.py +7 -0
- metaflow/plugins/pypi/utils.py +75 -0
- metaflow/task.py +0 -3
- metaflow/vendor.py +1 -0
- {ob_metaflow-2.9.10.1.dist-info → ob_metaflow-2.10.2.6.dist-info}/METADATA +1 -1
- {ob_metaflow-2.9.10.1.dist-info → ob_metaflow-2.10.2.6.dist-info}/RECORD +51 -33
- {ob_metaflow-2.9.10.1.dist-info → ob_metaflow-2.10.2.6.dist-info}/WHEEL +1 -1
- metaflow/plugins/conda/__init__.py +0 -90
- metaflow/plugins/conda/batch_bootstrap.py +0 -104
- metaflow/plugins/conda/conda.py +0 -247
- metaflow/plugins/conda/conda_environment.py +0 -136
- metaflow/plugins/conda/conda_flow_decorator.py +0 -35
- metaflow/plugins/conda/conda_step_decorator.py +0 -416
- {ob_metaflow-2.9.10.1.dist-info → ob_metaflow-2.10.2.6.dist-info}/LICENSE +0 -0
- {ob_metaflow-2.9.10.1.dist-info → ob_metaflow-2.10.2.6.dist-info}/entry_points.txt +0 -0
- {ob_metaflow-2.9.10.1.dist-info → ob_metaflow-2.10.2.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
from metaflow.exception import MetaflowException
|
|
7
|
+
from metaflow.util import which
|
|
8
|
+
|
|
9
|
+
from .utils import conda_platform
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MicromambaException(MetaflowException):
|
|
13
|
+
headline = "Micromamba ran into an error while setting up environment"
|
|
14
|
+
|
|
15
|
+
def __init__(self, error):
|
|
16
|
+
if isinstance(error, (list,)):
|
|
17
|
+
error = "\n".join(error)
|
|
18
|
+
msg = "{error}".format(error=error)
|
|
19
|
+
super(MicromambaException, self).__init__(msg)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Micromamba(object):
|
|
23
|
+
def __init__(self):
|
|
24
|
+
# micromamba is a tiny version of the mamba package manager and comes with
|
|
25
|
+
# metaflow specific performance enhancements.
|
|
26
|
+
|
|
27
|
+
_path_to_hidden_micromamba = os.path.join(
|
|
28
|
+
os.path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
|
|
29
|
+
"micromamba",
|
|
30
|
+
)
|
|
31
|
+
self.bin = (
|
|
32
|
+
which(os.environ.get("METAFLOW_PATH_TO_MICROMAMBA") or "micromamba")
|
|
33
|
+
or which("./micromamba") # to support remote execution
|
|
34
|
+
or which("./bin/micromamba")
|
|
35
|
+
or which(os.path.join(_path_to_hidden_micromamba, "bin/micromamba"))
|
|
36
|
+
)
|
|
37
|
+
if self.bin is None:
|
|
38
|
+
# Install Micromamba on the fly.
|
|
39
|
+
# TODO: Make this optional at some point.
|
|
40
|
+
_install_micromamba(_path_to_hidden_micromamba)
|
|
41
|
+
self.bin = which(os.path.join(_path_to_hidden_micromamba, "bin/micromamba"))
|
|
42
|
+
|
|
43
|
+
if self.bin is None:
|
|
44
|
+
msg = "No installation for *Micromamba* found.\n"
|
|
45
|
+
msg += "Visit https://mamba.readthedocs.io/en/latest/micromamba-installation.html for installation instructions."
|
|
46
|
+
raise MetaflowException(msg)
|
|
47
|
+
|
|
48
|
+
def solve(self, id_, packages, python, platform):
|
|
49
|
+
# Performance enhancements
|
|
50
|
+
# 1. Using zstd compressed repodata index files drops the index download time
|
|
51
|
+
# by a factor of 10x - conda-forge/noarch/repodata.json has a
|
|
52
|
+
# mean download time of 3.705s ± 2.283s vs 385.1ms ± 57.9ms for
|
|
53
|
+
# conda-forge/noarch/repodata.json.zst. Thankfully, now micromamba pulls
|
|
54
|
+
# zstd compressed files by default - https://github.com/conda-forge/conda-forge.github.io/issues/1835
|
|
55
|
+
# 2. Tweak repodata ttl to pull either only once a day or if a solve fails -
|
|
56
|
+
# this ensures that repodata is not pulled everytime it becomes dirty by
|
|
57
|
+
# default. This can result in an environment that has stale transitive
|
|
58
|
+
# dependencies but still correct. (--repodata-ttl 86400 --retry-clean-cache)
|
|
59
|
+
# 3. Introduce pip as python dependency to resolve pip packages within conda
|
|
60
|
+
# environment
|
|
61
|
+
# 4. Multiple solves can progress at the same time while relying on the same
|
|
62
|
+
# index
|
|
63
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
64
|
+
env = {
|
|
65
|
+
"MAMBA_ADD_PIP_AS_PYTHON_DEPENDENCY": "true",
|
|
66
|
+
"CONDA_SUBDIR": platform,
|
|
67
|
+
# "CONDA_UNSATISFIABLE_HINTS_CHECK_DEPTH": "0" # https://github.com/conda/conda/issues/9862
|
|
68
|
+
}
|
|
69
|
+
cmd = [
|
|
70
|
+
"create",
|
|
71
|
+
"--yes",
|
|
72
|
+
"--quiet",
|
|
73
|
+
"--dry-run",
|
|
74
|
+
"--no-extra-safety-checks",
|
|
75
|
+
"--repodata-ttl=86400",
|
|
76
|
+
"--retry-clean-cache",
|
|
77
|
+
"--prefix=%s/prefix" % tmp_dir,
|
|
78
|
+
]
|
|
79
|
+
# Introduce conda-forge as a default channel
|
|
80
|
+
for channel in self.info()["channels"] or ["conda-forge"]:
|
|
81
|
+
cmd.append("--channel=%s" % channel)
|
|
82
|
+
|
|
83
|
+
for package, version in packages.items():
|
|
84
|
+
cmd.append("%s==%s" % (package, version))
|
|
85
|
+
if python:
|
|
86
|
+
cmd.append("python==%s" % python)
|
|
87
|
+
# TODO: Ensure a human readable message is returned when the environment
|
|
88
|
+
# can't be resolved for any and all reasons.
|
|
89
|
+
return [
|
|
90
|
+
{k: v for k, v in item.items() if k in ["url"]}
|
|
91
|
+
for item in self._call(cmd, env)["actions"]["LINK"]
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
def download(self, id_, packages, python, platform):
|
|
95
|
+
# Unfortunately all the packages need to be catalogued in package cache
|
|
96
|
+
# because of which this function can't be parallelized
|
|
97
|
+
|
|
98
|
+
# Micromamba is painfully slow in determining if many packages are infact
|
|
99
|
+
# already cached. As a perf heuristic, we check if the environment already
|
|
100
|
+
# exists to short circuit package downloads.
|
|
101
|
+
if self.path_to_environment(id_, platform):
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
prefix = "{env_dirs}/{keyword}/{platform}/{id}".format(
|
|
105
|
+
env_dirs=self.info()["envs_dirs"][0],
|
|
106
|
+
platform=platform,
|
|
107
|
+
keyword="metaflow", # indicates metaflow generated environment
|
|
108
|
+
id=id_,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Another forced perf heuristic to skip cross-platform downloads.
|
|
112
|
+
if os.path.exists(f"{prefix}/fake.done"):
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
116
|
+
env = {
|
|
117
|
+
"CONDA_SUBDIR": platform,
|
|
118
|
+
}
|
|
119
|
+
cmd = [
|
|
120
|
+
"create",
|
|
121
|
+
"--yes",
|
|
122
|
+
"--no-deps",
|
|
123
|
+
"--download-only",
|
|
124
|
+
"--safety-checks=disabled",
|
|
125
|
+
"--no-extra-safety-checks",
|
|
126
|
+
"--repodata-ttl=86400",
|
|
127
|
+
"--prefix=%s/prefix" % tmp_dir,
|
|
128
|
+
"--quiet",
|
|
129
|
+
]
|
|
130
|
+
for package in packages:
|
|
131
|
+
cmd.append("{url}".format(**package))
|
|
132
|
+
|
|
133
|
+
self._call(cmd, env)
|
|
134
|
+
# Perf optimization to skip cross-platform downloads.
|
|
135
|
+
if platform != self.platform():
|
|
136
|
+
os.makedirs(prefix, exist_ok=True) or open(
|
|
137
|
+
f"{prefix}/fake.done", "w"
|
|
138
|
+
).close()
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
def create(self, id_, packages, python, platform):
|
|
142
|
+
# create environment only if the platform matches system platform
|
|
143
|
+
if platform != self.platform() or self.path_to_environment(id_, platform):
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
prefix = "{env_dirs}/{keyword}/{platform}/{id}".format(
|
|
147
|
+
env_dirs=self.info()["envs_dirs"][0],
|
|
148
|
+
platform=platform,
|
|
149
|
+
keyword="metaflow", # indicates metaflow generated environment
|
|
150
|
+
id=id_,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
env = {
|
|
154
|
+
# use hardlinks when possible, otherwise copy files
|
|
155
|
+
# disabled for now since it adds to environment creation latencies
|
|
156
|
+
"CONDA_ALLOW_SOFTLINKS": "0",
|
|
157
|
+
}
|
|
158
|
+
cmd = [
|
|
159
|
+
"create",
|
|
160
|
+
"--yes",
|
|
161
|
+
"--no-extra-safety-checks",
|
|
162
|
+
# "--offline", # not needed since micromamba will first look at the cache
|
|
163
|
+
"--prefix", # trick to ensure environments can be created in parallel
|
|
164
|
+
prefix,
|
|
165
|
+
"--quiet",
|
|
166
|
+
"--no-deps", # important!
|
|
167
|
+
]
|
|
168
|
+
for package in packages:
|
|
169
|
+
cmd.append("{url}".format(**package))
|
|
170
|
+
self._call(cmd, env)
|
|
171
|
+
|
|
172
|
+
def info(self):
|
|
173
|
+
return self._call(["config", "list", "-a"])
|
|
174
|
+
|
|
175
|
+
def path_to_environment(self, id_, platform=None):
|
|
176
|
+
if platform is None:
|
|
177
|
+
platform = self.platform()
|
|
178
|
+
suffix = "{keyword}/{platform}/{id}".format(
|
|
179
|
+
platform=platform,
|
|
180
|
+
keyword="metaflow", # indicates metaflow generated environment
|
|
181
|
+
id=id_,
|
|
182
|
+
)
|
|
183
|
+
for env in self._call(["env", "list"])["envs"]:
|
|
184
|
+
# TODO: Check bin/python is available as a heuristic for well formed env
|
|
185
|
+
if env.endswith(suffix):
|
|
186
|
+
return env
|
|
187
|
+
|
|
188
|
+
def metadata(self, id_, packages, python, platform):
|
|
189
|
+
# this method unfortunately relies on the implementation detail for
|
|
190
|
+
# conda environments and has the potential to break all of a sudden.
|
|
191
|
+
packages_to_filenames = {
|
|
192
|
+
package["url"]: package["url"].split("/")[-1] for package in packages
|
|
193
|
+
}
|
|
194
|
+
directories = self.info()["pkgs_dirs"]
|
|
195
|
+
# search all package caches for packages
|
|
196
|
+
metadata = {
|
|
197
|
+
url: os.path.join(d, file)
|
|
198
|
+
for url, file in packages_to_filenames.items()
|
|
199
|
+
for d in directories
|
|
200
|
+
if os.path.isdir(d)
|
|
201
|
+
and file in os.listdir(d)
|
|
202
|
+
and os.path.isfile(os.path.join(d, file))
|
|
203
|
+
}
|
|
204
|
+
# set package tarball local paths to None if package tarballs are missing
|
|
205
|
+
for url in packages_to_filenames:
|
|
206
|
+
metadata.setdefault(url, None)
|
|
207
|
+
return metadata
|
|
208
|
+
|
|
209
|
+
def interpreter(self, id_):
|
|
210
|
+
return os.path.join(self.path_to_environment(id_), "bin/python")
|
|
211
|
+
|
|
212
|
+
def platform(self):
|
|
213
|
+
return self.info()["platform"]
|
|
214
|
+
|
|
215
|
+
def _call(self, args, env=None):
|
|
216
|
+
if env is None:
|
|
217
|
+
env = {}
|
|
218
|
+
try:
|
|
219
|
+
result = (
|
|
220
|
+
subprocess.check_output(
|
|
221
|
+
[self.bin] + args,
|
|
222
|
+
stderr=subprocess.PIPE,
|
|
223
|
+
env={
|
|
224
|
+
**os.environ,
|
|
225
|
+
# prioritize metaflow-specific env vars
|
|
226
|
+
**{k: v for k, v in env.items() if v is not None},
|
|
227
|
+
**{
|
|
228
|
+
"MAMBA_NO_BANNER": "1",
|
|
229
|
+
"MAMBA_JSON": "true",
|
|
230
|
+
# play with fire! needed for resolving cross-platform
|
|
231
|
+
# environments
|
|
232
|
+
"CONDA_SAFETY_CHECKS": "disabled",
|
|
233
|
+
# "CONDA_UNSATISFIABLE_HINTS_CHECK_DEPTH": "0",
|
|
234
|
+
# Support packages on S3
|
|
235
|
+
# "CONDA_ALLOW_NON_CHANNEL_URLS": "1",
|
|
236
|
+
"MAMBA_USE_LOCKFILES": "false",
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
.decode()
|
|
241
|
+
.strip()
|
|
242
|
+
)
|
|
243
|
+
if result:
|
|
244
|
+
return json.loads(result)
|
|
245
|
+
return {}
|
|
246
|
+
except subprocess.CalledProcessError as e:
|
|
247
|
+
msg = "command '{cmd}' returned error ({code})\n{stderr}"
|
|
248
|
+
try:
|
|
249
|
+
output = json.loads(e.output)
|
|
250
|
+
err = []
|
|
251
|
+
for error in output.get("solver_problems", []):
|
|
252
|
+
err.append(error)
|
|
253
|
+
raise MicromambaException(
|
|
254
|
+
msg.format(
|
|
255
|
+
cmd=" ".join(e.cmd),
|
|
256
|
+
code=e.returncode,
|
|
257
|
+
output=e.output.decode(),
|
|
258
|
+
stderr="\n".join(err),
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
except (TypeError, ValueError) as ve:
|
|
262
|
+
pass
|
|
263
|
+
raise MicromambaException(
|
|
264
|
+
msg.format(
|
|
265
|
+
cmd=" ".join(e.cmd),
|
|
266
|
+
code=e.returncode,
|
|
267
|
+
output=e.output.decode(),
|
|
268
|
+
stderr=e.stderr.decode(),
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _install_micromamba(installation_location):
|
|
274
|
+
# Unfortunately no 32bit binaries are available for micromamba, which ideally
|
|
275
|
+
# shouldn't be much of a problem in today's world.
|
|
276
|
+
platform = conda_platform()
|
|
277
|
+
try:
|
|
278
|
+
subprocess.Popen(f"mkdir -p {installation_location}", shell=True).wait()
|
|
279
|
+
# https://mamba.readthedocs.io/en/latest/micromamba-installation.html#manual-installation
|
|
280
|
+
result = subprocess.Popen(
|
|
281
|
+
f"curl -Ls https://micro.mamba.pm/api/micromamba/{platform}/latest | tar -xvj -C {installation_location} bin/micromamba",
|
|
282
|
+
shell=True,
|
|
283
|
+
stderr=subprocess.PIPE,
|
|
284
|
+
)
|
|
285
|
+
_, err = result.communicate()
|
|
286
|
+
if result.returncode != 0:
|
|
287
|
+
raise MicromambaException(
|
|
288
|
+
f"Micromamba installation '{result.args}' failed:\n{err.decode()}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
except subprocess.CalledProcessError as e:
|
|
292
|
+
raise MicromambaException(
|
|
293
|
+
"Micromamba installation failed:\n{}".format(e.stderr.decode())
|
|
294
|
+
)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import tempfile
|
|
5
|
+
from itertools import chain, product
|
|
6
|
+
|
|
7
|
+
from metaflow.exception import MetaflowException
|
|
8
|
+
from metaflow.util import which
|
|
9
|
+
|
|
10
|
+
from .micromamba import Micromamba
|
|
11
|
+
from .utils import pip_tags
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PipException(MetaflowException):
|
|
15
|
+
headline = "Pip ran into an error while setting up environment"
|
|
16
|
+
|
|
17
|
+
def __init__(self, error):
|
|
18
|
+
if isinstance(error, (list,)):
|
|
19
|
+
error = "\n".join(error)
|
|
20
|
+
msg = "{error}".format(error=error)
|
|
21
|
+
super(PipException, self).__init__(msg)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
METADATA_FILE = "{prefix}/.pip/metadata"
|
|
25
|
+
INSTALLATION_MARKER = "{prefix}/.pip/id"
|
|
26
|
+
|
|
27
|
+
# TODO:
|
|
28
|
+
# 1. Support git repositories, local dirs, non-wheel like packages
|
|
29
|
+
# 2. Support protected indices
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Pip(object):
|
|
33
|
+
def __init__(self, micromamba=None):
|
|
34
|
+
# pip is assumed to be installed inside a conda environment managed by
|
|
35
|
+
# micromamba. pip commands are executed using `micromamba run --prefix`
|
|
36
|
+
self.micromamba = micromamba or Micromamba()
|
|
37
|
+
|
|
38
|
+
def solve(self, id_, packages, python, platform):
|
|
39
|
+
prefix = self.micromamba.path_to_environment(id_)
|
|
40
|
+
if prefix is None:
|
|
41
|
+
msg = "Unable to locate a Micromamba managed virtual environment\n"
|
|
42
|
+
msg += "for id {id}".format(id=id_)
|
|
43
|
+
raise PipException(msg)
|
|
44
|
+
|
|
45
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
46
|
+
report = "{tmp_dir}/report.json".format(tmp_dir=tmp_dir)
|
|
47
|
+
implementations, platforms, abis = zip(
|
|
48
|
+
*[
|
|
49
|
+
(tag.interpreter, tag.platform, tag.abi)
|
|
50
|
+
for tag in pip_tags(python, platform)
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
extra_index_urls = self.extra_index_urls(prefix)
|
|
54
|
+
cmd = [
|
|
55
|
+
"install",
|
|
56
|
+
"--dry-run",
|
|
57
|
+
"--only-binary=:all:", # only wheels
|
|
58
|
+
"--upgrade-strategy=only-if-needed",
|
|
59
|
+
"--target=%s" % tmp_dir,
|
|
60
|
+
"--report=%s" % report,
|
|
61
|
+
"--progress-bar=off",
|
|
62
|
+
"--quiet",
|
|
63
|
+
*(
|
|
64
|
+
chain.from_iterable(
|
|
65
|
+
product(["--extra-index-url"], set(extra_index_urls))
|
|
66
|
+
)
|
|
67
|
+
),
|
|
68
|
+
*(chain.from_iterable(product(["--abi"], set(abis)))),
|
|
69
|
+
*(chain.from_iterable(product(["--platform"], set(platforms)))),
|
|
70
|
+
# *(chain.from_iterable(product(["--implementations"], set(implementations)))),
|
|
71
|
+
]
|
|
72
|
+
for package, version in packages.items():
|
|
73
|
+
if version.startswith(("<", ">", "!", "~")):
|
|
74
|
+
cmd.append(f"{package}{version}")
|
|
75
|
+
else:
|
|
76
|
+
cmd.append(f"{package}=={version}")
|
|
77
|
+
self._call(prefix, cmd)
|
|
78
|
+
with open(report, mode="r", encoding="utf-8") as f:
|
|
79
|
+
return [
|
|
80
|
+
{k: v for k, v in item["download_info"].items() if k in ["url"]}
|
|
81
|
+
for item in json.load(f)["install"]
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
def download(self, id_, packages, python, platform):
|
|
85
|
+
prefix = self.micromamba.path_to_environment(id_)
|
|
86
|
+
metadata_file = METADATA_FILE.format(prefix=prefix)
|
|
87
|
+
# download packages only if they haven't ever been downloaded before
|
|
88
|
+
if os.path.isfile(metadata_file):
|
|
89
|
+
return
|
|
90
|
+
metadata = {}
|
|
91
|
+
implementations, platforms, abis = zip(
|
|
92
|
+
*[
|
|
93
|
+
(tag.interpreter, tag.platform, tag.abi)
|
|
94
|
+
for tag in pip_tags(python, platform)
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
extra_index_urls = self.extra_index_urls(prefix)
|
|
98
|
+
cmd = [
|
|
99
|
+
"download",
|
|
100
|
+
"--no-deps",
|
|
101
|
+
"--no-index",
|
|
102
|
+
"--progress-bar=off",
|
|
103
|
+
# if packages are present in Pip cache, this will be a local copy
|
|
104
|
+
"--dest=%s/.pip/wheels" % prefix,
|
|
105
|
+
"--quiet",
|
|
106
|
+
*(
|
|
107
|
+
chain.from_iterable(
|
|
108
|
+
product(["--extra-index-url"], set(extra_index_urls))
|
|
109
|
+
)
|
|
110
|
+
),
|
|
111
|
+
*(chain.from_iterable(product(["--abi"], set(abis)))),
|
|
112
|
+
*(chain.from_iterable(product(["--platform"], set(platforms)))),
|
|
113
|
+
# *(chain.from_iterable(product(["--implementations"], set(implementations)))),
|
|
114
|
+
]
|
|
115
|
+
for package in packages:
|
|
116
|
+
cmd.append("{url}".format(**package))
|
|
117
|
+
metadata["{url}".format(**package)] = "{prefix}/.pip/wheels/{wheel}".format(
|
|
118
|
+
prefix=prefix, wheel=package["url"].split("/")[-1]
|
|
119
|
+
)
|
|
120
|
+
self._call(prefix, cmd)
|
|
121
|
+
# write the url to wheel mappings in a magic location
|
|
122
|
+
with open(metadata_file, "w") as file:
|
|
123
|
+
file.write(json.dumps(metadata))
|
|
124
|
+
|
|
125
|
+
def create(self, id_, packages, python, platform):
|
|
126
|
+
prefix = self.micromamba.path_to_environment(id_)
|
|
127
|
+
installation_marker = INSTALLATION_MARKER.format(prefix=prefix)
|
|
128
|
+
# install packages only if they haven't been installed before
|
|
129
|
+
if os.path.isfile(installation_marker):
|
|
130
|
+
return
|
|
131
|
+
# Pip can't install packages if the underlying virtual environment doesn't
|
|
132
|
+
# share the same platform
|
|
133
|
+
if self.micromamba.platform() == platform:
|
|
134
|
+
cmd = [
|
|
135
|
+
"install",
|
|
136
|
+
"--no-compile",
|
|
137
|
+
"--no-deps",
|
|
138
|
+
"--no-index",
|
|
139
|
+
"--progress-bar=off",
|
|
140
|
+
"--quiet",
|
|
141
|
+
]
|
|
142
|
+
for package in packages:
|
|
143
|
+
cmd.append("{url}".format(**package))
|
|
144
|
+
self._call(prefix, cmd)
|
|
145
|
+
with open(installation_marker, "w") as file:
|
|
146
|
+
file.write(json.dumps({"id": id_}))
|
|
147
|
+
|
|
148
|
+
def metadata(self, id_, packages, python, platform):
|
|
149
|
+
# read the url to wheel mappings from a magic location
|
|
150
|
+
prefix = self.micromamba.path_to_environment(id_)
|
|
151
|
+
metadata_file = METADATA_FILE.format(prefix=prefix)
|
|
152
|
+
with open(metadata_file, "r") as file:
|
|
153
|
+
return json.loads(file.read())
|
|
154
|
+
|
|
155
|
+
def extra_index_urls(self, prefix):
|
|
156
|
+
# get extra index urls from Pip conf
|
|
157
|
+
extra_indices = []
|
|
158
|
+
for key in [":env:.extra-index-url", "global.extra-index-url"]:
|
|
159
|
+
try:
|
|
160
|
+
extras = self._call(prefix, args=["config", "get", key], isolated=False)
|
|
161
|
+
extra_indices.extend(extras.split(" "))
|
|
162
|
+
except Exception:
|
|
163
|
+
# Pip will throw an error when trying to get a config key that does
|
|
164
|
+
# not exist
|
|
165
|
+
pass
|
|
166
|
+
return extra_indices
|
|
167
|
+
|
|
168
|
+
def _call(self, prefix, args, env=None, isolated=True):
|
|
169
|
+
if env is None:
|
|
170
|
+
env = {}
|
|
171
|
+
try:
|
|
172
|
+
return (
|
|
173
|
+
subprocess.check_output(
|
|
174
|
+
[
|
|
175
|
+
self.micromamba.bin,
|
|
176
|
+
"run",
|
|
177
|
+
"--prefix",
|
|
178
|
+
prefix,
|
|
179
|
+
"pip3",
|
|
180
|
+
"--disable-pip-version-check",
|
|
181
|
+
"--no-input",
|
|
182
|
+
"--no-color",
|
|
183
|
+
]
|
|
184
|
+
+ (["--isolated"] if isolated else [])
|
|
185
|
+
+ args,
|
|
186
|
+
stderr=subprocess.PIPE,
|
|
187
|
+
env={
|
|
188
|
+
**os.environ,
|
|
189
|
+
# prioritize metaflow-specific env vars
|
|
190
|
+
**{"PYTHONNOUSERSITE": "1"}, # no user installation!
|
|
191
|
+
**env,
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
.decode()
|
|
195
|
+
.strip()
|
|
196
|
+
)
|
|
197
|
+
except subprocess.CalledProcessError as e:
|
|
198
|
+
raise PipException(
|
|
199
|
+
"command '{cmd}' returned error ({code}) {output}\n{stderr}".format(
|
|
200
|
+
cmd=" ".join(e.cmd),
|
|
201
|
+
code=e.returncode,
|
|
202
|
+
output=e.output.decode(),
|
|
203
|
+
stderr=e.stderr.decode(),
|
|
204
|
+
)
|
|
205
|
+
)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from metaflow.decorators import FlowDecorator, StepDecorator
|
|
2
|
+
from metaflow.metaflow_environment import InvalidEnvironmentException
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PyPIStepDecorator(StepDecorator):
|
|
6
|
+
"""
|
|
7
|
+
Specifies the PyPI packages for the step.
|
|
8
|
+
|
|
9
|
+
Information in this decorator will augment any
|
|
10
|
+
attributes set in the `@pyi_base` flow-level decorator. Hence,
|
|
11
|
+
you can use `@pypi_base` to set packages required by all
|
|
12
|
+
steps and use `@pypi` to specify step-specific overrides.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
packages : Dict[str, str], default: {}
|
|
17
|
+
Packages to use for this step. The key is the name of the package
|
|
18
|
+
and the value is the version to use.
|
|
19
|
+
python : str, optional
|
|
20
|
+
Version of Python to use, e.g. '3.7.4'. A default value of None implies
|
|
21
|
+
that the version used will correspond to the version of the Python interpreter used to start the run.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
name = "pypi"
|
|
25
|
+
defaults = {"packages": {}, "python": None, "disabled": None} # wheels
|
|
26
|
+
|
|
27
|
+
def step_init(self, flow, graph, step, decos, environment, flow_datastore, logger):
|
|
28
|
+
# The init_environment hook for Environment creates the relevant virtual
|
|
29
|
+
# environments. The step_init hook sets up the relevant state for that hook to
|
|
30
|
+
# do it's magic.
|
|
31
|
+
|
|
32
|
+
self.flow = flow
|
|
33
|
+
self.step = step
|
|
34
|
+
|
|
35
|
+
# Support flow-level decorator
|
|
36
|
+
if "pypi_base" in self.flow._flow_decorators:
|
|
37
|
+
super_attributes = self.flow._flow_decorators["pypi_base"][0].attributes
|
|
38
|
+
self.attributes["packages"] = {
|
|
39
|
+
**super_attributes["packages"],
|
|
40
|
+
**self.attributes["packages"],
|
|
41
|
+
}
|
|
42
|
+
self.attributes["python"] = (
|
|
43
|
+
self.attributes["python"] or super_attributes["python"]
|
|
44
|
+
)
|
|
45
|
+
self.attributes["disabled"] = (
|
|
46
|
+
self.attributes["disabled"]
|
|
47
|
+
if self.attributes["disabled"] is not None
|
|
48
|
+
else super_attributes["disabled"]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# At the moment, @pypi uses a conda environment as a virtual environment. This
|
|
52
|
+
# is to ensure that we can have a dedicated Python interpreter within the
|
|
53
|
+
# virtual environment. The conda environment is currently created through
|
|
54
|
+
# micromamba. As a follow up, we can look into creating a virtualenv using
|
|
55
|
+
# venv.
|
|
56
|
+
|
|
57
|
+
# Currently, @pypi relies on pip for package resolution. We can introduce
|
|
58
|
+
# support for Poetry in the near future, if desired. Poetry is great for
|
|
59
|
+
# interactive use cases, but not so much for programmatic use cases like the
|
|
60
|
+
# one here. We can consider introducing a UX where @pypi is able to consume
|
|
61
|
+
# poetry.lock files in the future.
|
|
62
|
+
|
|
63
|
+
_supported_virtual_envs = ["conda"] # , "venv"]
|
|
64
|
+
|
|
65
|
+
# To placate people who don't want to see a shred of conda in UX, we symlink
|
|
66
|
+
# --environment=pypi to --environment=conda
|
|
67
|
+
_supported_virtual_envs.extend(["pypi"])
|
|
68
|
+
|
|
69
|
+
# The --environment= requirement ensures that valid virtual environments are
|
|
70
|
+
# created for every step to execute it, greatly simplifying the @pypi
|
|
71
|
+
# implementation.
|
|
72
|
+
if environment.TYPE not in _supported_virtual_envs:
|
|
73
|
+
raise InvalidEnvironmentException(
|
|
74
|
+
"@%s decorator requires %s"
|
|
75
|
+
% (
|
|
76
|
+
self.name,
|
|
77
|
+
" or ".join(
|
|
78
|
+
["--environment=%s" % env for env in _supported_virtual_envs]
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class PyPIFlowDecorator(FlowDecorator):
|
|
85
|
+
"""
|
|
86
|
+
Specifies the PyPI packages for all steps of the flow.
|
|
87
|
+
|
|
88
|
+
Use `@pypi_base` to set common packages required by all
|
|
89
|
+
steps and use `@pypi` to specify step-specific overrides.
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
packages : Dict[str, str], default: {}
|
|
93
|
+
Packages to use for this flow. The key is the name of the package
|
|
94
|
+
and the value is the version to use.
|
|
95
|
+
python : str, optional
|
|
96
|
+
Version of Python to use, e.g. '3.7.4'. A default value of None implies
|
|
97
|
+
that the version used will correspond to the version of the Python interpreter used to start the run.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
name = "pypi_base"
|
|
101
|
+
defaults = {"packages": {}, "python": None, "disabled": None}
|
|
102
|
+
|
|
103
|
+
def flow_init(
|
|
104
|
+
self, flow, graph, environment, flow_datastore, metadata, logger, echo, options
|
|
105
|
+
):
|
|
106
|
+
from metaflow import decorators
|
|
107
|
+
|
|
108
|
+
decorators._attach_decorators(flow, ["pypi"])
|
|
109
|
+
|
|
110
|
+
# @pypi uses a conda environment to create a virtual environment.
|
|
111
|
+
# The conda environment can be created through micromamba.
|
|
112
|
+
_supported_virtual_envs = ["conda"]
|
|
113
|
+
|
|
114
|
+
# To placate people who don't want to see a shred of conda in UX, we symlink
|
|
115
|
+
# --environment=pypi to --environment=conda
|
|
116
|
+
_supported_virtual_envs.extend(["pypi"])
|
|
117
|
+
|
|
118
|
+
# The --environment= requirement ensures that valid virtual environments are
|
|
119
|
+
# created for every step to execute it, greatly simplifying the @conda
|
|
120
|
+
# implementation.
|
|
121
|
+
if environment.TYPE not in _supported_virtual_envs:
|
|
122
|
+
raise InvalidEnvironmentException(
|
|
123
|
+
"@%s decorator requires %s"
|
|
124
|
+
% (
|
|
125
|
+
self.name,
|
|
126
|
+
" or ".join(
|
|
127
|
+
["--environment=%s" % env for env in _supported_virtual_envs]
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
)
|