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.

Files changed (57) hide show
  1. metaflow/_vendor/packaging/__init__.py +15 -0
  2. metaflow/_vendor/packaging/_elffile.py +108 -0
  3. metaflow/_vendor/packaging/_manylinux.py +238 -0
  4. metaflow/_vendor/packaging/_musllinux.py +80 -0
  5. metaflow/_vendor/packaging/_parser.py +328 -0
  6. metaflow/_vendor/packaging/_structures.py +61 -0
  7. metaflow/_vendor/packaging/_tokenizer.py +188 -0
  8. metaflow/_vendor/packaging/markers.py +245 -0
  9. metaflow/_vendor/packaging/requirements.py +95 -0
  10. metaflow/_vendor/packaging/specifiers.py +1005 -0
  11. metaflow/_vendor/packaging/tags.py +546 -0
  12. metaflow/_vendor/packaging/utils.py +141 -0
  13. metaflow/_vendor/packaging/version.py +563 -0
  14. metaflow/_vendor/v3_7/__init__.py +1 -0
  15. metaflow/_vendor/v3_7/zipp.py +329 -0
  16. metaflow/metaflow_config.py +2 -1
  17. metaflow/metaflow_environment.py +3 -1
  18. metaflow/mflog/mflog.py +7 -1
  19. metaflow/multicore_utils.py +12 -2
  20. metaflow/plugins/__init__.py +8 -3
  21. metaflow/plugins/airflow/airflow.py +13 -0
  22. metaflow/plugins/argo/argo_client.py +16 -0
  23. metaflow/plugins/argo/argo_events.py +7 -1
  24. metaflow/plugins/argo/argo_workflows.py +62 -0
  25. metaflow/plugins/argo/argo_workflows_cli.py +15 -0
  26. metaflow/plugins/aws/batch/batch.py +10 -0
  27. metaflow/plugins/aws/batch/batch_cli.py +1 -2
  28. metaflow/plugins/aws/batch/batch_decorator.py +2 -9
  29. metaflow/plugins/datatools/s3/s3.py +4 -0
  30. metaflow/plugins/env_escape/client.py +24 -3
  31. metaflow/plugins/env_escape/stub.py +2 -8
  32. metaflow/plugins/kubernetes/kubernetes.py +13 -0
  33. metaflow/plugins/kubernetes/kubernetes_cli.py +1 -2
  34. metaflow/plugins/kubernetes/kubernetes_decorator.py +9 -2
  35. metaflow/plugins/pypi/__init__.py +29 -0
  36. metaflow/plugins/pypi/bootstrap.py +131 -0
  37. metaflow/plugins/pypi/conda_decorator.py +335 -0
  38. metaflow/plugins/pypi/conda_environment.py +414 -0
  39. metaflow/plugins/pypi/micromamba.py +294 -0
  40. metaflow/plugins/pypi/pip.py +205 -0
  41. metaflow/plugins/pypi/pypi_decorator.py +130 -0
  42. metaflow/plugins/pypi/pypi_environment.py +7 -0
  43. metaflow/plugins/pypi/utils.py +75 -0
  44. metaflow/task.py +0 -3
  45. metaflow/vendor.py +1 -0
  46. {ob_metaflow-2.9.10.1.dist-info → ob_metaflow-2.10.2.6.dist-info}/METADATA +1 -1
  47. {ob_metaflow-2.9.10.1.dist-info → ob_metaflow-2.10.2.6.dist-info}/RECORD +51 -33
  48. {ob_metaflow-2.9.10.1.dist-info → ob_metaflow-2.10.2.6.dist-info}/WHEEL +1 -1
  49. metaflow/plugins/conda/__init__.py +0 -90
  50. metaflow/plugins/conda/batch_bootstrap.py +0 -104
  51. metaflow/plugins/conda/conda.py +0 -247
  52. metaflow/plugins/conda/conda_environment.py +0 -136
  53. metaflow/plugins/conda/conda_flow_decorator.py +0 -35
  54. metaflow/plugins/conda/conda_step_decorator.py +0 -416
  55. {ob_metaflow-2.9.10.1.dist-info → ob_metaflow-2.10.2.6.dist-info}/LICENSE +0 -0
  56. {ob_metaflow-2.9.10.1.dist-info → ob_metaflow-2.10.2.6.dist-info}/entry_points.txt +0 -0
  57. {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
+ )
@@ -0,0 +1,7 @@
1
+ from .conda_environment import CondaEnvironment
2
+
3
+
4
+ # To placate people who don't want to see a shred of conda in UX, we symlink
5
+ # --environment=pypi to --environment=conda
6
+ class PyPIEnvironment(CondaEnvironment):
7
+ TYPE = "pypi"