ob-metaflow-extensions 1.1.79__tar.gz → 1.1.80__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.

Potentially problematic release.


This version of ob-metaflow-extensions might be problematic. Click here for more details.

Files changed (45) hide show
  1. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/PKG-INFO +1 -1
  2. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/config/__init__.py +33 -0
  3. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/plugins/__init__.py +17 -3
  4. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/fast_bakery/docker_environment.py +268 -0
  5. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery.py +160 -0
  6. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery_cli.py +54 -0
  7. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery_decorator.py +50 -0
  8. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/nvcf/__init__.py +0 -0
  9. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/snowpark/__init__.py +0 -0
  10. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/snowpark/snowpark.py +299 -0
  11. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_cli.py +271 -0
  12. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_client.py +123 -0
  13. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_decorator.py +264 -0
  14. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_exceptions.py +13 -0
  15. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_job.py +235 -0
  16. ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/snowpark/snowpark_service_spec.py +259 -0
  17. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/ob_metaflow_extensions.egg-info/PKG-INFO +1 -1
  18. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/ob_metaflow_extensions.egg-info/SOURCES.txt +13 -0
  19. ob-metaflow-extensions-1.1.80/ob_metaflow_extensions.egg-info/requires.txt +3 -0
  20. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/setup.py +2 -2
  21. ob-metaflow-extensions-1.1.79/metaflow_extensions/outerbounds/config/__init__.py +0 -5
  22. ob-metaflow-extensions-1.1.79/ob_metaflow_extensions.egg-info/requires.txt +0 -3
  23. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/README.md +0 -0
  24. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/__init__.py +0 -0
  25. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/plugins/auth_server.py +0 -0
  26. {ob-metaflow-extensions-1.1.79/metaflow_extensions/outerbounds/plugins/nvcf → ob-metaflow-extensions-1.1.80/metaflow_extensions/outerbounds/plugins/fast_bakery}/__init__.py +0 -0
  27. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/plugins/kubernetes/__init__.py +0 -0
  28. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/plugins/kubernetes/kubernetes_client.py +0 -0
  29. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/plugins/nim/__init__.py +0 -0
  30. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/plugins/nim/nim_manager.py +0 -0
  31. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/plugins/nvcf/nvcf.py +0 -0
  32. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/plugins/nvcf/nvcf_cli.py +0 -0
  33. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/plugins/nvcf/nvcf_decorator.py +0 -0
  34. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/plugins/perimeters.py +0 -0
  35. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/profilers/__init__.py +0 -0
  36. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/profilers/gpu.py +0 -0
  37. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/remote_config.py +0 -0
  38. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/toplevel/__init__.py +0 -0
  39. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +0 -0
  40. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/toplevel/plugins/azure/__init__.py +0 -0
  41. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/toplevel/plugins/gcp/__init__.py +0 -0
  42. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/metaflow_extensions/outerbounds/toplevel/plugins/kubernetes/__init__.py +0 -0
  43. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/ob_metaflow_extensions.egg-info/dependency_links.txt +0 -0
  44. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/ob_metaflow_extensions.egg-info/top_level.txt +0 -0
  45. {ob-metaflow-extensions-1.1.79 → ob-metaflow-extensions-1.1.80}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ob-metaflow-extensions
3
- Version: 1.1.79
3
+ Version: 1.1.80
4
4
  Summary: Outerbounds Platform Extensions for Metaflow
5
5
  Author: Outerbounds, Inc.
6
6
  License: Commercial
@@ -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 = [("nvcf", ".nvcf.nvcf_cli.cli")]
244
- STEP_DECORATORS_DESC = [("nvidia", ".nvcf.nvcf_decorator.NvcfDecorator")]
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)
@@ -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}")
@@ -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)