ob-metaflow 2.15.13.1__py2.py3-none-any.whl → 2.19.7.1rc0__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.
- metaflow/__init__.py +10 -3
- metaflow/_vendor/imghdr/__init__.py +186 -0
- metaflow/_vendor/yaml/__init__.py +427 -0
- metaflow/_vendor/yaml/composer.py +139 -0
- metaflow/_vendor/yaml/constructor.py +748 -0
- metaflow/_vendor/yaml/cyaml.py +101 -0
- metaflow/_vendor/yaml/dumper.py +62 -0
- metaflow/_vendor/yaml/emitter.py +1137 -0
- metaflow/_vendor/yaml/error.py +75 -0
- metaflow/_vendor/yaml/events.py +86 -0
- metaflow/_vendor/yaml/loader.py +63 -0
- metaflow/_vendor/yaml/nodes.py +49 -0
- metaflow/_vendor/yaml/parser.py +589 -0
- metaflow/_vendor/yaml/reader.py +185 -0
- metaflow/_vendor/yaml/representer.py +389 -0
- metaflow/_vendor/yaml/resolver.py +227 -0
- metaflow/_vendor/yaml/scanner.py +1435 -0
- metaflow/_vendor/yaml/serializer.py +111 -0
- metaflow/_vendor/yaml/tokens.py +104 -0
- metaflow/cards.py +4 -0
- metaflow/cli.py +125 -21
- metaflow/cli_components/init_cmd.py +1 -0
- metaflow/cli_components/run_cmds.py +204 -40
- metaflow/cli_components/step_cmd.py +160 -4
- metaflow/client/__init__.py +1 -0
- metaflow/client/core.py +198 -130
- metaflow/client/filecache.py +59 -32
- metaflow/cmd/code/__init__.py +2 -1
- metaflow/cmd/develop/stub_generator.py +49 -18
- metaflow/cmd/develop/stubs.py +9 -27
- metaflow/cmd/make_wrapper.py +30 -0
- metaflow/datastore/__init__.py +1 -0
- metaflow/datastore/content_addressed_store.py +40 -9
- metaflow/datastore/datastore_set.py +10 -1
- metaflow/datastore/flow_datastore.py +124 -4
- metaflow/datastore/spin_datastore.py +91 -0
- metaflow/datastore/task_datastore.py +92 -6
- metaflow/debug.py +5 -0
- metaflow/decorators.py +331 -82
- metaflow/extension_support/__init__.py +414 -356
- metaflow/extension_support/_empty_file.py +2 -2
- metaflow/flowspec.py +322 -82
- metaflow/graph.py +178 -15
- metaflow/includefile.py +25 -3
- metaflow/lint.py +94 -3
- metaflow/meta_files.py +13 -0
- metaflow/metadata_provider/metadata.py +13 -2
- metaflow/metaflow_config.py +66 -4
- metaflow/metaflow_environment.py +91 -25
- metaflow/metaflow_profile.py +18 -0
- metaflow/metaflow_version.py +16 -1
- metaflow/package/__init__.py +673 -0
- metaflow/packaging_sys/__init__.py +880 -0
- metaflow/packaging_sys/backend.py +128 -0
- metaflow/packaging_sys/distribution_support.py +153 -0
- metaflow/packaging_sys/tar_backend.py +99 -0
- metaflow/packaging_sys/utils.py +54 -0
- metaflow/packaging_sys/v1.py +527 -0
- metaflow/parameters.py +6 -2
- metaflow/plugins/__init__.py +6 -0
- metaflow/plugins/airflow/airflow.py +11 -1
- metaflow/plugins/airflow/airflow_cli.py +16 -5
- metaflow/plugins/argo/argo_client.py +42 -20
- metaflow/plugins/argo/argo_events.py +6 -6
- metaflow/plugins/argo/argo_workflows.py +1023 -344
- metaflow/plugins/argo/argo_workflows_cli.py +396 -94
- metaflow/plugins/argo/argo_workflows_decorator.py +9 -0
- metaflow/plugins/argo/argo_workflows_deployer_objects.py +75 -49
- metaflow/plugins/argo/capture_error.py +5 -2
- metaflow/plugins/argo/conditional_input_paths.py +35 -0
- metaflow/plugins/argo/exit_hooks.py +209 -0
- metaflow/plugins/argo/param_val.py +19 -0
- metaflow/plugins/aws/aws_client.py +6 -0
- metaflow/plugins/aws/aws_utils.py +33 -1
- metaflow/plugins/aws/batch/batch.py +72 -5
- metaflow/plugins/aws/batch/batch_cli.py +24 -3
- metaflow/plugins/aws/batch/batch_decorator.py +57 -6
- metaflow/plugins/aws/step_functions/step_functions.py +28 -3
- metaflow/plugins/aws/step_functions/step_functions_cli.py +49 -4
- metaflow/plugins/aws/step_functions/step_functions_deployer.py +3 -0
- metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +30 -0
- metaflow/plugins/cards/card_cli.py +20 -1
- metaflow/plugins/cards/card_creator.py +24 -1
- metaflow/plugins/cards/card_datastore.py +21 -49
- metaflow/plugins/cards/card_decorator.py +58 -6
- metaflow/plugins/cards/card_modules/basic.py +38 -9
- metaflow/plugins/cards/card_modules/bundle.css +1 -1
- metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
- metaflow/plugins/cards/card_modules/components.py +592 -3
- metaflow/plugins/cards/card_modules/convert_to_native_type.py +34 -5
- metaflow/plugins/cards/card_modules/json_viewer.py +232 -0
- metaflow/plugins/cards/card_modules/main.css +1 -0
- metaflow/plugins/cards/card_modules/main.js +56 -41
- metaflow/plugins/cards/card_modules/test_cards.py +22 -6
- metaflow/plugins/cards/component_serializer.py +1 -8
- metaflow/plugins/cards/metadata.py +22 -0
- metaflow/plugins/catch_decorator.py +9 -0
- metaflow/plugins/datastores/local_storage.py +12 -6
- metaflow/plugins/datastores/spin_storage.py +12 -0
- metaflow/plugins/datatools/s3/s3.py +49 -17
- metaflow/plugins/datatools/s3/s3op.py +113 -66
- metaflow/plugins/env_escape/client_modules.py +102 -72
- metaflow/plugins/events_decorator.py +127 -121
- metaflow/plugins/exit_hook/__init__.py +0 -0
- metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
- metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
- metaflow/plugins/kubernetes/kubernetes.py +12 -1
- metaflow/plugins/kubernetes/kubernetes_cli.py +11 -0
- metaflow/plugins/kubernetes/kubernetes_decorator.py +25 -6
- metaflow/plugins/kubernetes/kubernetes_job.py +12 -4
- metaflow/plugins/kubernetes/kubernetes_jobsets.py +31 -30
- metaflow/plugins/metadata_providers/local.py +76 -82
- metaflow/plugins/metadata_providers/service.py +13 -9
- metaflow/plugins/metadata_providers/spin.py +16 -0
- metaflow/plugins/package_cli.py +36 -24
- metaflow/plugins/parallel_decorator.py +11 -2
- metaflow/plugins/parsers.py +16 -0
- metaflow/plugins/pypi/bootstrap.py +7 -1
- metaflow/plugins/pypi/conda_decorator.py +41 -82
- metaflow/plugins/pypi/conda_environment.py +14 -6
- metaflow/plugins/pypi/micromamba.py +9 -1
- metaflow/plugins/pypi/pip.py +41 -5
- metaflow/plugins/pypi/pypi_decorator.py +4 -4
- metaflow/plugins/pypi/utils.py +22 -0
- metaflow/plugins/secrets/__init__.py +3 -0
- metaflow/plugins/secrets/secrets_decorator.py +14 -178
- metaflow/plugins/secrets/secrets_func.py +49 -0
- metaflow/plugins/secrets/secrets_spec.py +101 -0
- metaflow/plugins/secrets/utils.py +74 -0
- metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
- metaflow/plugins/timeout_decorator.py +0 -1
- metaflow/plugins/uv/bootstrap.py +29 -1
- metaflow/plugins/uv/uv_environment.py +5 -3
- metaflow/pylint_wrapper.py +5 -1
- metaflow/runner/click_api.py +79 -26
- metaflow/runner/deployer.py +208 -6
- metaflow/runner/deployer_impl.py +32 -12
- metaflow/runner/metaflow_runner.py +266 -33
- metaflow/runner/subprocess_manager.py +21 -1
- metaflow/runner/utils.py +27 -16
- metaflow/runtime.py +660 -66
- metaflow/task.py +255 -26
- metaflow/user_configs/config_options.py +33 -21
- metaflow/user_configs/config_parameters.py +220 -58
- metaflow/user_decorators/__init__.py +0 -0
- metaflow/user_decorators/common.py +144 -0
- metaflow/user_decorators/mutable_flow.py +512 -0
- metaflow/user_decorators/mutable_step.py +424 -0
- metaflow/user_decorators/user_flow_decorator.py +264 -0
- metaflow/user_decorators/user_step_decorator.py +749 -0
- metaflow/util.py +197 -7
- metaflow/vendor.py +23 -7
- metaflow/version.py +1 -1
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Makefile +13 -2
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Tiltfile +107 -7
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/pick_services.sh +1 -0
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/METADATA +2 -3
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/RECORD +162 -121
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/WHEEL +1 -1
- metaflow/_vendor/v3_5/__init__.py +0 -1
- metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
- metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
- metaflow/_vendor/v3_5/zipp.py +0 -329
- metaflow/info_file.py +0 -25
- metaflow/package.py +0 -203
- metaflow/user_configs/config_decorators.py +0 -568
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/entry_points.txt +0 -0
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/licenses/LICENSE +0 -0
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/top_level.txt +0 -0
|
@@ -12,6 +12,7 @@ from metaflow.metaflow_config import DATASTORE_LOCAL_DIR
|
|
|
12
12
|
from metaflow.mflog import TASK_LOG_SOURCE
|
|
13
13
|
from metaflow.unbounded_foreach import UBF_CONTROL, UBF_TASK
|
|
14
14
|
from .batch import Batch, BatchKilledException
|
|
15
|
+
from ..aws_utils import validate_aws_tag
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
@click.group()
|
|
@@ -47,7 +48,7 @@ def _execute_cmd(func, flow_name, run_id, user, my_runs, echo):
|
|
|
47
48
|
func(flow_name, run_id, user, echo)
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
@batch.command(help="List unfinished AWS Batch tasks of this flow")
|
|
51
|
+
@batch.command("list", help="List unfinished AWS Batch tasks of this flow")
|
|
51
52
|
@click.option(
|
|
52
53
|
"--my-runs",
|
|
53
54
|
default=False,
|
|
@@ -61,7 +62,7 @@ def _execute_cmd(func, flow_name, run_id, user, my_runs, echo):
|
|
|
61
62
|
help="List unfinished tasks corresponding to the run id.",
|
|
62
63
|
)
|
|
63
64
|
@click.pass_context
|
|
64
|
-
def
|
|
65
|
+
def _list(ctx, run_id, user, my_runs):
|
|
65
66
|
batch = Batch(ctx.obj.metadata, ctx.obj.environment)
|
|
66
67
|
_execute_cmd(
|
|
67
68
|
batch.list_jobs, ctx.obj.flow.name, run_id, user, my_runs, ctx.obj.echo
|
|
@@ -100,6 +101,7 @@ def kill(ctx, run_id, user, my_runs):
|
|
|
100
101
|
"Metaflow."
|
|
101
102
|
)
|
|
102
103
|
@click.argument("step-name")
|
|
104
|
+
@click.argument("code-package-metadata")
|
|
103
105
|
@click.argument("code-package-sha")
|
|
104
106
|
@click.argument("code-package-url")
|
|
105
107
|
@click.option("--executable", help="Executable requirement for AWS Batch.")
|
|
@@ -146,6 +148,13 @@ def kill(ctx, run_id, user, my_runs):
|
|
|
146
148
|
help="Activate designated number of elastic fabric adapter devices. "
|
|
147
149
|
"EFA driver must be installed and instance type compatible with EFA",
|
|
148
150
|
)
|
|
151
|
+
@click.option(
|
|
152
|
+
"--aws-batch-tag",
|
|
153
|
+
"aws_batch_tags",
|
|
154
|
+
multiple=True,
|
|
155
|
+
default=None,
|
|
156
|
+
help="AWS tags. Format: key=value, multiple allowed",
|
|
157
|
+
)
|
|
149
158
|
@click.option("--use-tmpfs", is_flag=True, help="tmpfs requirement for AWS Batch.")
|
|
150
159
|
@click.option("--tmpfs-tempdir", is_flag=True, help="tmpfs requirement for AWS Batch.")
|
|
151
160
|
@click.option("--tmpfs-size", help="tmpfs requirement for AWS Batch.")
|
|
@@ -185,6 +194,7 @@ def kill(ctx, run_id, user, my_runs):
|
|
|
185
194
|
def step(
|
|
186
195
|
ctx,
|
|
187
196
|
step_name,
|
|
197
|
+
code_package_metadata,
|
|
188
198
|
code_package_sha,
|
|
189
199
|
code_package_url,
|
|
190
200
|
executable=None,
|
|
@@ -201,6 +211,7 @@ def step(
|
|
|
201
211
|
swappiness=None,
|
|
202
212
|
inferentia=None,
|
|
203
213
|
efa=None,
|
|
214
|
+
aws_batch_tags=None,
|
|
204
215
|
use_tmpfs=None,
|
|
205
216
|
tmpfs_tempdir=None,
|
|
206
217
|
tmpfs_size=None,
|
|
@@ -211,7 +222,7 @@ def step(
|
|
|
211
222
|
log_driver=None,
|
|
212
223
|
log_options=None,
|
|
213
224
|
num_parallel=None,
|
|
214
|
-
**kwargs
|
|
225
|
+
**kwargs
|
|
215
226
|
):
|
|
216
227
|
def echo(msg, stream="stderr", batch_id=None, **kwargs):
|
|
217
228
|
msg = util.to_unicode(msg)
|
|
@@ -275,6 +286,14 @@ def step(
|
|
|
275
286
|
|
|
276
287
|
env = {"METAFLOW_FLOW_FILENAME": os.path.basename(sys.argv[0])}
|
|
277
288
|
|
|
289
|
+
if aws_batch_tags is not None:
|
|
290
|
+
# We do not need to validate these again,
|
|
291
|
+
# as they come supplied by the batch decorator which already performed validation.
|
|
292
|
+
batch_tags = {}
|
|
293
|
+
for item in list(aws_batch_tags):
|
|
294
|
+
key, value = item.split("=")
|
|
295
|
+
batch_tags[key] = value
|
|
296
|
+
|
|
278
297
|
env_deco = [deco for deco in node.decorators if deco.name == "environment"]
|
|
279
298
|
if env_deco:
|
|
280
299
|
env.update(env_deco[0].attributes["vars"])
|
|
@@ -317,6 +336,7 @@ def step(
|
|
|
317
336
|
step_name,
|
|
318
337
|
step_cli,
|
|
319
338
|
task_spec,
|
|
339
|
+
code_package_metadata,
|
|
320
340
|
code_package_sha,
|
|
321
341
|
code_package_url,
|
|
322
342
|
ctx.obj.flow_datastore.TYPE,
|
|
@@ -338,6 +358,7 @@ def step(
|
|
|
338
358
|
host_volumes=host_volumes,
|
|
339
359
|
efs_volumes=efs_volumes,
|
|
340
360
|
use_tmpfs=use_tmpfs,
|
|
361
|
+
aws_batch_tags=batch_tags,
|
|
341
362
|
tmpfs_tempdir=tmpfs_tempdir,
|
|
342
363
|
tmpfs_size=tmpfs_size,
|
|
343
364
|
tmpfs_path=tmpfs_path,
|
|
@@ -10,10 +10,12 @@ from metaflow.metadata_provider.util import sync_local_metadata_to_datastore
|
|
|
10
10
|
from metaflow.metaflow_config import (
|
|
11
11
|
BATCH_CONTAINER_IMAGE,
|
|
12
12
|
BATCH_CONTAINER_REGISTRY,
|
|
13
|
+
BATCH_DEFAULT_TAGS,
|
|
13
14
|
BATCH_JOB_QUEUE,
|
|
14
15
|
DATASTORE_LOCAL_DIR,
|
|
15
16
|
ECS_FARGATE_EXECUTION_ROLE,
|
|
16
17
|
ECS_S3_ACCESS_IAM_ROLE,
|
|
18
|
+
FEAT_ALWAYS_UPLOAD_CODE_PACKAGE,
|
|
17
19
|
)
|
|
18
20
|
from metaflow.plugins.timeout_decorator import get_run_time_limit_for_task
|
|
19
21
|
from metaflow.sidecar import Sidecar
|
|
@@ -23,6 +25,7 @@ from ..aws_utils import (
|
|
|
23
25
|
compute_resource_attributes,
|
|
24
26
|
get_docker_registry,
|
|
25
27
|
get_ec2_instance_metadata,
|
|
28
|
+
validate_aws_tag,
|
|
26
29
|
)
|
|
27
30
|
from .batch import BatchException
|
|
28
31
|
|
|
@@ -67,6 +70,9 @@ class BatchDecorator(StepDecorator):
|
|
|
67
70
|
A swappiness value of 0 causes swapping not to happen unless absolutely
|
|
68
71
|
necessary. A swappiness value of 100 causes pages to be swapped very
|
|
69
72
|
aggressively. Accepted values are whole numbers between 0 and 100.
|
|
73
|
+
aws_batch_tags: Dict[str, str], optional, default None
|
|
74
|
+
Sets arbitrary AWS tags on the AWS Batch compute environment.
|
|
75
|
+
Set as string key-value pairs.
|
|
70
76
|
use_tmpfs : bool, default False
|
|
71
77
|
This enables an explicit tmpfs mount for this step. Note that tmpfs is
|
|
72
78
|
not available on Fargate compute environments
|
|
@@ -113,6 +119,7 @@ class BatchDecorator(StepDecorator):
|
|
|
113
119
|
"host_volumes": None,
|
|
114
120
|
"efs_volumes": None,
|
|
115
121
|
"use_tmpfs": False,
|
|
122
|
+
"aws_batch_tags": None,
|
|
116
123
|
"tmpfs_tempdir": True,
|
|
117
124
|
"tmpfs_size": None,
|
|
118
125
|
"tmpfs_path": "/metaflow_temp",
|
|
@@ -126,6 +133,7 @@ class BatchDecorator(StepDecorator):
|
|
|
126
133
|
"gpu": "0",
|
|
127
134
|
"memory": "4096",
|
|
128
135
|
}
|
|
136
|
+
package_metadata = None
|
|
129
137
|
package_url = None
|
|
130
138
|
package_sha = None
|
|
131
139
|
run_time_limit = None
|
|
@@ -135,8 +143,6 @@ class BatchDecorator(StepDecorator):
|
|
|
135
143
|
target_platform = "linux-64"
|
|
136
144
|
|
|
137
145
|
def init(self):
|
|
138
|
-
super(BatchDecorator, self).init()
|
|
139
|
-
|
|
140
146
|
# If no docker image is explicitly specified, impute a default image.
|
|
141
147
|
if not self.attributes["image"]:
|
|
142
148
|
# If metaflow-config specifies a docker image, just use that.
|
|
@@ -175,6 +181,29 @@ class BatchDecorator(StepDecorator):
|
|
|
175
181
|
if self.attributes["trainium"] is not None:
|
|
176
182
|
self.attributes["inferentia"] = self.attributes["trainium"]
|
|
177
183
|
|
|
184
|
+
if not isinstance(BATCH_DEFAULT_TAGS, dict) and not all(
|
|
185
|
+
isinstance(k, str) and isinstance(v, str)
|
|
186
|
+
for k, v in BATCH_DEFAULT_TAGS.items()
|
|
187
|
+
):
|
|
188
|
+
raise BatchException(
|
|
189
|
+
"BATCH_DEFAULT_TAGS environment variable must be Dict[str, str]"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if self.attributes["aws_batch_tags"] is not None:
|
|
193
|
+
if not isinstance(self.attributes["aws_batch_tags"], dict) and not all(
|
|
194
|
+
isinstance(k, str) and isinstance(v, str)
|
|
195
|
+
for k, v in self.attributes["aws_batch_tags"].items()
|
|
196
|
+
):
|
|
197
|
+
raise BatchException("aws_batch_tags must be Dict[str, str]")
|
|
198
|
+
else:
|
|
199
|
+
self.attributes["aws_batch_tags"] = {}
|
|
200
|
+
|
|
201
|
+
if BATCH_DEFAULT_TAGS:
|
|
202
|
+
self.attributes["aws_batch_tags"] = {
|
|
203
|
+
**BATCH_DEFAULT_TAGS,
|
|
204
|
+
**self.attributes["aws_batch_tags"],
|
|
205
|
+
}
|
|
206
|
+
|
|
178
207
|
# clean up the alias attribute so it is not passed on.
|
|
179
208
|
self.attributes.pop("trainium", None)
|
|
180
209
|
|
|
@@ -207,6 +236,11 @@ class BatchDecorator(StepDecorator):
|
|
|
207
236
|
if self.attributes["tmpfs_path"] and self.attributes["tmpfs_path"][0] != "/":
|
|
208
237
|
raise BatchException("'tmpfs_path' needs to be an absolute path")
|
|
209
238
|
|
|
239
|
+
# Validate Batch tags
|
|
240
|
+
if self.attributes["aws_batch_tags"]:
|
|
241
|
+
for key, val in self.attributes["aws_batch_tags"].items():
|
|
242
|
+
validate_aws_tag(key, val)
|
|
243
|
+
|
|
210
244
|
def runtime_init(self, flow, graph, package, run_id):
|
|
211
245
|
# Set some more internal state.
|
|
212
246
|
self.flow = flow
|
|
@@ -228,10 +262,20 @@ class BatchDecorator(StepDecorator):
|
|
|
228
262
|
# to execute on AWS Batch anymore. We can execute possible fallback
|
|
229
263
|
# code locally.
|
|
230
264
|
cli_args.commands = ["batch", "step"]
|
|
265
|
+
cli_args.command_args.append(self.package_metadata)
|
|
231
266
|
cli_args.command_args.append(self.package_sha)
|
|
232
267
|
cli_args.command_args.append(self.package_url)
|
|
233
|
-
|
|
268
|
+
# skip certain keys as CLI arguments
|
|
269
|
+
_skip_keys = ["aws_batch_tags"]
|
|
270
|
+
cli_args.command_options.update(
|
|
271
|
+
{k: v for k, v in self.attributes.items() if k not in _skip_keys}
|
|
272
|
+
)
|
|
234
273
|
cli_args.command_options["run-time-limit"] = self.run_time_limit
|
|
274
|
+
|
|
275
|
+
# Pass the supplied AWS batch tags to the step CLI cmd
|
|
276
|
+
cli_args.command_options["aws-batch-tag"] = [
|
|
277
|
+
"%s=%s" % (k, v) for k, v in self.attributes["aws_batch_tags"].items()
|
|
278
|
+
]
|
|
235
279
|
if not R.use_r():
|
|
236
280
|
cli_args.entrypoint[0] = sys.executable
|
|
237
281
|
|
|
@@ -403,9 +447,16 @@ class BatchDecorator(StepDecorator):
|
|
|
403
447
|
@classmethod
|
|
404
448
|
def _save_package_once(cls, flow_datastore, package):
|
|
405
449
|
if cls.package_url is None:
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
450
|
+
if not FEAT_ALWAYS_UPLOAD_CODE_PACKAGE:
|
|
451
|
+
cls.package_url, cls.package_sha = flow_datastore.save_data(
|
|
452
|
+
[package.blob], len_hint=1
|
|
453
|
+
)[0]
|
|
454
|
+
cls.package_metadata = package.package_metadata
|
|
455
|
+
else:
|
|
456
|
+
# Blocks until the package is uploaded
|
|
457
|
+
cls.package_url = package.package_url()
|
|
458
|
+
cls.package_sha = package.package_sha()
|
|
459
|
+
cls.package_metadata = package.package_metadata
|
|
409
460
|
|
|
410
461
|
|
|
411
462
|
def _setup_multinode_environment():
|
|
@@ -40,6 +40,7 @@ class StepFunctions(object):
|
|
|
40
40
|
name,
|
|
41
41
|
graph,
|
|
42
42
|
flow,
|
|
43
|
+
code_package_metadata,
|
|
43
44
|
code_package_sha,
|
|
44
45
|
code_package_url,
|
|
45
46
|
production_token,
|
|
@@ -49,16 +50,19 @@ class StepFunctions(object):
|
|
|
49
50
|
event_logger,
|
|
50
51
|
monitor,
|
|
51
52
|
tags=None,
|
|
53
|
+
aws_batch_tags=None,
|
|
52
54
|
namespace=None,
|
|
53
55
|
username=None,
|
|
54
56
|
max_workers=None,
|
|
55
57
|
workflow_timeout=None,
|
|
56
58
|
is_project=False,
|
|
57
59
|
use_distributed_map=False,
|
|
60
|
+
compress_state_machine=False,
|
|
58
61
|
):
|
|
59
62
|
self.name = name
|
|
60
63
|
self.graph = graph
|
|
61
64
|
self.flow = flow
|
|
65
|
+
self.code_package_metadata = code_package_metadata
|
|
62
66
|
self.code_package_sha = code_package_sha
|
|
63
67
|
self.code_package_url = code_package_url
|
|
64
68
|
self.production_token = production_token
|
|
@@ -68,6 +72,7 @@ class StepFunctions(object):
|
|
|
68
72
|
self.event_logger = event_logger
|
|
69
73
|
self.monitor = monitor
|
|
70
74
|
self.tags = tags
|
|
75
|
+
self.aws_batch_tags = aws_batch_tags or {}
|
|
71
76
|
self.namespace = namespace
|
|
72
77
|
self.username = username
|
|
73
78
|
self.max_workers = max_workers
|
|
@@ -77,6 +82,9 @@ class StepFunctions(object):
|
|
|
77
82
|
# https://aws.amazon.com/blogs/aws/step-functions-distributed-map-a-serverless-solution-for-large-scale-parallel-data-processing/
|
|
78
83
|
self.use_distributed_map = use_distributed_map
|
|
79
84
|
|
|
85
|
+
# S3 command upload configuration
|
|
86
|
+
self.compress_state_machine = compress_state_machine
|
|
87
|
+
|
|
80
88
|
self._client = StepFunctionsClient()
|
|
81
89
|
self._workflow = self._compile()
|
|
82
90
|
self._cron = self._cron()
|
|
@@ -192,6 +200,7 @@ class StepFunctions(object):
|
|
|
192
200
|
"on AWS Step Functions. Please "
|
|
193
201
|
"deploy your flow first." % name
|
|
194
202
|
)
|
|
203
|
+
|
|
195
204
|
# Dump parameters into `Parameters` input field.
|
|
196
205
|
input = json.dumps({"Parameters": json.dumps(parameters)})
|
|
197
206
|
# AWS Step Functions limits input to be 32KiB, but AWS Batch
|
|
@@ -301,6 +310,12 @@ class StepFunctions(object):
|
|
|
301
310
|
"to AWS Step Functions is not supported currently."
|
|
302
311
|
)
|
|
303
312
|
|
|
313
|
+
if self.flow._flow_decorators.get("exit_hook"):
|
|
314
|
+
raise StepFunctionsException(
|
|
315
|
+
"Deploying flows with the @exit_hook decorator "
|
|
316
|
+
"to AWS Step Functions is not currently supported."
|
|
317
|
+
)
|
|
318
|
+
|
|
304
319
|
# Visit every node of the flow and recursively build the state machine.
|
|
305
320
|
def _visit(node, workflow, exit_node=None):
|
|
306
321
|
if node.parallel_foreach:
|
|
@@ -309,6 +324,12 @@ class StepFunctions(object):
|
|
|
309
324
|
"to AWS Step Functions is not supported currently."
|
|
310
325
|
)
|
|
311
326
|
|
|
327
|
+
if node.type == "split-switch":
|
|
328
|
+
raise StepFunctionsException(
|
|
329
|
+
"Deploying flows with switch statement "
|
|
330
|
+
"to AWS Step Functions is not supported currently."
|
|
331
|
+
)
|
|
332
|
+
|
|
312
333
|
# Assign an AWS Batch job to the AWS Step Functions state
|
|
313
334
|
# and pass the intermediate state by exposing `JobId` and
|
|
314
335
|
# `Parameters` to the child job(s) as outputs. `Index` and
|
|
@@ -838,15 +859,17 @@ class StepFunctions(object):
|
|
|
838
859
|
# AWS_BATCH_JOB_ATTEMPT as the job counter.
|
|
839
860
|
"retry_count": "$((AWS_BATCH_JOB_ATTEMPT-1))",
|
|
840
861
|
}
|
|
841
|
-
|
|
862
|
+
# merge batch tags supplied through step-fuctions CLI and ones defined in decorator
|
|
863
|
+
batch_tags = {**self.aws_batch_tags, **resources["aws_batch_tags"]}
|
|
842
864
|
return (
|
|
843
|
-
Batch(self.metadata, self.environment)
|
|
865
|
+
Batch(self.metadata, self.environment, self.flow_datastore)
|
|
844
866
|
.create_job(
|
|
845
867
|
step_name=node.name,
|
|
846
868
|
step_cli=self._step_cli(
|
|
847
869
|
node, input_paths, self.code_package_url, user_code_retries
|
|
848
870
|
),
|
|
849
871
|
task_spec=task_spec,
|
|
872
|
+
code_package_metadata=self.code_package_metadata,
|
|
850
873
|
code_package_sha=self.code_package_sha,
|
|
851
874
|
code_package_url=self.code_package_url,
|
|
852
875
|
code_package_ds=self.flow_datastore.TYPE,
|
|
@@ -863,6 +886,7 @@ class StepFunctions(object):
|
|
|
863
886
|
swappiness=resources["swappiness"],
|
|
864
887
|
efa=resources["efa"],
|
|
865
888
|
use_tmpfs=resources["use_tmpfs"],
|
|
889
|
+
aws_batch_tags=batch_tags,
|
|
866
890
|
tmpfs_tempdir=resources["tmpfs_tempdir"],
|
|
867
891
|
tmpfs_size=resources["tmpfs_size"],
|
|
868
892
|
tmpfs_path=resources["tmpfs_path"],
|
|
@@ -874,6 +898,7 @@ class StepFunctions(object):
|
|
|
874
898
|
ephemeral_storage=resources["ephemeral_storage"],
|
|
875
899
|
log_driver=resources["log_driver"],
|
|
876
900
|
log_options=resources["log_options"],
|
|
901
|
+
offload_command_to_s3=self.compress_state_machine,
|
|
877
902
|
)
|
|
878
903
|
.attempts(total_retries + 1)
|
|
879
904
|
)
|
|
@@ -907,7 +932,7 @@ class StepFunctions(object):
|
|
|
907
932
|
"with": [
|
|
908
933
|
decorator.make_decorator_spec()
|
|
909
934
|
for decorator in node.decorators
|
|
910
|
-
if not decorator.statically_defined
|
|
935
|
+
if not decorator.statically_defined and decorator.inserted_by is None
|
|
911
936
|
]
|
|
912
937
|
}
|
|
913
938
|
# FlowDecorators can define their own top-level options. They are
|
|
@@ -7,8 +7,10 @@ from metaflow import JSONType, current, decorators, parameters
|
|
|
7
7
|
from metaflow._vendor import click
|
|
8
8
|
from metaflow.exception import MetaflowException, MetaflowInternalError
|
|
9
9
|
from metaflow.metaflow_config import (
|
|
10
|
+
FEAT_ALWAYS_UPLOAD_CODE_PACKAGE,
|
|
10
11
|
SERVICE_VERSION_CHECK,
|
|
11
12
|
SFN_STATE_MACHINE_PREFIX,
|
|
13
|
+
SFN_COMPRESS_STATE_MACHINE,
|
|
12
14
|
UI_URL,
|
|
13
15
|
)
|
|
14
16
|
from metaflow.package import MetaflowPackage
|
|
@@ -18,6 +20,8 @@ from metaflow.util import get_username, to_bytes, to_unicode, version_parse
|
|
|
18
20
|
|
|
19
21
|
from .production_token import load_token, new_token, store_token
|
|
20
22
|
from .step_functions import StepFunctions
|
|
23
|
+
from metaflow.tagging_util import validate_tags
|
|
24
|
+
from ..aws_utils import validate_aws_tag
|
|
21
25
|
|
|
22
26
|
VALID_NAME = re.compile(r"[^a-zA-Z0-9_\-\.]")
|
|
23
27
|
|
|
@@ -96,6 +100,13 @@ def step_functions(obj, name=None):
|
|
|
96
100
|
"with the given tag. You can specify this option multiple "
|
|
97
101
|
"times to attach multiple tags.",
|
|
98
102
|
)
|
|
103
|
+
@click.option(
|
|
104
|
+
"--aws-batch-tag",
|
|
105
|
+
"aws_batch_tags",
|
|
106
|
+
multiple=True,
|
|
107
|
+
default=None,
|
|
108
|
+
help="AWS Batch tags.",
|
|
109
|
+
)
|
|
99
110
|
@click.option(
|
|
100
111
|
"--namespace",
|
|
101
112
|
"user_namespace",
|
|
@@ -130,6 +141,12 @@ def step_functions(obj, name=None):
|
|
|
130
141
|
help="Use AWS Step Functions Distributed Map instead of Inline Map for "
|
|
131
142
|
"defining foreach tasks in Amazon State Language.",
|
|
132
143
|
)
|
|
144
|
+
@click.option(
|
|
145
|
+
"--compress-state-machine/--no-compress-state-machine",
|
|
146
|
+
is_flag=True,
|
|
147
|
+
default=SFN_COMPRESS_STATE_MACHINE,
|
|
148
|
+
help="Compress AWS Step Functions state machine to fit within the 8K limit.",
|
|
149
|
+
)
|
|
133
150
|
@click.option(
|
|
134
151
|
"--deployer-attribute-file",
|
|
135
152
|
default=None,
|
|
@@ -142,6 +159,7 @@ def step_functions(obj, name=None):
|
|
|
142
159
|
def create(
|
|
143
160
|
obj,
|
|
144
161
|
tags=None,
|
|
162
|
+
aws_batch_tags=None,
|
|
145
163
|
user_namespace=None,
|
|
146
164
|
only_json=False,
|
|
147
165
|
authorize=None,
|
|
@@ -151,6 +169,7 @@ def create(
|
|
|
151
169
|
workflow_timeout=None,
|
|
152
170
|
log_execution_history=False,
|
|
153
171
|
use_distributed_map=False,
|
|
172
|
+
compress_state_machine=False,
|
|
154
173
|
deployer_attribute_file=None,
|
|
155
174
|
):
|
|
156
175
|
for node in obj.graph:
|
|
@@ -195,11 +214,13 @@ def create(
|
|
|
195
214
|
token,
|
|
196
215
|
obj.state_machine_name,
|
|
197
216
|
tags,
|
|
217
|
+
aws_batch_tags,
|
|
198
218
|
user_namespace,
|
|
199
219
|
max_workers,
|
|
200
220
|
workflow_timeout,
|
|
201
221
|
obj.is_project,
|
|
202
222
|
use_distributed_map,
|
|
223
|
+
compress_state_machine,
|
|
203
224
|
)
|
|
204
225
|
|
|
205
226
|
if only_json:
|
|
@@ -314,11 +335,13 @@ def make_flow(
|
|
|
314
335
|
token,
|
|
315
336
|
name,
|
|
316
337
|
tags,
|
|
338
|
+
aws_batch_tags,
|
|
317
339
|
namespace,
|
|
318
340
|
max_workers,
|
|
319
341
|
workflow_timeout,
|
|
320
342
|
is_project,
|
|
321
343
|
use_distributed_map,
|
|
344
|
+
compress_state_machine=False,
|
|
322
345
|
):
|
|
323
346
|
if obj.flow_datastore.TYPE != "s3":
|
|
324
347
|
raise MetaflowException("AWS Step Functions requires --datastore=s3.")
|
|
@@ -329,18 +352,38 @@ def make_flow(
|
|
|
329
352
|
decorators._init_step_decorators(
|
|
330
353
|
obj.flow, obj.graph, obj.environment, obj.flow_datastore, obj.logger
|
|
331
354
|
)
|
|
355
|
+
obj.graph = obj.flow._graph
|
|
332
356
|
|
|
333
357
|
obj.package = MetaflowPackage(
|
|
334
|
-
obj.flow,
|
|
358
|
+
obj.flow,
|
|
359
|
+
obj.environment,
|
|
360
|
+
obj.echo,
|
|
361
|
+
suffixes=obj.package_suffixes,
|
|
362
|
+
flow_datastore=obj.flow_datastore if FEAT_ALWAYS_UPLOAD_CODE_PACKAGE else None,
|
|
335
363
|
)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
364
|
+
# This blocks until the package is created
|
|
365
|
+
if FEAT_ALWAYS_UPLOAD_CODE_PACKAGE:
|
|
366
|
+
package_url = obj.package.package_url()
|
|
367
|
+
package_sha = obj.package.package_sha()
|
|
368
|
+
else:
|
|
369
|
+
package_url, package_sha = obj.flow_datastore.save_data(
|
|
370
|
+
[obj.package.blob], len_hint=1
|
|
371
|
+
)[0]
|
|
372
|
+
|
|
373
|
+
if aws_batch_tags is not None:
|
|
374
|
+
batch_tags = {}
|
|
375
|
+
for item in list(aws_batch_tags):
|
|
376
|
+
key, value = item.split("=")
|
|
377
|
+
# These are fresh AWS tags provided by the user through the CLI,
|
|
378
|
+
# so we must validate them.
|
|
379
|
+
validate_aws_tag(key, value)
|
|
380
|
+
batch_tags[key] = value
|
|
339
381
|
|
|
340
382
|
return StepFunctions(
|
|
341
383
|
name,
|
|
342
384
|
obj.graph,
|
|
343
385
|
obj.flow,
|
|
386
|
+
obj.package.package_metadata,
|
|
344
387
|
package_sha,
|
|
345
388
|
package_url,
|
|
346
389
|
token,
|
|
@@ -350,12 +393,14 @@ def make_flow(
|
|
|
350
393
|
obj.event_logger,
|
|
351
394
|
obj.monitor,
|
|
352
395
|
tags=tags,
|
|
396
|
+
aws_batch_tags=batch_tags,
|
|
353
397
|
namespace=namespace,
|
|
354
398
|
max_workers=max_workers,
|
|
355
399
|
username=get_username(),
|
|
356
400
|
workflow_timeout=workflow_timeout,
|
|
357
401
|
is_project=is_project,
|
|
358
402
|
use_distributed_map=use_distributed_map,
|
|
403
|
+
compress_state_machine=compress_state_machine,
|
|
359
404
|
)
|
|
360
405
|
|
|
361
406
|
|
|
@@ -76,6 +76,9 @@ class StepFunctionsDeployer(DeployerImpl):
|
|
|
76
76
|
use_distributed_map : bool, optional, default False
|
|
77
77
|
Use AWS Step Functions Distributed Map instead of Inline Map for defining foreach
|
|
78
78
|
tasks in Amazon State Language.
|
|
79
|
+
compress_state_machine : bool, optional, default False
|
|
80
|
+
Compress AWS Step Functions state machine to fit within the 8K limit.
|
|
81
|
+
|
|
79
82
|
deployer_attribute_file : str, optional, default None
|
|
80
83
|
Write the workflow name to the specified file. Used internally for Metaflow's Deployer API.
|
|
81
84
|
|
|
@@ -56,6 +56,20 @@ class StepFunctionsDeployedFlow(DeployedFlow):
|
|
|
56
56
|
|
|
57
57
|
TYPE: ClassVar[Optional[str]] = "step-functions"
|
|
58
58
|
|
|
59
|
+
@classmethod
|
|
60
|
+
def list_deployed_flows(cls, flow_name: Optional[str] = None):
|
|
61
|
+
"""
|
|
62
|
+
This method is not currently implemented for Step Functions.
|
|
63
|
+
|
|
64
|
+
Raises
|
|
65
|
+
------
|
|
66
|
+
NotImplementedError
|
|
67
|
+
This method is not implemented for Step Functions.
|
|
68
|
+
"""
|
|
69
|
+
raise NotImplementedError(
|
|
70
|
+
"list_deployed_flows is not implemented for StepFunctions"
|
|
71
|
+
)
|
|
72
|
+
|
|
59
73
|
@classmethod
|
|
60
74
|
def from_deployment(cls, identifier: str, metadata: Optional[str] = None):
|
|
61
75
|
"""
|
|
@@ -70,6 +84,22 @@ class StepFunctionsDeployedFlow(DeployedFlow):
|
|
|
70
84
|
"from_deployment is not implemented for StepFunctions"
|
|
71
85
|
)
|
|
72
86
|
|
|
87
|
+
@classmethod
|
|
88
|
+
def get_triggered_run(
|
|
89
|
+
cls, identifier: str, run_id: str, metadata: Optional[str] = None
|
|
90
|
+
):
|
|
91
|
+
"""
|
|
92
|
+
This method is not currently implemented for Step Functions.
|
|
93
|
+
|
|
94
|
+
Raises
|
|
95
|
+
------
|
|
96
|
+
NotImplementedError
|
|
97
|
+
This method is not implemented for Step Functions.
|
|
98
|
+
"""
|
|
99
|
+
raise NotImplementedError(
|
|
100
|
+
"get_triggered_run is not implemented for StepFunctions"
|
|
101
|
+
)
|
|
102
|
+
|
|
73
103
|
@property
|
|
74
104
|
def production_token(self: DeployedFlow) -> Optional[str]:
|
|
75
105
|
"""
|
|
@@ -30,7 +30,7 @@ from .exception import (
|
|
|
30
30
|
)
|
|
31
31
|
import traceback
|
|
32
32
|
from collections import namedtuple
|
|
33
|
-
|
|
33
|
+
from .metadata import _save_metadata
|
|
34
34
|
from .card_resolver import resolve_paths_from_task, resumed_info
|
|
35
35
|
|
|
36
36
|
id_func = id
|
|
@@ -613,6 +613,14 @@ def update_card(mf_card, mode, task, data, timeout_value=None):
|
|
|
613
613
|
hidden=True,
|
|
614
614
|
help="Delete data-file and component-file after reading. (internal)",
|
|
615
615
|
)
|
|
616
|
+
@click.option(
|
|
617
|
+
"--save-metadata",
|
|
618
|
+
default=None,
|
|
619
|
+
show_default=True,
|
|
620
|
+
type=JSONTypeClass(),
|
|
621
|
+
hidden=True,
|
|
622
|
+
help="JSON string containing metadata to be saved. (internal)",
|
|
623
|
+
)
|
|
616
624
|
@click.pass_context
|
|
617
625
|
def create(
|
|
618
626
|
ctx,
|
|
@@ -627,6 +635,7 @@ def create(
|
|
|
627
635
|
card_uuid=None,
|
|
628
636
|
delete_input_files=None,
|
|
629
637
|
id=None,
|
|
638
|
+
save_metadata=None,
|
|
630
639
|
):
|
|
631
640
|
card_id = id
|
|
632
641
|
rendered_info = None # Variable holding all the information which will be rendered
|
|
@@ -824,6 +833,16 @@ def create(
|
|
|
824
833
|
% (card_info.type, card_info.hash[:NUM_SHORT_HASH_CHARS]),
|
|
825
834
|
fg="green",
|
|
826
835
|
)
|
|
836
|
+
if save_metadata:
|
|
837
|
+
_save_metadata(
|
|
838
|
+
ctx.obj.metadata,
|
|
839
|
+
task.parent.parent.id,
|
|
840
|
+
task.parent.id,
|
|
841
|
+
task.id,
|
|
842
|
+
task.current_attempt,
|
|
843
|
+
card_uuid,
|
|
844
|
+
save_metadata,
|
|
845
|
+
)
|
|
827
846
|
|
|
828
847
|
|
|
829
848
|
@card.command()
|
|
@@ -5,6 +5,8 @@ import json
|
|
|
5
5
|
import sys
|
|
6
6
|
import os
|
|
7
7
|
from metaflow import current
|
|
8
|
+
from typing import Callable, Tuple, Dict
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
ASYNC_TIMEOUT = 30
|
|
10
12
|
|
|
@@ -44,8 +46,18 @@ class CardProcessManager:
|
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
class CardCreator:
|
|
47
|
-
def __init__(
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
top_level_options,
|
|
52
|
+
should_save_metadata_lambda: Callable[[str], Tuple[bool, Dict]],
|
|
53
|
+
):
|
|
54
|
+
# should_save_metadata_lambda is a lambda that provides a flag to indicate if
|
|
55
|
+
# card metadata should be written to the metadata store.
|
|
56
|
+
# It gets called only once when the card is created inside the subprocess.
|
|
57
|
+
# The intent is that this is a stateful lambda that will ensure that we only end
|
|
58
|
+
# up writing to the metadata store once.
|
|
48
59
|
self._top_level_options = top_level_options
|
|
60
|
+
self._should_save_metadata = should_save_metadata_lambda
|
|
49
61
|
|
|
50
62
|
def create(
|
|
51
63
|
self,
|
|
@@ -62,6 +74,8 @@ class CardCreator:
|
|
|
62
74
|
# Setting `final` will affect the Reload token set during the card refresh
|
|
63
75
|
# data creation along with synchronous execution of subprocess.
|
|
64
76
|
# Setting `sync` will only cause synchronous execution of subprocess.
|
|
77
|
+
save_metadata = False
|
|
78
|
+
metadata_dict = {}
|
|
65
79
|
if mode != "render" and not runtime_card:
|
|
66
80
|
# silently ignore runtime updates for cards that don't support them
|
|
67
81
|
return
|
|
@@ -71,6 +85,8 @@ class CardCreator:
|
|
|
71
85
|
component_strings = []
|
|
72
86
|
else:
|
|
73
87
|
component_strings = current.card._serialize_components(card_uuid)
|
|
88
|
+
# Since the mode is a render, we can check if we need to write to the metadata store.
|
|
89
|
+
save_metadata, metadata_dict = self._should_save_metadata(card_uuid)
|
|
74
90
|
data = current.card._get_latest_data(card_uuid, final=final, mode=mode)
|
|
75
91
|
runspec = "/".join([current.run_id, current.step_name, current.task_id])
|
|
76
92
|
self._run_cards_subprocess(
|
|
@@ -85,6 +101,8 @@ class CardCreator:
|
|
|
85
101
|
data,
|
|
86
102
|
final=final,
|
|
87
103
|
sync=sync,
|
|
104
|
+
save_metadata=save_metadata,
|
|
105
|
+
metadata_dict=metadata_dict,
|
|
88
106
|
)
|
|
89
107
|
|
|
90
108
|
def _run_cards_subprocess(
|
|
@@ -100,6 +118,8 @@ class CardCreator:
|
|
|
100
118
|
data=None,
|
|
101
119
|
final=False,
|
|
102
120
|
sync=False,
|
|
121
|
+
save_metadata=False,
|
|
122
|
+
metadata_dict=None,
|
|
103
123
|
):
|
|
104
124
|
components_file = data_file = None
|
|
105
125
|
wait = final or sync
|
|
@@ -156,6 +176,9 @@ class CardCreator:
|
|
|
156
176
|
if data_file is not None:
|
|
157
177
|
cmd += ["--data-file", data_file.name]
|
|
158
178
|
|
|
179
|
+
if save_metadata:
|
|
180
|
+
cmd += ["--save-metadata", json.dumps(metadata_dict)]
|
|
181
|
+
|
|
159
182
|
response, fail = self._run_command(
|
|
160
183
|
cmd,
|
|
161
184
|
card_uuid,
|