ob-metaflow-extensions 1.1.78__py2.py3-none-any.whl → 1.1.80__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-extensions might be problematic. Click here for more details.

Files changed (19) hide show
  1. metaflow_extensions/outerbounds/config/__init__.py +28 -0
  2. metaflow_extensions/outerbounds/plugins/__init__.py +17 -3
  3. metaflow_extensions/outerbounds/plugins/fast_bakery/__init__.py +0 -0
  4. metaflow_extensions/outerbounds/plugins/fast_bakery/docker_environment.py +268 -0
  5. metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery.py +160 -0
  6. metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery_cli.py +54 -0
  7. metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery_decorator.py +50 -0
  8. metaflow_extensions/outerbounds/plugins/snowpark/__init__.py +0 -0
  9. metaflow_extensions/outerbounds/plugins/snowpark/snowpark.py +299 -0
  10. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_cli.py +271 -0
  11. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_client.py +123 -0
  12. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_decorator.py +264 -0
  13. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_exceptions.py +13 -0
  14. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_job.py +235 -0
  15. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_service_spec.py +259 -0
  16. {ob_metaflow_extensions-1.1.78.dist-info → ob_metaflow_extensions-1.1.80.dist-info}/METADATA +2 -2
  17. {ob_metaflow_extensions-1.1.78.dist-info → ob_metaflow_extensions-1.1.80.dist-info}/RECORD +19 -6
  18. {ob_metaflow_extensions-1.1.78.dist-info → ob_metaflow_extensions-1.1.80.dist-info}/WHEEL +0 -0
  19. {ob_metaflow_extensions-1.1.78.dist-info → ob_metaflow_extensions-1.1.80.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,264 @@
1
+ import os
2
+ import sys
3
+ import platform
4
+
5
+ from metaflow import R, current
6
+ from metaflow.metadata import MetaDatum
7
+ from metaflow.metadata.util import sync_local_metadata_to_datastore
8
+ from metaflow.sidecar import Sidecar
9
+ from metaflow.decorators import StepDecorator
10
+ from metaflow.exception import MetaflowException
11
+ from metaflow.metaflow_config import (
12
+ DEFAULT_CONTAINER_IMAGE,
13
+ DEFAULT_CONTAINER_REGISTRY,
14
+ SNOWPARK_ACCOUNT,
15
+ SNOWPARK_USER,
16
+ SNOWPARK_PASSWORD,
17
+ SNOWPARK_ROLE,
18
+ SNOWPARK_DATABASE,
19
+ SNOWPARK_WAREHOUSE,
20
+ SNOWPARK_SCHEMA,
21
+ )
22
+
23
+ from metaflow.metaflow_config import (
24
+ DATASTORE_LOCAL_DIR,
25
+ )
26
+
27
+ from .snowpark_exceptions import SnowflakeException
28
+ from metaflow.plugins.aws.aws_utils import get_docker_registry
29
+
30
+
31
+ class Snowflake(object):
32
+ def __init__(self, connection_params):
33
+ self.connection_params = connection_params
34
+
35
+ def session(self):
36
+ # if using the pypi/conda decorator with @snowpark of any step,
37
+ # make sure to pass {'snowflake': '0.11.0'} as well to that step
38
+ try:
39
+ from snowflake.snowpark import Session
40
+
41
+ session = Session.builder.configs(self.connection_params).create()
42
+ return session
43
+ except (NameError, ImportError, ModuleNotFoundError):
44
+ raise SnowflakeException(
45
+ "Could not import module 'snowflake'.\n\nInstall Snowflake "
46
+ "Python package (https://pypi.org/project/snowflake/) first.\n"
47
+ "You can install the module by using the @pypi decorator - "
48
+ "Eg: @pypi(packages={'snowflake': '0.11.0'})\n"
49
+ )
50
+
51
+
52
+ class SnowparkDecorator(StepDecorator):
53
+ name = "snowpark"
54
+
55
+ defaults = {
56
+ "account": None,
57
+ "user": None,
58
+ "password": None,
59
+ "role": None,
60
+ "database": None,
61
+ "warehouse": None,
62
+ "schema": None,
63
+ "image": None,
64
+ "stage": None,
65
+ "compute_pool": None,
66
+ "volume_mounts": None,
67
+ "external_integration": None,
68
+ "cpu": None,
69
+ "gpu": None,
70
+ "memory": None,
71
+ }
72
+
73
+ package_url = None
74
+ package_sha = None
75
+ run_time_limit = None
76
+
77
+ def __init__(self, attributes=None, statically_defined=False):
78
+ super(SnowparkDecorator, self).__init__(attributes, statically_defined)
79
+
80
+ if not self.attributes["account"]:
81
+ self.attributes["account"] = SNOWPARK_ACCOUNT
82
+ if not self.attributes["user"]:
83
+ self.attributes["user"] = SNOWPARK_USER
84
+ if not self.attributes["password"]:
85
+ self.attributes["password"] = SNOWPARK_PASSWORD
86
+ if not self.attributes["role"]:
87
+ self.attributes["role"] = SNOWPARK_ROLE
88
+ if not self.attributes["database"]:
89
+ self.attributes["database"] = SNOWPARK_DATABASE
90
+ if not self.attributes["warehouse"]:
91
+ self.attributes["warehouse"] = SNOWPARK_WAREHOUSE
92
+ if not self.attributes["schema"]:
93
+ self.attributes["schema"] = SNOWPARK_SCHEMA
94
+
95
+ # If no docker image is explicitly specified, impute a default image.
96
+ if not self.attributes["image"]:
97
+ # If metaflow-config specifies a docker image, just use that.
98
+ if DEFAULT_CONTAINER_IMAGE:
99
+ self.attributes["image"] = DEFAULT_CONTAINER_IMAGE
100
+ # If metaflow-config doesn't specify a docker image, assign a
101
+ # default docker image.
102
+ else:
103
+ # Metaflow-R has its own default docker image (rocker family)
104
+ if R.use_r():
105
+ self.attributes["image"] = R.container_image()
106
+ # Default to vanilla Python image corresponding to major.minor
107
+ # version of the Python interpreter launching the flow.
108
+ self.attributes["image"] = "python:%s.%s" % (
109
+ platform.python_version_tuple()[0],
110
+ platform.python_version_tuple()[1],
111
+ )
112
+
113
+ # Assign docker registry URL for the image.
114
+ if not get_docker_registry(self.attributes["image"]):
115
+ if DEFAULT_CONTAINER_REGISTRY:
116
+ self.attributes["image"] = "%s/%s" % (
117
+ DEFAULT_CONTAINER_REGISTRY.rstrip("/"),
118
+ self.attributes["image"],
119
+ )
120
+
121
+ # Refer https://github.com/Netflix/metaflow/blob/master/docs/lifecycle.png
122
+ # to understand where these functions are invoked in the lifecycle of a
123
+ # Metaflow flow.
124
+ def step_init(self, flow, graph, step, decos, environment, flow_datastore, logger):
125
+ # Set internal state.
126
+ self.logger = logger
127
+ self.environment = environment
128
+ self.step = step
129
+ self.flow_datastore = flow_datastore
130
+
131
+ if any([deco.name == "parallel" for deco in decos]):
132
+ raise MetaflowException(
133
+ "Step *{step}* contains a @parallel decorator "
134
+ "with the @snowpark decorator. @parallel is not supported with @snowpark.".format(
135
+ step=step
136
+ )
137
+ )
138
+
139
+ def package_init(self, flow, step_name, environment):
140
+ try:
141
+ # Snowflake is a soft dependency.
142
+ from snowflake.snowpark import Session
143
+ except (NameError, ImportError, ModuleNotFoundError):
144
+ raise SnowflakeException(
145
+ "Could not import module 'snowflake'.\n\nInstall Snowflake "
146
+ "Python package (https://pypi.org/project/snowflake/) first.\n"
147
+ "You can install the module by executing - "
148
+ "%s -m pip install snowflake\n"
149
+ "or equivalent through your favorite Python package manager."
150
+ % sys.executable
151
+ )
152
+
153
+ def runtime_init(self, flow, graph, package, run_id):
154
+ # Set some more internal state.
155
+ self.flow = flow
156
+ self.graph = graph
157
+ self.package = package
158
+ self.run_id = run_id
159
+
160
+ def runtime_task_created(
161
+ self, task_datastore, task_id, split_index, input_paths, is_cloned, ubf_context
162
+ ):
163
+ if not is_cloned:
164
+ self._save_package_once(self.flow_datastore, self.package)
165
+
166
+ def runtime_step_cli(
167
+ self, cli_args, retry_count, max_user_code_retries, ubf_context
168
+ ):
169
+ if retry_count <= max_user_code_retries:
170
+ cli_args.commands = ["snowpark", "step"]
171
+ cli_args.command_args.append(self.package_sha)
172
+ cli_args.command_args.append(self.package_url)
173
+ cli_args.command_options.update(self.attributes)
174
+ cli_args.command_options["run-time-limit"] = self.run_time_limit
175
+ if not R.use_r():
176
+ cli_args.entrypoint[0] = sys.executable
177
+
178
+ def task_pre_step(
179
+ self,
180
+ step_name,
181
+ task_datastore,
182
+ metadata,
183
+ run_id,
184
+ task_id,
185
+ flow,
186
+ graph,
187
+ retry_count,
188
+ max_retries,
189
+ ubf_context,
190
+ inputs,
191
+ ):
192
+ self.metadata = metadata
193
+ self.task_datastore = task_datastore
194
+
195
+ # this path will exist within snowpark container services
196
+ login_token = open("/snowflake/session/token", "r").read()
197
+ connection_params = {
198
+ "account": os.environ.get("SNOWFLAKE_ACCOUNT"),
199
+ "host": os.environ.get("SNOWFLAKE_HOST"),
200
+ "authenticator": "oauth",
201
+ "token": login_token,
202
+ "database": os.environ.get("SNOWFLAKE_DATABASE"),
203
+ "schema": os.environ.get("SNOWFLAKE_SCHEMA"),
204
+ "autocommit": True,
205
+ "client_session_keep_alive": True,
206
+ }
207
+
208
+ # SNOWFLAKE_WAREHOUSE is injected explicitly by us
209
+ # but is not really required. So if it exists, we use it in
210
+ # connection parameters
211
+ if os.environ.get("SNOWFLAKE_WAREHOUSE"):
212
+ connection_params["warehouse"] = os.environ.get("SNOWFLAKE_WAREHOUSE")
213
+
214
+ snowflake_obj = Snowflake(connection_params)
215
+ current._update_env({"snowflake": snowflake_obj})
216
+
217
+ meta = {}
218
+ if "METAFLOW_SNOWPARK_WORKLOAD" in os.environ:
219
+ meta["snowflake-account"] = os.environ.get("SNOWFLAKE_ACCOUNT")
220
+ meta["snowflake-database"] = os.environ.get("SNOWFLAKE_DATABASE")
221
+ meta["snowflake-schema"] = os.environ.get("SNOWFLAKE_SCHEMA")
222
+ meta["snowflake-host"] = os.environ.get("SNOWFLAKE_HOST")
223
+ meta["snowflake-service-name"] = os.environ.get("SNOWFLAKE_SERVICE_NAME")
224
+
225
+ self._save_logs_sidecar = Sidecar("save_logs_periodically")
226
+ self._save_logs_sidecar.start()
227
+
228
+ if len(meta) > 0:
229
+ entries = [
230
+ MetaDatum(
231
+ field=k,
232
+ value=v,
233
+ type=k,
234
+ tags=["attempt_id:{0}".format(retry_count)],
235
+ )
236
+ for k, v in meta.items()
237
+ if v is not None
238
+ ]
239
+ # Register book-keeping metadata for debugging.
240
+ metadata.register_metadata(run_id, step_name, task_id, entries)
241
+
242
+ def task_finished(
243
+ self, step_name, flow, graph, is_task_ok, retry_count, max_retries
244
+ ):
245
+ if "METAFLOW_SNOWPARK_WORKLOAD" in os.environ:
246
+ if hasattr(self, "metadata") and self.metadata.TYPE == "local":
247
+ # Note that the datastore is *always* Amazon S3 (see
248
+ # runtime_task_created function).
249
+ sync_local_metadata_to_datastore(
250
+ DATASTORE_LOCAL_DIR, self.task_datastore
251
+ )
252
+
253
+ try:
254
+ self._save_logs_sidecar.terminate()
255
+ except:
256
+ # Best effort kill
257
+ pass
258
+
259
+ @classmethod
260
+ def _save_package_once(cls, flow_datastore, package):
261
+ if cls.package_url is None:
262
+ cls.package_url, cls.package_sha = flow_datastore.save_data(
263
+ [package.blob], len_hint=1
264
+ )[0]
@@ -0,0 +1,13 @@
1
+ from metaflow.exception import MetaflowException
2
+
3
+
4
+ class SnowflakeException(MetaflowException):
5
+ headline = "Snowflake error"
6
+
7
+
8
+ class SnowparkException(MetaflowException):
9
+ headline = "Snowpark error"
10
+
11
+
12
+ class SnowparkKilledException(MetaflowException):
13
+ headline = "Snowpark job killed"
@@ -0,0 +1,235 @@
1
+ import time
2
+
3
+ from .snowpark_client import SnowparkClient
4
+ from .snowpark_service_spec import (
5
+ Container,
6
+ Resources,
7
+ SnowparkServiceSpec,
8
+ VolumeMount,
9
+ )
10
+ from .snowpark_exceptions import SnowparkException
11
+
12
+ mapping = str.maketrans("0123456789", "abcdefghij")
13
+
14
+
15
+ # keep only alpha numeric characters and underscores..
16
+ def sanitize_name(job_name: str):
17
+ return "".join(char for char in job_name if char.isalnum() or char == "_")
18
+
19
+
20
+ # this is not a decorator since the exception imports need to be inside
21
+ # and not at the top level..
22
+ def retry_operation(
23
+ exception_type, func, max_retries=3, retry_delay=2, *args, **kwargs
24
+ ):
25
+ for attempt in range(max_retries):
26
+ try:
27
+ return func(*args, **kwargs)
28
+ except exception_type as e:
29
+ if attempt < max_retries - 1:
30
+ time.sleep(retry_delay)
31
+ else:
32
+ raise e
33
+
34
+
35
+ class SnowparkJob(object):
36
+ def __init__(self, client: SnowparkClient, name, command, **kwargs):
37
+ self.client = client
38
+ self.name = sanitize_name(name)
39
+ self.command = command
40
+ self.kwargs = kwargs
41
+ self.container_name = self.name.translate(mapping).lower()
42
+
43
+ def create_job_spec(self):
44
+ if self.kwargs.get("image") is None:
45
+ raise SnowparkException(
46
+ "Unable to launch job on Snowpark Container Services. No docker 'image' specified."
47
+ )
48
+
49
+ if self.kwargs.get("stage") is None:
50
+ raise SnowparkException(
51
+ "Unable to launch job on Snowpark Container Services. No 'stage' specified."
52
+ )
53
+
54
+ if self.kwargs.get("compute_pool") is None:
55
+ raise SnowparkException(
56
+ "Unable to launch job on Snowpark Container Services. No 'compute_pool' specified."
57
+ )
58
+
59
+ resources = Resources(
60
+ requests={
61
+ k: v
62
+ for k, v in [
63
+ ("cpu", self.kwargs.get("cpu")),
64
+ ("nvidia.com/gpu", self.kwargs.get("gpu")),
65
+ ("memory", self.kwargs.get("memory")),
66
+ ]
67
+ if v
68
+ },
69
+ limits={
70
+ k: v
71
+ for k, v in [
72
+ ("nvidia.com/gpu", self.kwargs.get("gpu")),
73
+ ]
74
+ if v
75
+ },
76
+ )
77
+
78
+ volume_mounts = self.kwargs.get("volume_mounts")
79
+ vm_objs = []
80
+ if volume_mounts:
81
+ if isinstance(volume_mounts, str):
82
+ volume_mounts = [volume_mounts]
83
+ for vm in volume_mounts:
84
+ name, mount_path = vm.split(":", 1)
85
+ vm_objs.append(VolumeMount(name=name, mount_path=mount_path))
86
+
87
+ container = (
88
+ Container(name=self.container_name, image=self.kwargs.get("image"))
89
+ .env(self.kwargs.get("environment_variables"))
90
+ .resources(resources)
91
+ .volume_mounts(vm_objs)
92
+ .command(self.command)
93
+ )
94
+
95
+ self.spec = SnowparkServiceSpec().containers([container])
96
+ return self
97
+
98
+ def environment_variable(self, name, value):
99
+ # Never set to None
100
+ if value is None:
101
+ return self
102
+ self.kwargs["environment_variables"] = dict(
103
+ self.kwargs.get("environment_variables", {}), **{name: value}
104
+ )
105
+ return self
106
+
107
+ def create(self):
108
+ return self.create_job_spec()
109
+
110
+ def execute(self):
111
+ query_id, service_name = self.client.submit(
112
+ self.name,
113
+ self.spec,
114
+ self.kwargs.get("stage"),
115
+ self.kwargs.get("compute_pool"),
116
+ self.kwargs.get("external_integration"),
117
+ )
118
+ return RunningJob(
119
+ client=self.client,
120
+ query_id=query_id,
121
+ service_name=service_name,
122
+ **self.kwargs
123
+ )
124
+
125
+ def image(self, image):
126
+ self.kwargs["image"] = image
127
+ return self
128
+
129
+ def stage(self, stage):
130
+ self.kwargs["stage"] = stage
131
+ return self
132
+
133
+ def compute_pool(self, compute_pool):
134
+ self.kwargs["compute_pool"] = compute_pool
135
+ return self
136
+
137
+ def volume_mounts(self, volume_mounts):
138
+ self.kwargs["volume_mounts"] = volume_mounts
139
+ return self
140
+
141
+ def external_integration(self, external_integration):
142
+ self.kwargs["external_integration"] = external_integration
143
+ return self
144
+
145
+ def cpu(self, cpu):
146
+ self.kwargs["cpu"] = cpu
147
+ return self
148
+
149
+ def gpu(self, gpu):
150
+ self.kwargs["gpu"] = gpu
151
+ return self
152
+
153
+ def memory(self, memory):
154
+ self.kwargs["memory"] = memory
155
+ return self
156
+
157
+
158
+ class RunningJob(object):
159
+ def __init__(self, client, query_id, service_name, **kwargs):
160
+ self.client = client
161
+ self.query_id = query_id
162
+ self.service_name = service_name
163
+ self.kwargs = kwargs
164
+
165
+ from snowflake.core.exceptions import NotFoundError
166
+
167
+ self.service = retry_operation(NotFoundError, self.__get_service)
168
+
169
+ def __get_service(self):
170
+ db = self.client.session.get_current_database()
171
+ schema = self.client.session.get_current_schema()
172
+ return (
173
+ self.client.root.databases[db].schemas[schema].services[self.service_name]
174
+ )
175
+
176
+ def __repr__(self):
177
+ return "{}('{}')".format(self.__class__.__name__, self.query_id)
178
+
179
+ @property
180
+ def id(self):
181
+ return self.query_id
182
+
183
+ @property
184
+ def job_name(self):
185
+ return self.service_name
186
+
187
+ def status_obj(self, timeout=0):
188
+ from snowflake.core.exceptions import APIError, NotFoundError
189
+
190
+ try:
191
+ return retry_operation(
192
+ APIError, self.service.get_service_status, timeout=timeout
193
+ )
194
+ except NotFoundError:
195
+ raise SnowparkException(
196
+ "The image *%s* most probably doesn't exist on Snowpark, or too many resources (CPU, GPU, memory) were requested."
197
+ % self.kwargs.get("image")
198
+ )
199
+
200
+ @property
201
+ def status(self):
202
+ return self.status_obj()[0].get("status")
203
+
204
+ @property
205
+ def message(self):
206
+ return self.status_obj()[0].get("message")
207
+
208
+ @property
209
+ def is_waiting(self):
210
+ return self.status == "PENDING"
211
+
212
+ @property
213
+ def is_running(self):
214
+ return self.status in ["PENDING", "READY"]
215
+
216
+ @property
217
+ def has_failed(self):
218
+ return self.status == "FAILED"
219
+
220
+ @property
221
+ def has_succeeded(self):
222
+ return self.status == "DONE"
223
+
224
+ @property
225
+ def has_finished(self):
226
+ return self.has_succeeded or self.has_failed
227
+
228
+ def kill(self):
229
+ from snowflake.core.exceptions import NotFoundError
230
+
231
+ try:
232
+ if not self.has_finished:
233
+ self.client.terminate_job(service=self.service)
234
+ except (NotFoundError, TypeError):
235
+ pass