ob-metaflow-extensions 1.1.79__tar.gz → 1.1.81__tar.gz
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.
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/PKG-INFO +1 -1
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/config/__init__.py +33 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/plugins/__init__.py +17 -3
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/fast_bakery/docker_environment.py +268 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery.py +160 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery_cli.py +54 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery_decorator.py +50 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/nvcf/__init__.py +0 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/snowpark/__init__.py +0 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/snowpark/snowpark.py +299 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_cli.py +271 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_client.py +123 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_decorator.py +264 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_exceptions.py +13 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_job.py +235 -0
- ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_service_spec.py +259 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/ob_metaflow_extensions.egg-info/PKG-INFO +1 -1
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/ob_metaflow_extensions.egg-info/SOURCES.txt +13 -0
- ob-metaflow-extensions-1.1.81/ob_metaflow_extensions.egg-info/requires.txt +3 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/setup.py +2 -2
- ob-metaflow-extensions-1.1.79/metaflow_extensions/outerbounds/config/__init__.py +0 -5
- ob-metaflow-extensions-1.1.79/ob_metaflow_extensions.egg-info/requires.txt +0 -3
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/README.md +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/__init__.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/plugins/auth_server.py +0 -0
- {ob-metaflow-extensions-1.1.79/metaflow_extensions/outerbounds/plugins/nvcf → ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/fast_bakery}/__init__.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/plugins/kubernetes/__init__.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/plugins/kubernetes/kubernetes_client.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/plugins/nim/__init__.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/plugins/nim/nim_manager.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/plugins/nvcf/nvcf.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/plugins/nvcf/nvcf_cli.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/plugins/nvcf/nvcf_decorator.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/plugins/perimeters.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/profilers/__init__.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/profilers/gpu.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/remote_config.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/toplevel/__init__.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/toplevel/plugins/azure/__init__.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/toplevel/plugins/gcp/__init__.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/metaflow_extensions/outerbounds/toplevel/plugins/kubernetes/__init__.py +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/ob_metaflow_extensions.egg-info/dependency_links.txt +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/ob_metaflow_extensions.egg-info/top_level.txt +0 -0
- {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.81}/setup.cfg +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from metaflow.metaflow_config import from_conf
|
|
2
|
+
|
|
3
|
+
DEFAULT_AWS_CLIENT_PROVIDER = "obp"
|
|
4
|
+
|
|
5
|
+
DEFAULT_AZURE_CLIENT_PROVIDER = "obp"
|
|
6
|
+
|
|
7
|
+
DEFAULT_GCP_CLIENT_PROVIDER = "obp"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
###
|
|
11
|
+
# On Demand Docker image build configuration
|
|
12
|
+
###
|
|
13
|
+
# Image builder service url
|
|
14
|
+
FAST_BAKERY_URL = from_conf("FAST_BAKERY_URL", None)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
###
|
|
18
|
+
# Snowpark configuration
|
|
19
|
+
###
|
|
20
|
+
# Snowflake account to use with the @snowpark decorator
|
|
21
|
+
SNOWPARK_ACCOUNT = from_conf("SNOWPARK_ACCOUNT")
|
|
22
|
+
# Snowflake user to use with the @snowpark decorator
|
|
23
|
+
SNOWPARK_USER = from_conf("SNOWPARK_USER")
|
|
24
|
+
# Snowflake password to use with the @snowpark decorator
|
|
25
|
+
SNOWPARK_PASSWORD = from_conf("SNOWPARK_PASSWORD")
|
|
26
|
+
# Snowflake role to use with the @snowpark decorator
|
|
27
|
+
SNOWPARK_ROLE = from_conf("SNOWPARK_ROLE")
|
|
28
|
+
# Snowflake database to use with the @snowpark decorator
|
|
29
|
+
SNOWPARK_DATABASE = from_conf("SNOWPARK_DATABASE")
|
|
30
|
+
# Snowflake warehouse to use with the @snowpark decorator
|
|
31
|
+
SNOWPARK_WAREHOUSE = from_conf("SNOWPARK_WAREHOUSE")
|
|
32
|
+
# Snowflake schema to use with the @snowpark decorator
|
|
33
|
+
SNOWPARK_SCHEMA = from_conf("SNOWPARK_SCHEMA")
|
|
@@ -178,7 +178,6 @@ class ObpGcpAuthProvider(object):
|
|
|
178
178
|
|
|
179
179
|
@staticmethod
|
|
180
180
|
def get_gs_storage_client(*args, **kwargs):
|
|
181
|
-
|
|
182
181
|
import sys
|
|
183
182
|
from metaflow_extensions.outerbounds.plugins.auth_server import get_token
|
|
184
183
|
|
|
@@ -240,8 +239,19 @@ class ObpGcpAuthProvider(object):
|
|
|
240
239
|
|
|
241
240
|
|
|
242
241
|
GCP_CLIENT_PROVIDERS_DESC = [("obp", ".ObpGcpAuthProvider")]
|
|
243
|
-
CLIS_DESC = [
|
|
244
|
-
|
|
242
|
+
CLIS_DESC = [
|
|
243
|
+
("nvcf", ".nvcf.nvcf_cli.cli"),
|
|
244
|
+
("fast-bakery", ".fast_bakery.fast_bakery_cli.cli"),
|
|
245
|
+
("snowpark", ".snowpark.snowpark_cli.cli"),
|
|
246
|
+
]
|
|
247
|
+
STEP_DECORATORS_DESC = [
|
|
248
|
+
("nvidia", ".nvcf.nvcf_decorator.NvcfDecorator"),
|
|
249
|
+
(
|
|
250
|
+
"fast_bakery_internal",
|
|
251
|
+
".fast_bakery.fast_bakery_decorator.InternalFastBakeryDecorator",
|
|
252
|
+
),
|
|
253
|
+
("snowpark", ".snowpark.snowpark_decorator.SnowparkDecorator"),
|
|
254
|
+
]
|
|
245
255
|
FLOW_DECORATORS_DESC = [("nim", ".nim.NimDecorator")]
|
|
246
256
|
TOGGLE_STEP_DECORATOR = [
|
|
247
257
|
"-batch",
|
|
@@ -250,3 +260,7 @@ TOGGLE_STEP_DECORATOR = [
|
|
|
250
260
|
]
|
|
251
261
|
|
|
252
262
|
TOGGLE_CLI = ["-batch", "-step-functions", "-airflow"]
|
|
263
|
+
|
|
264
|
+
ENVIRONMENTS_DESC = [
|
|
265
|
+
("fast-bakery", ".fast_bakery.docker_environment.DockerEnvironment")
|
|
266
|
+
]
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from metaflow.exception import MetaflowException
|
|
8
|
+
from metaflow.metaflow_config import (
|
|
9
|
+
FAST_BAKERY_URL,
|
|
10
|
+
get_pinned_conda_libs,
|
|
11
|
+
)
|
|
12
|
+
from metaflow.metaflow_environment import MetaflowEnvironment
|
|
13
|
+
from metaflow.plugins.pypi.conda_environment import CondaEnvironment
|
|
14
|
+
from .fast_bakery import FastBakery, FastBakeryApiResponse, FastBakeryException
|
|
15
|
+
from metaflow.plugins.aws.batch.batch_decorator import BatchDecorator
|
|
16
|
+
from metaflow.plugins.kubernetes.kubernetes_decorator import KubernetesDecorator
|
|
17
|
+
from metaflow.plugins.pypi.conda_decorator import CondaStepDecorator
|
|
18
|
+
from metaflow.plugins.pypi.pypi_decorator import PyPIStepDecorator
|
|
19
|
+
|
|
20
|
+
BAKERY_METAFILE = ".imagebakery-cache"
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import fcntl
|
|
25
|
+
from functools import wraps
|
|
26
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# TODO - ensure that both @conda/@pypi are not assigned to the same step
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cache_request(cache_file):
|
|
33
|
+
def decorator(func):
|
|
34
|
+
@wraps(func)
|
|
35
|
+
def wrapper(*args, **kwargs):
|
|
36
|
+
call_args = kwargs.copy()
|
|
37
|
+
call_args.update(zip(func.__code__.co_varnames, args))
|
|
38
|
+
call_args.pop("self", None)
|
|
39
|
+
cache_key = hashlib.md5(
|
|
40
|
+
json.dumps(call_args, sort_keys=True).encode("utf-8")
|
|
41
|
+
).hexdigest()
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
with open(cache_file, "r") as f:
|
|
45
|
+
cache = json.load(f)
|
|
46
|
+
if cache_key in cache:
|
|
47
|
+
return FastBakeryApiResponse(cache[cache_key])
|
|
48
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
49
|
+
cache = {}
|
|
50
|
+
|
|
51
|
+
result = func(*args, **kwargs)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
with open(cache_file, "r+") as f:
|
|
55
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
56
|
+
try:
|
|
57
|
+
f.seek(0)
|
|
58
|
+
cache = json.load(f)
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
cache = {}
|
|
61
|
+
|
|
62
|
+
cache[cache_key] = result.response
|
|
63
|
+
|
|
64
|
+
f.seek(0)
|
|
65
|
+
f.truncate()
|
|
66
|
+
json.dump(cache, f)
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
# path to cachefile might not exist.
|
|
69
|
+
os.makedirs(os.path.dirname(cache_file), exist_ok=True)
|
|
70
|
+
with open(cache_file, "w") as f:
|
|
71
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
72
|
+
json.dump({cache_key: result.response}, f)
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
return wrapper
|
|
77
|
+
|
|
78
|
+
return decorator
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class DockerEnvironmentException(MetaflowException):
|
|
82
|
+
headline = "Ran into an error while setting up the environment"
|
|
83
|
+
|
|
84
|
+
def __init__(self, msg):
|
|
85
|
+
super(DockerEnvironmentException, self).__init__(msg)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DockerEnvironment(MetaflowEnvironment):
|
|
89
|
+
TYPE = "fast-bakery"
|
|
90
|
+
_filecache = None
|
|
91
|
+
|
|
92
|
+
def __init__(self, flow):
|
|
93
|
+
self.skipped_steps = set()
|
|
94
|
+
self.flow = flow
|
|
95
|
+
|
|
96
|
+
self.bakery = FastBakery(url=FAST_BAKERY_URL)
|
|
97
|
+
self.results = {}
|
|
98
|
+
|
|
99
|
+
def set_local_root(self, local_root):
|
|
100
|
+
self.local_root = local_root
|
|
101
|
+
|
|
102
|
+
def decospecs(self):
|
|
103
|
+
return ("conda", "fast_bakery_internal") + super().decospecs()
|
|
104
|
+
|
|
105
|
+
def validate_environment(self, echo, datastore_type):
|
|
106
|
+
self.datastore_type = datastore_type
|
|
107
|
+
self.echo = echo
|
|
108
|
+
|
|
109
|
+
# Avoiding circular imports.
|
|
110
|
+
from metaflow.plugins import DATASTORES
|
|
111
|
+
|
|
112
|
+
self.datastore = [d for d in DATASTORES if d.TYPE == self.datastore_type][0]
|
|
113
|
+
|
|
114
|
+
def init_environment(self, echo):
|
|
115
|
+
self.skipped_steps = {
|
|
116
|
+
step.name
|
|
117
|
+
for step in self.flow
|
|
118
|
+
if not any(
|
|
119
|
+
isinstance(deco, (BatchDecorator, KubernetesDecorator))
|
|
120
|
+
for deco in step.decorators
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
steps_to_bake = [
|
|
125
|
+
step for step in self.flow if step.name not in self.skipped_steps
|
|
126
|
+
]
|
|
127
|
+
if steps_to_bake:
|
|
128
|
+
echo("Baking container image(s) ...")
|
|
129
|
+
self.results = self._bake(steps_to_bake, echo)
|
|
130
|
+
for step in self.flow:
|
|
131
|
+
for d in step.decorators:
|
|
132
|
+
if isinstance(d, (BatchDecorator, KubernetesDecorator)):
|
|
133
|
+
d.attributes["image"] = self.results[step.name].container_image
|
|
134
|
+
d.attributes["executable"] = self.results[step.name].python_path
|
|
135
|
+
echo("Container image(s) baked!")
|
|
136
|
+
|
|
137
|
+
if self.skipped_steps:
|
|
138
|
+
self.delegate = CondaEnvironment(self.flow)
|
|
139
|
+
self.delegate.set_local_root(self.local_root)
|
|
140
|
+
self.delegate.validate_environment(echo, self.datastore_type)
|
|
141
|
+
self.delegate.init_environment(echo, self.skipped_steps)
|
|
142
|
+
|
|
143
|
+
def _bake(self, steps, echo) -> Dict[str, FastBakeryApiResponse]:
|
|
144
|
+
metafile_path = get_fastbakery_metafile_path(self.local_root, self.flow.name)
|
|
145
|
+
|
|
146
|
+
@cache_request(metafile_path)
|
|
147
|
+
def _cached_bake(
|
|
148
|
+
python=None, pypi_packages=None, conda_packages=None, base_image=None
|
|
149
|
+
):
|
|
150
|
+
self.bakery._reset_payload()
|
|
151
|
+
self.bakery.python_version(python)
|
|
152
|
+
self.bakery.pypi_packages(pypi_packages)
|
|
153
|
+
self.bakery.conda_packages(conda_packages)
|
|
154
|
+
self.bakery.base_image(base_image)
|
|
155
|
+
# self.bakery.ignore_cache()
|
|
156
|
+
try:
|
|
157
|
+
res = self.bakery.bake()
|
|
158
|
+
if res.baking_stats:
|
|
159
|
+
echo(
|
|
160
|
+
"baked image in: %s milliseconds"
|
|
161
|
+
% res.baking_stats.solver_stats.duration_ms
|
|
162
|
+
)
|
|
163
|
+
return res
|
|
164
|
+
except FastBakeryException as ex:
|
|
165
|
+
raise DockerEnvironmentException(str(ex))
|
|
166
|
+
|
|
167
|
+
def prepare_step(step):
|
|
168
|
+
base_image = next(
|
|
169
|
+
(
|
|
170
|
+
d.attributes.get("image")
|
|
171
|
+
for d in step.decorators
|
|
172
|
+
if isinstance(d, (KubernetesDecorator))
|
|
173
|
+
),
|
|
174
|
+
None,
|
|
175
|
+
)
|
|
176
|
+
dependencies = next(
|
|
177
|
+
(
|
|
178
|
+
d
|
|
179
|
+
for d in step.decorators
|
|
180
|
+
if isinstance(d, (CondaStepDecorator, PyPIStepDecorator))
|
|
181
|
+
),
|
|
182
|
+
None,
|
|
183
|
+
)
|
|
184
|
+
python = next(
|
|
185
|
+
(
|
|
186
|
+
d.attributes["python"]
|
|
187
|
+
for d in step.decorators
|
|
188
|
+
if isinstance(d, CondaStepDecorator)
|
|
189
|
+
),
|
|
190
|
+
None,
|
|
191
|
+
)
|
|
192
|
+
pypi_deco = next(
|
|
193
|
+
(d for d in step.decorators if isinstance(d, PyPIStepDecorator)), None
|
|
194
|
+
)
|
|
195
|
+
# if pypi decorator is set and user has specified a python version, we must create a new environment.
|
|
196
|
+
# otherwise rely on the base environment
|
|
197
|
+
if pypi_deco is not None:
|
|
198
|
+
python = (
|
|
199
|
+
pypi_deco.attributes["python"]
|
|
200
|
+
if pypi_deco.is_attribute_user_defined("python")
|
|
201
|
+
else None
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
packages = get_pinned_conda_libs(python, self.datastore_type)
|
|
205
|
+
packages.update(dependencies.attributes["packages"] if dependencies else {})
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
"python": python,
|
|
209
|
+
"pypi_packages": (
|
|
210
|
+
packages if isinstance(dependencies, PyPIStepDecorator) else None
|
|
211
|
+
),
|
|
212
|
+
"conda_packages": (
|
|
213
|
+
packages if isinstance(dependencies, CondaStepDecorator) else None
|
|
214
|
+
),
|
|
215
|
+
"base_image": base_image,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
with ThreadPoolExecutor() as executor:
|
|
219
|
+
return {
|
|
220
|
+
step.name: _cached_bake(**args)
|
|
221
|
+
for step, args in zip(steps, executor.map(prepare_step, steps))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
def executable(self, step_name, default=None):
|
|
225
|
+
if step_name in self.skipped_steps:
|
|
226
|
+
return self.delegate.executable(step_name, default)
|
|
227
|
+
# default is set to the right executable
|
|
228
|
+
if default is not None:
|
|
229
|
+
return default
|
|
230
|
+
if default is None and step_name in self.results:
|
|
231
|
+
# try to read pythonpath from results. This can happen immediately after baking.
|
|
232
|
+
return self.results[step_name].python_path
|
|
233
|
+
# we lack a default and baking results. fallback to parent executable.
|
|
234
|
+
return super().executable(step_name, default)
|
|
235
|
+
|
|
236
|
+
def interpreter(self, step_name):
|
|
237
|
+
if step_name in self.skipped_steps:
|
|
238
|
+
return self.delegate.interpreter(step_name)
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
def is_disabled(self, step):
|
|
242
|
+
for decorator in step.decorators:
|
|
243
|
+
# @conda decorator is guaranteed to exist thanks to self.decospecs
|
|
244
|
+
if decorator.name in ["conda", "pypi"]:
|
|
245
|
+
# handle @conda/@pypi(disabled=True)
|
|
246
|
+
disabled = decorator.attributes["disabled"]
|
|
247
|
+
return str(disabled).lower() == "true"
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
def pylint_config(self):
|
|
251
|
+
config = super().pylint_config()
|
|
252
|
+
# Disable (import-error) in pylint
|
|
253
|
+
config.append("--disable=F0401")
|
|
254
|
+
return config
|
|
255
|
+
|
|
256
|
+
def bootstrap_commands(self, step_name, datastore_type):
|
|
257
|
+
if step_name in self.skipped_steps:
|
|
258
|
+
return self.delegate.bootstrap_commands(step_name, datastore_type)
|
|
259
|
+
# Bootstrap conda and execution environment for step
|
|
260
|
+
# we set the environment flag for skipping bootstrap dependencies, as these are
|
|
261
|
+
# provided in all baked images.
|
|
262
|
+
return [
|
|
263
|
+
"export METAFLOW_SKIP_INSTALL_DEPENDENCIES=$FASTBAKERY_IMAGE",
|
|
264
|
+
] + super().bootstrap_commands(step_name, datastore_type)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def get_fastbakery_metafile_path(local_root, flow_name):
|
|
268
|
+
return os.path.join(local_root, flow_name, BAKERY_METAFILE)
|
ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FastBakeryException(Exception):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SolverStats:
|
|
10
|
+
def __init__(self, stats) -> None:
|
|
11
|
+
self.stats = stats
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def duration_ms(self):
|
|
15
|
+
return self.stats["durationMs"]
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def packages_in_solved_environment(self):
|
|
19
|
+
return self.stats["packagesInSolvedEnvironment"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BakingStats:
|
|
23
|
+
def __init__(self, stats) -> None:
|
|
24
|
+
self.stats = stats
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def solver_stats(self) -> Optional[SolverStats]:
|
|
28
|
+
if "solverStats" not in self.stats:
|
|
29
|
+
return None
|
|
30
|
+
return SolverStats(self.stats["solverStats"])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FastBakeryApiResponse:
|
|
34
|
+
def __init__(self, response) -> None:
|
|
35
|
+
self.response = response
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def python_path(self) -> Optional[str]:
|
|
39
|
+
if not self.success:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
return self.response["success"]["pythonPath"]
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def container_image(self) -> Optional[str]:
|
|
46
|
+
if not self.success:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
return self.response["success"]["containerImage"]
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def success(self) -> bool:
|
|
53
|
+
return "success" in self.response
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def baking_stats(self) -> Optional[BakingStats]:
|
|
57
|
+
if not self.success:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if "bakingStats" not in self.response["success"]:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
if self.response["success"]["bakingStats"] is None:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return BakingStats(self.response["success"]["bakingStats"])
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def failure(self) -> bool:
|
|
70
|
+
return "failure" in self.response
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class FastBakery:
|
|
74
|
+
def __init__(self, url: str):
|
|
75
|
+
self.url = url
|
|
76
|
+
self.headers = {"Content-Type": "application/json", "Connection": "keep-alive"}
|
|
77
|
+
self._reset_payload()
|
|
78
|
+
|
|
79
|
+
def _reset_payload(self):
|
|
80
|
+
self._payload = {}
|
|
81
|
+
|
|
82
|
+
def python_version(self, version: str):
|
|
83
|
+
self._payload["pythonVersion"] = version
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def pypi_packages(self, packages: Dict[str, str]):
|
|
87
|
+
self._payload.setdefault("pipRequirements", []).extend(
|
|
88
|
+
self._format_packages(packages)
|
|
89
|
+
)
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def conda_packages(self, packages: Dict[str, str]):
|
|
93
|
+
self._payload.setdefault("condaMatchspecs", []).extend(
|
|
94
|
+
self._format_packages(packages)
|
|
95
|
+
)
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def base_image(self, image: str):
|
|
99
|
+
self._payload["baseImage"] = {"imageReference": image}
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def image_kind(self, kind: str):
|
|
103
|
+
self._payload["imageKind"] = kind
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def ignore_cache(self):
|
|
107
|
+
self._payload["cacheBehavior"] = {
|
|
108
|
+
"responseMaxAgeSeconds": 0,
|
|
109
|
+
"layerMaxAgeSeconds": 0,
|
|
110
|
+
"baseImageMaxAgeSeconds": 0,
|
|
111
|
+
}
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _format_packages(packages: Dict[str, str]) -> list:
|
|
116
|
+
if not packages:
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
def format_package(pkg: str, ver: str) -> str:
|
|
120
|
+
return (
|
|
121
|
+
f"{pkg}{ver}"
|
|
122
|
+
if any(ver.startswith(c) for c in [">", "<", "~", "@", "="])
|
|
123
|
+
else f"{pkg}=={ver}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return [format_package(pkg, ver) for pkg, ver in packages.items()]
|
|
127
|
+
|
|
128
|
+
def bake(self) -> FastBakeryApiResponse:
|
|
129
|
+
if "imageKind" not in self._payload:
|
|
130
|
+
self._payload["imageKind"] = "oci-zstd" # Set default if not specified
|
|
131
|
+
|
|
132
|
+
res = self._make_request(self._payload)
|
|
133
|
+
self._reset_payload()
|
|
134
|
+
return res
|
|
135
|
+
|
|
136
|
+
def _make_request(self, payload: Dict) -> FastBakeryApiResponse:
|
|
137
|
+
try:
|
|
138
|
+
from metaflow.metaflow_config import SERVICE_HEADERS
|
|
139
|
+
|
|
140
|
+
headers = {**self.headers, **(SERVICE_HEADERS or {})}
|
|
141
|
+
except ImportError:
|
|
142
|
+
headers = self.headers
|
|
143
|
+
response = requests.post(self.url, json=payload, headers=headers)
|
|
144
|
+
self._handle_error_response(response)
|
|
145
|
+
return FastBakeryApiResponse(response.json())
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _handle_error_response(response: requests.Response):
|
|
149
|
+
if response.status_code >= 500:
|
|
150
|
+
raise FastBakeryException(f"Server error: {response.text}")
|
|
151
|
+
|
|
152
|
+
body = response.json()
|
|
153
|
+
status_code = body.get("error", {}).get("statusCode", response.status_code)
|
|
154
|
+
if status_code >= 400:
|
|
155
|
+
try:
|
|
156
|
+
raise FastBakeryException(
|
|
157
|
+
f"*{body['error']['details']['kind']}*\n{body['error']['details']['message']}"
|
|
158
|
+
)
|
|
159
|
+
except KeyError:
|
|
160
|
+
raise FastBakeryException(f"Unexpected error: {body}")
|
ob-metaflow-extensions-1.1.81/metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery_cli.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from metaflow._vendor import click
|
|
4
|
+
from metaflow.cli import echo_always as echo
|
|
5
|
+
from metaflow.plugins.datastores.local_storage import LocalStorage
|
|
6
|
+
|
|
7
|
+
from .docker_environment import get_fastbakery_metafile_path
|
|
8
|
+
from .fast_bakery import FastBakeryApiResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def cli():
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@cli.group(help="Commands related to Fast Bakery support.")
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def fast_bakery(ctx):
|
|
19
|
+
path = LocalStorage.get_datastore_root_from_config(echo, create_on_absent=False)
|
|
20
|
+
ctx.obj.metafile_path = get_fastbakery_metafile_path(path, ctx.obj.flow.name)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@fast_bakery.command(help="Purge local Fast Bakery cache.")
|
|
24
|
+
@click.pass_obj
|
|
25
|
+
def purge(obj):
|
|
26
|
+
try:
|
|
27
|
+
os.remove(obj.metafile_path)
|
|
28
|
+
echo("Local Fast Bakery cache purged.")
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
echo("No local Fast Bakery cache found.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@fast_bakery.command(help="List the cached images")
|
|
34
|
+
@click.pass_obj
|
|
35
|
+
def images(obj):
|
|
36
|
+
current_cache = None
|
|
37
|
+
try:
|
|
38
|
+
with open(obj.metafile_path, "r") as f:
|
|
39
|
+
current_cache = json.load(f)
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
if current_cache:
|
|
44
|
+
echo("List of locally cached image tags:\n")
|
|
45
|
+
|
|
46
|
+
for val in current_cache.values():
|
|
47
|
+
response = FastBakeryApiResponse(val)
|
|
48
|
+
echo(response.container_image)
|
|
49
|
+
|
|
50
|
+
echo(
|
|
51
|
+
"In order to clear the cached images, you can use the command\n *fast-bakery purge*"
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
echo("No locally cached images.")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from metaflow.decorators import StepDecorator
|
|
3
|
+
from metaflow.metadata.metadata import MetaDatum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InternalFastBakeryDecorator(StepDecorator):
|
|
7
|
+
"""
|
|
8
|
+
Internal decorator to support Fast bakery
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
name = "fast_bakery_internal"
|
|
12
|
+
|
|
13
|
+
def task_pre_step(
|
|
14
|
+
self,
|
|
15
|
+
step_name,
|
|
16
|
+
task_datastore,
|
|
17
|
+
metadata,
|
|
18
|
+
run_id,
|
|
19
|
+
task_id,
|
|
20
|
+
flow,
|
|
21
|
+
graph,
|
|
22
|
+
retry_count,
|
|
23
|
+
max_retries,
|
|
24
|
+
ubf_context,
|
|
25
|
+
inputs,
|
|
26
|
+
):
|
|
27
|
+
# task_pre_step may run locally if fallback is activated for @catch
|
|
28
|
+
# decorator. In that scenario, we skip collecting Kubernetes execution
|
|
29
|
+
# metadata. A rudimentary way to detect non-local execution is to
|
|
30
|
+
# check for the existence of METAFLOW_KUBERNETES_WORKLOAD environment
|
|
31
|
+
# variable.
|
|
32
|
+
meta = {}
|
|
33
|
+
if "METAFLOW_KUBERNETES_WORKLOAD" in os.environ:
|
|
34
|
+
image = os.environ.get("FASTBAKERY_IMAGE")
|
|
35
|
+
if image:
|
|
36
|
+
meta["fast-bakery-image-name"] = image
|
|
37
|
+
|
|
38
|
+
if len(meta) > 0:
|
|
39
|
+
entries = [
|
|
40
|
+
MetaDatum(
|
|
41
|
+
field=k,
|
|
42
|
+
value=v,
|
|
43
|
+
type=k,
|
|
44
|
+
tags=["attempt_id:{0}".format(retry_count)],
|
|
45
|
+
)
|
|
46
|
+
for k, v in meta.items()
|
|
47
|
+
if v is not None
|
|
48
|
+
]
|
|
49
|
+
# Register book-keeping metadata for debugging.
|
|
50
|
+
metadata.register_metadata(run_id, step_name, task_id, entries)
|
|
File without changes
|
|
File without changes
|