ob-metaflow 2.15.18.1__py2.py3-none-any.whl → 2.16.0.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (93) hide show
  1. metaflow/__init__.py +7 -1
  2. metaflow/_vendor/imghdr/__init__.py +180 -0
  3. metaflow/cli.py +16 -1
  4. metaflow/cli_components/init_cmd.py +1 -0
  5. metaflow/cli_components/run_cmds.py +6 -2
  6. metaflow/client/core.py +22 -30
  7. metaflow/cmd/develop/stub_generator.py +19 -2
  8. metaflow/datastore/task_datastore.py +0 -1
  9. metaflow/debug.py +5 -0
  10. metaflow/decorators.py +230 -70
  11. metaflow/extension_support/__init__.py +15 -8
  12. metaflow/extension_support/_empty_file.py +2 -2
  13. metaflow/flowspec.py +80 -53
  14. metaflow/graph.py +24 -2
  15. metaflow/meta_files.py +13 -0
  16. metaflow/metadata_provider/metadata.py +7 -1
  17. metaflow/metaflow_config.py +5 -0
  18. metaflow/metaflow_environment.py +82 -25
  19. metaflow/metaflow_version.py +1 -1
  20. metaflow/package/__init__.py +664 -0
  21. metaflow/packaging_sys/__init__.py +870 -0
  22. metaflow/packaging_sys/backend.py +113 -0
  23. metaflow/packaging_sys/distribution_support.py +153 -0
  24. metaflow/packaging_sys/tar_backend.py +86 -0
  25. metaflow/packaging_sys/utils.py +91 -0
  26. metaflow/packaging_sys/v1.py +476 -0
  27. metaflow/plugins/__init__.py +3 -0
  28. metaflow/plugins/airflow/airflow.py +11 -1
  29. metaflow/plugins/airflow/airflow_cli.py +15 -4
  30. metaflow/plugins/argo/argo_workflows.py +346 -301
  31. metaflow/plugins/argo/argo_workflows_cli.py +16 -4
  32. metaflow/plugins/argo/exit_hooks.py +209 -0
  33. metaflow/plugins/aws/aws_utils.py +1 -1
  34. metaflow/plugins/aws/batch/batch.py +22 -3
  35. metaflow/plugins/aws/batch/batch_cli.py +3 -0
  36. metaflow/plugins/aws/batch/batch_decorator.py +13 -5
  37. metaflow/plugins/aws/step_functions/step_functions.py +10 -1
  38. metaflow/plugins/aws/step_functions/step_functions_cli.py +15 -4
  39. metaflow/plugins/cards/card_cli.py +20 -1
  40. metaflow/plugins/cards/card_creator.py +24 -1
  41. metaflow/plugins/cards/card_decorator.py +57 -6
  42. metaflow/plugins/cards/card_modules/convert_to_native_type.py +5 -2
  43. metaflow/plugins/cards/card_modules/test_cards.py +16 -0
  44. metaflow/plugins/cards/metadata.py +22 -0
  45. metaflow/plugins/exit_hook/__init__.py +0 -0
  46. metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
  47. metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
  48. metaflow/plugins/kubernetes/kubernetes.py +8 -1
  49. metaflow/plugins/kubernetes/kubernetes_cli.py +3 -0
  50. metaflow/plugins/kubernetes/kubernetes_decorator.py +13 -5
  51. metaflow/plugins/package_cli.py +25 -23
  52. metaflow/plugins/parallel_decorator.py +4 -2
  53. metaflow/plugins/pypi/bootstrap.py +8 -2
  54. metaflow/plugins/pypi/conda_decorator.py +39 -82
  55. metaflow/plugins/pypi/conda_environment.py +6 -2
  56. metaflow/plugins/pypi/pypi_decorator.py +4 -4
  57. metaflow/plugins/secrets/__init__.py +3 -0
  58. metaflow/plugins/secrets/secrets_decorator.py +9 -173
  59. metaflow/plugins/secrets/secrets_func.py +49 -0
  60. metaflow/plugins/secrets/secrets_spec.py +101 -0
  61. metaflow/plugins/secrets/utils.py +74 -0
  62. metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
  63. metaflow/plugins/timeout_decorator.py +0 -1
  64. metaflow/plugins/uv/bootstrap.py +11 -0
  65. metaflow/plugins/uv/uv_environment.py +4 -2
  66. metaflow/pylint_wrapper.py +5 -1
  67. metaflow/runner/click_api.py +5 -4
  68. metaflow/runner/metaflow_runner.py +16 -1
  69. metaflow/runner/subprocess_manager.py +14 -2
  70. metaflow/runtime.py +82 -11
  71. metaflow/task.py +91 -7
  72. metaflow/user_configs/config_options.py +13 -8
  73. metaflow/user_configs/config_parameters.py +0 -4
  74. metaflow/user_decorators/__init__.py +0 -0
  75. metaflow/user_decorators/common.py +144 -0
  76. metaflow/user_decorators/mutable_flow.py +499 -0
  77. metaflow/user_decorators/mutable_step.py +424 -0
  78. metaflow/user_decorators/user_flow_decorator.py +263 -0
  79. metaflow/user_decorators/user_step_decorator.py +712 -0
  80. metaflow/util.py +4 -1
  81. metaflow/version.py +1 -1
  82. {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/Tiltfile +27 -2
  83. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/METADATA +2 -2
  84. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/RECORD +90 -70
  85. metaflow/info_file.py +0 -25
  86. metaflow/package.py +0 -203
  87. metaflow/user_configs/config_decorators.py +0 -568
  88. {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/Makefile +0 -0
  89. {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
  90. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/WHEEL +0 -0
  91. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/entry_points.txt +0 -0
  92. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/licenses/LICENSE +0 -0
  93. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/top_level.txt +0 -0
@@ -15,6 +15,7 @@ from metaflow.exception import (
15
15
  )
16
16
  from metaflow.metaflow_config import (
17
17
  ARGO_WORKFLOWS_UI_URL,
18
+ FEAT_ALWAYS_UPLOAD_CODE_PACKAGE,
18
19
  KUBERNETES_NAMESPACE,
19
20
  SERVICE_VERSION_CHECK,
20
21
  UI_URL,
@@ -518,16 +519,27 @@ def make_flow(
518
519
  # Save the code package in the flow datastore so that both user code and
519
520
  # metaflow package can be retrieved during workflow execution.
520
521
  obj.package = MetaflowPackage(
521
- obj.flow, obj.environment, obj.echo, obj.package_suffixes
522
+ obj.flow,
523
+ obj.environment,
524
+ obj.echo,
525
+ suffixes=obj.package_suffixes,
526
+ flow_datastore=obj.flow_datastore if FEAT_ALWAYS_UPLOAD_CODE_PACKAGE else None,
522
527
  )
523
- package_url, package_sha = obj.flow_datastore.save_data(
524
- [obj.package.blob], len_hint=1
525
- )[0]
528
+
529
+ # This blocks until the package is created
530
+ if FEAT_ALWAYS_UPLOAD_CODE_PACKAGE:
531
+ package_url = obj.package.package_url()
532
+ package_sha = obj.package.package_sha()
533
+ else:
534
+ package_url, package_sha = obj.flow_datastore.save_data(
535
+ [obj.package.blob], len_hint=1
536
+ )[0]
526
537
 
527
538
  return ArgoWorkflows(
528
539
  name,
529
540
  obj.graph,
530
541
  obj.flow,
542
+ obj.package.package_metadata,
531
543
  package_sha,
532
544
  package_url,
533
545
  token,
@@ -0,0 +1,209 @@
1
+ from collections import defaultdict
2
+ import json
3
+ from typing import Dict, List
4
+
5
+
6
+ class JsonSerializable(object):
7
+ def to_json(self):
8
+ return self.payload
9
+
10
+ def __str__(self):
11
+ return json.dumps(self.payload, indent=4)
12
+
13
+
14
+ class _LifecycleHook(JsonSerializable):
15
+ # https://argoproj.github.io/argo-workflows/fields/#lifecyclehook
16
+
17
+ def __init__(self, name):
18
+ tree = lambda: defaultdict(tree)
19
+ self.name = name
20
+ self.payload = tree()
21
+
22
+ def expression(self, expression):
23
+ self.payload["expression"] = str(expression)
24
+ return self
25
+
26
+ def template(self, template):
27
+ self.payload["template"] = template
28
+ return self
29
+
30
+
31
+ class _Template(JsonSerializable):
32
+ # https://argoproj.github.io/argo-workflows/fields/#template
33
+
34
+ def __init__(self, name):
35
+ tree = lambda: defaultdict(tree)
36
+ self.name = name
37
+ self.payload = tree()
38
+ self.payload["name"] = name
39
+
40
+ def http(self, http):
41
+ self.payload["http"] = http.to_json()
42
+ return self
43
+
44
+ def script(self, script):
45
+ self.payload["script"] = script.to_json()
46
+ return self
47
+
48
+ def container(self, container):
49
+ self.payload["container"] = container
50
+ return self
51
+
52
+ def service_account_name(self, service_account_name):
53
+ self.payload["serviceAccountName"] = service_account_name
54
+ return self
55
+
56
+
57
+ class Hook(object):
58
+ """
59
+ Abstraction for Argo Workflows exit hooks.
60
+ A hook consists of a Template, and one or more LifecycleHooks that trigger the template
61
+ """
62
+
63
+ template: "_Template"
64
+ lifecycle_hooks: List["_LifecycleHook"]
65
+
66
+
67
+ class _HttpSpec(JsonSerializable):
68
+ # https://argoproj.github.io/argo-workflows/fields/#http
69
+
70
+ def __init__(self, method):
71
+ tree = lambda: defaultdict(tree)
72
+ self.payload = tree()
73
+ self.payload["method"] = method
74
+ self.payload["headers"] = []
75
+
76
+ def header(self, header, value):
77
+ self.payload["headers"].append({"name": header, "value": value})
78
+ return self
79
+
80
+ def body(self, body):
81
+ self.payload["body"] = str(body)
82
+ return self
83
+
84
+ def url(self, url):
85
+ self.payload["url"] = url
86
+ return self
87
+
88
+ def success_condition(self, success_condition):
89
+ self.payload["successCondition"] = success_condition
90
+ return self
91
+
92
+
93
+ # HTTP hook
94
+ class HttpExitHook(Hook):
95
+ def __init__(
96
+ self,
97
+ name,
98
+ url,
99
+ method="GET",
100
+ headers=None,
101
+ body=None,
102
+ on_success=False,
103
+ on_error=False,
104
+ ):
105
+ self.template = _Template(name)
106
+ http = _HttpSpec(method).url(url)
107
+ if headers is not None:
108
+ for header, value in headers.items():
109
+ http.header(header, value)
110
+
111
+ if body is not None:
112
+ http.body(json.dumps(body))
113
+
114
+ self.template.http(http)
115
+
116
+ self.lifecycle_hooks = []
117
+
118
+ if on_success and on_error:
119
+ raise Exception("Set only one of the on_success/on_error at a time.")
120
+
121
+ if on_success:
122
+ self.lifecycle_hooks.append(
123
+ _LifecycleHook(name)
124
+ .expression("workflow.status == 'Succeeded'")
125
+ .template(self.template.name)
126
+ )
127
+
128
+ if on_error:
129
+ self.lifecycle_hooks.append(
130
+ _LifecycleHook(name)
131
+ .expression("workflow.status == 'Error' || workflow.status == 'Failed'")
132
+ .template(self.template.name)
133
+ )
134
+
135
+ if not on_success and not on_error:
136
+ # add an expressionless lifecycle hook
137
+ self.lifecycle_hooks.append(_LifecycleHook(name).template(name))
138
+
139
+
140
+ class ExitHookHack(Hook):
141
+ # Warning: terrible hack to workaround a bug in Argo Workflow where the
142
+ # templates listed above do not execute unless there is an
143
+ # explicit exit hook. as and when this bug is patched, we should
144
+ # remove this effectively no-op template.
145
+ # Note: We use the Http template because changing this to an actual no-op container had the side-effect of
146
+ # leaving LifecycleHooks in a pending state even when they have finished execution.
147
+ def __init__(
148
+ self,
149
+ url,
150
+ headers=None,
151
+ body=None,
152
+ ):
153
+ self.template = _Template("exit-hook-hack")
154
+ http = _HttpSpec("GET").url(url)
155
+ if headers is not None:
156
+ for header, value in headers.items():
157
+ http.header(header, value)
158
+
159
+ if body is not None:
160
+ http.body(json.dumps(body))
161
+
162
+ http.success_condition("true == true")
163
+
164
+ self.template.http(http)
165
+
166
+ self.lifecycle_hooks = []
167
+
168
+ # add an expressionless lifecycle hook
169
+ self.lifecycle_hooks.append(_LifecycleHook("exit").template("exit-hook-hack"))
170
+
171
+
172
+ class ContainerHook(Hook):
173
+ def __init__(
174
+ self,
175
+ name: str,
176
+ container: Dict,
177
+ service_account_name: str = None,
178
+ on_success: bool = False,
179
+ on_error: bool = False,
180
+ ):
181
+ self.template = _Template(name)
182
+
183
+ if service_account_name is not None:
184
+ self.template.service_account_name(service_account_name)
185
+
186
+ self.template.container(container)
187
+
188
+ self.lifecycle_hooks = []
189
+
190
+ if on_success and on_error:
191
+ raise Exception("Set only one of the on_success/on_error at a time.")
192
+
193
+ if on_success:
194
+ self.lifecycle_hooks.append(
195
+ _LifecycleHook(name)
196
+ .expression("workflow.status == 'Succeeded'")
197
+ .template(self.template.name)
198
+ )
199
+
200
+ if on_error:
201
+ self.lifecycle_hooks.append(
202
+ _LifecycleHook(name)
203
+ .expression("workflow.status == 'Error' || workflow.status == 'Failed'")
204
+ .template(self.template.name)
205
+ )
206
+
207
+ if not on_success and not on_error:
208
+ # add an expressionless lifecycle hook
209
+ self.lifecycle_hooks.append(_LifecycleHook(name).template(name))
@@ -49,7 +49,7 @@ def get_ec2_instance_metadata():
49
49
  # Try to get an IMDSv2 token.
50
50
  token = requests.put(
51
51
  url="http://169.254.169.254/latest/api/token",
52
- headers={"X-aws-ec2-metadata-token-ttl-seconds": 100},
52
+ headers={"X-aws-ec2-metadata-token-ttl-seconds": "100"},
53
53
  timeout=timeout,
54
54
  ).text
55
55
  except:
@@ -59,14 +59,24 @@ class Batch(object):
59
59
  self._client = BatchClient()
60
60
  atexit.register(lambda: self.job.kill() if hasattr(self, "job") else None)
61
61
 
62
- def _command(self, environment, code_package_url, step_name, step_cmds, task_spec):
62
+ def _command(
63
+ self,
64
+ environment,
65
+ code_package_metadata,
66
+ code_package_url,
67
+ step_name,
68
+ step_cmds,
69
+ task_spec,
70
+ ):
63
71
  mflog_expr = export_mflog_env_vars(
64
72
  datastore_type="s3",
65
73
  stdout_path=STDOUT_PATH,
66
74
  stderr_path=STDERR_PATH,
67
75
  **task_spec
68
76
  )
69
- init_cmds = environment.get_package_commands(code_package_url, "s3")
77
+ init_cmds = environment.get_package_commands(
78
+ code_package_url, "s3", code_package_metadata
79
+ )
70
80
  init_expr = " && ".join(init_cmds)
71
81
  step_expr = bash_capture_logs(
72
82
  " && ".join(environment.bootstrap_commands(step_name, "s3") + step_cmds)
@@ -167,6 +177,7 @@ class Batch(object):
167
177
  step_name,
168
178
  step_cli,
169
179
  task_spec,
180
+ code_package_metadata,
170
181
  code_package_sha,
171
182
  code_package_url,
172
183
  code_package_ds,
@@ -210,7 +221,12 @@ class Batch(object):
210
221
  .job_queue(queue)
211
222
  .command(
212
223
  self._command(
213
- self.environment, code_package_url, step_name, [step_cli], task_spec
224
+ self.environment,
225
+ code_package_metadata,
226
+ code_package_url,
227
+ step_name,
228
+ [step_cli],
229
+ task_spec,
214
230
  )
215
231
  )
216
232
  .image(image)
@@ -249,6 +265,7 @@ class Batch(object):
249
265
  )
250
266
  .task_id(attrs.get("metaflow.task_id"))
251
267
  .environment_variable("AWS_DEFAULT_REGION", self._client.region())
268
+ .environment_variable("METAFLOW_CODE_METADATA", code_package_metadata)
252
269
  .environment_variable("METAFLOW_CODE_SHA", code_package_sha)
253
270
  .environment_variable("METAFLOW_CODE_URL", code_package_url)
254
271
  .environment_variable("METAFLOW_CODE_DS", code_package_ds)
@@ -334,6 +351,7 @@ class Batch(object):
334
351
  step_name,
335
352
  step_cli,
336
353
  task_spec,
354
+ code_package_metadata,
337
355
  code_package_sha,
338
356
  code_package_url,
339
357
  code_package_ds,
@@ -374,6 +392,7 @@ class Batch(object):
374
392
  step_name,
375
393
  step_cli,
376
394
  task_spec,
395
+ code_package_metadata,
377
396
  code_package_sha,
378
397
  code_package_url,
379
398
  code_package_ds,
@@ -100,6 +100,7 @@ def kill(ctx, run_id, user, my_runs):
100
100
  "Metaflow."
101
101
  )
102
102
  @click.argument("step-name")
103
+ @click.argument("code-package-metadata")
103
104
  @click.argument("code-package-sha")
104
105
  @click.argument("code-package-url")
105
106
  @click.option("--executable", help="Executable requirement for AWS Batch.")
@@ -185,6 +186,7 @@ def kill(ctx, run_id, user, my_runs):
185
186
  def step(
186
187
  ctx,
187
188
  step_name,
189
+ code_package_metadata,
188
190
  code_package_sha,
189
191
  code_package_url,
190
192
  executable=None,
@@ -317,6 +319,7 @@ def step(
317
319
  step_name,
318
320
  step_cli,
319
321
  task_spec,
322
+ code_package_metadata,
320
323
  code_package_sha,
321
324
  code_package_url,
322
325
  ctx.obj.flow_datastore.TYPE,
@@ -14,6 +14,7 @@ from metaflow.metaflow_config import (
14
14
  DATASTORE_LOCAL_DIR,
15
15
  ECS_FARGATE_EXECUTION_ROLE,
16
16
  ECS_S3_ACCESS_IAM_ROLE,
17
+ FEAT_ALWAYS_UPLOAD_CODE_PACKAGE,
17
18
  )
18
19
  from metaflow.plugins.timeout_decorator import get_run_time_limit_for_task
19
20
  from metaflow.sidecar import Sidecar
@@ -126,6 +127,7 @@ class BatchDecorator(StepDecorator):
126
127
  "gpu": "0",
127
128
  "memory": "4096",
128
129
  }
130
+ package_metadata = None
129
131
  package_url = None
130
132
  package_sha = None
131
133
  run_time_limit = None
@@ -135,8 +137,6 @@ class BatchDecorator(StepDecorator):
135
137
  target_platform = "linux-64"
136
138
 
137
139
  def init(self):
138
- super(BatchDecorator, self).init()
139
-
140
140
  # If no docker image is explicitly specified, impute a default image.
141
141
  if not self.attributes["image"]:
142
142
  # If metaflow-config specifies a docker image, just use that.
@@ -228,6 +228,7 @@ class BatchDecorator(StepDecorator):
228
228
  # to execute on AWS Batch anymore. We can execute possible fallback
229
229
  # code locally.
230
230
  cli_args.commands = ["batch", "step"]
231
+ cli_args.command_args.append(self.package_metadata)
231
232
  cli_args.command_args.append(self.package_sha)
232
233
  cli_args.command_args.append(self.package_url)
233
234
  cli_args.command_options.update(self.attributes)
@@ -403,9 +404,16 @@ class BatchDecorator(StepDecorator):
403
404
  @classmethod
404
405
  def _save_package_once(cls, flow_datastore, package):
405
406
  if cls.package_url is None:
406
- cls.package_url, cls.package_sha = flow_datastore.save_data(
407
- [package.blob], len_hint=1
408
- )[0]
407
+ if not FEAT_ALWAYS_UPLOAD_CODE_PACKAGE:
408
+ cls.package_url, cls.package_sha = flow_datastore.save_data(
409
+ [package.blob], len_hint=1
410
+ )[0]
411
+ cls.package_metadata = package.package_metadata
412
+ else:
413
+ # Blocks until the package is uploaded
414
+ cls.package_url = package.package_url()
415
+ cls.package_sha = package.package_sha()
416
+ cls.package_metadata = package.package_metadata
409
417
 
410
418
 
411
419
  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,
@@ -59,6 +60,7 @@ class StepFunctions(object):
59
60
  self.name = name
60
61
  self.graph = graph
61
62
  self.flow = flow
63
+ self.code_package_metadata = code_package_metadata
62
64
  self.code_package_sha = code_package_sha
63
65
  self.code_package_url = code_package_url
64
66
  self.production_token = production_token
@@ -301,6 +303,12 @@ class StepFunctions(object):
301
303
  "to AWS Step Functions is not supported currently."
302
304
  )
303
305
 
306
+ if self.flow._flow_decorators.get("exit_hook"):
307
+ raise StepFunctionsException(
308
+ "Deploying flows with the @exit_hook decorator "
309
+ "to AWS Step Functions is not currently supported."
310
+ )
311
+
304
312
  # Visit every node of the flow and recursively build the state machine.
305
313
  def _visit(node, workflow, exit_node=None):
306
314
  if node.parallel_foreach:
@@ -847,6 +855,7 @@ class StepFunctions(object):
847
855
  node, input_paths, self.code_package_url, user_code_retries
848
856
  ),
849
857
  task_spec=task_spec,
858
+ code_package_metadata=self.code_package_metadata,
850
859
  code_package_sha=self.code_package_sha,
851
860
  code_package_url=self.code_package_url,
852
861
  code_package_ds=self.flow_datastore.TYPE,
@@ -907,7 +916,7 @@ class StepFunctions(object):
907
916
  "with": [
908
917
  decorator.make_decorator_spec()
909
918
  for decorator in node.decorators
910
- if not decorator.statically_defined
919
+ if not decorator.statically_defined and decorator.inserted_by is None
911
920
  ]
912
921
  }
913
922
  # FlowDecorators can define their own top-level options. They are
@@ -7,6 +7,7 @@ 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,
12
13
  UI_URL,
@@ -331,16 +332,26 @@ def make_flow(
331
332
  )
332
333
 
333
334
  obj.package = MetaflowPackage(
334
- obj.flow, obj.environment, obj.echo, obj.package_suffixes
335
+ obj.flow,
336
+ obj.environment,
337
+ obj.echo,
338
+ suffixes=obj.package_suffixes,
339
+ flow_datastore=obj.flow_datastore if FEAT_ALWAYS_UPLOAD_CODE_PACKAGE else None,
335
340
  )
336
- package_url, package_sha = obj.flow_datastore.save_data(
337
- [obj.package.blob], len_hint=1
338
- )[0]
341
+ # This blocks until the package is created
342
+ if FEAT_ALWAYS_UPLOAD_CODE_PACKAGE:
343
+ package_url = obj.package.package_url()
344
+ package_sha = obj.package.package_sha()
345
+ else:
346
+ package_url, package_sha = obj.flow_datastore.save_data(
347
+ [obj.package.blob], len_hint=1
348
+ )[0]
339
349
 
340
350
  return StepFunctions(
341
351
  name,
342
352
  obj.graph,
343
353
  obj.flow,
354
+ obj.package.package_metadata,
344
355
  package_sha,
345
356
  package_url,
346
357
  token,
@@ -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__(self, top_level_options):
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,