ob-metaflow 2.12.36.1__py2.py3-none-any.whl → 2.12.36.2__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 (56) hide show
  1. metaflow/__init__.py +3 -0
  2. metaflow/cli.py +84 -697
  3. metaflow/cli_args.py +17 -0
  4. metaflow/cli_components/__init__.py +0 -0
  5. metaflow/cli_components/dump_cmd.py +96 -0
  6. metaflow/cli_components/init_cmd.py +51 -0
  7. metaflow/cli_components/run_cmds.py +358 -0
  8. metaflow/cli_components/step_cmd.py +189 -0
  9. metaflow/cli_components/utils.py +140 -0
  10. metaflow/cmd/develop/stub_generator.py +9 -2
  11. metaflow/decorators.py +63 -2
  12. metaflow/extension_support/plugins.py +41 -27
  13. metaflow/flowspec.py +156 -16
  14. metaflow/includefile.py +50 -22
  15. metaflow/metaflow_config.py +1 -1
  16. metaflow/package.py +17 -3
  17. metaflow/parameters.py +80 -23
  18. metaflow/plugins/__init__.py +4 -0
  19. metaflow/plugins/airflow/airflow_cli.py +1 -0
  20. metaflow/plugins/argo/argo_workflows.py +44 -4
  21. metaflow/plugins/argo/argo_workflows_cli.py +1 -0
  22. metaflow/plugins/argo/argo_workflows_deployer_objects.py +5 -1
  23. metaflow/plugins/aws/batch/batch_decorator.py +2 -2
  24. metaflow/plugins/aws/step_functions/step_functions.py +32 -0
  25. metaflow/plugins/aws/step_functions/step_functions_cli.py +1 -0
  26. metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +3 -0
  27. metaflow/plugins/datatools/s3/s3op.py +3 -3
  28. metaflow/plugins/kubernetes/kubernetes.py +3 -3
  29. metaflow/plugins/kubernetes/kubernetes_cli.py +1 -1
  30. metaflow/plugins/kubernetes/kubernetes_decorator.py +2 -2
  31. metaflow/plugins/kubernetes/kubernetes_job.py +3 -3
  32. metaflow/plugins/pypi/conda_decorator.py +20 -10
  33. metaflow/plugins/pypi/pypi_decorator.py +11 -9
  34. metaflow/plugins/timeout_decorator.py +2 -2
  35. metaflow/runner/click_api.py +73 -19
  36. metaflow/runner/deployer.py +1 -1
  37. metaflow/runner/deployer_impl.py +2 -2
  38. metaflow/runner/metaflow_runner.py +4 -1
  39. metaflow/runner/nbdeploy.py +2 -0
  40. metaflow/runner/nbrun.py +1 -1
  41. metaflow/runner/subprocess_manager.py +3 -1
  42. metaflow/runner/utils.py +37 -20
  43. metaflow/runtime.py +111 -73
  44. metaflow/sidecar/sidecar_worker.py +1 -1
  45. metaflow/user_configs/__init__.py +0 -0
  46. metaflow/user_configs/config_decorators.py +563 -0
  47. metaflow/user_configs/config_options.py +495 -0
  48. metaflow/user_configs/config_parameters.py +386 -0
  49. metaflow/util.py +17 -0
  50. metaflow/version.py +1 -1
  51. {ob_metaflow-2.12.36.1.dist-info → ob_metaflow-2.12.36.2.dist-info}/METADATA +3 -2
  52. {ob_metaflow-2.12.36.1.dist-info → ob_metaflow-2.12.36.2.dist-info}/RECORD +56 -46
  53. {ob_metaflow-2.12.36.1.dist-info → ob_metaflow-2.12.36.2.dist-info}/LICENSE +0 -0
  54. {ob_metaflow-2.12.36.1.dist-info → ob_metaflow-2.12.36.2.dist-info}/WHEEL +0 -0
  55. {ob_metaflow-2.12.36.1.dist-info → ob_metaflow-2.12.36.2.dist-info}/entry_points.txt +0 -0
  56. {ob_metaflow-2.12.36.1.dist-info → ob_metaflow-2.12.36.2.dist-info}/top_level.txt +0 -0
@@ -61,6 +61,7 @@ from metaflow.plugins.kubernetes.kubernetes import (
61
61
  )
62
62
  from metaflow.plugins.kubernetes.kubernetes_jobsets import KubernetesArgoJobSet
63
63
  from metaflow.unbounded_foreach import UBF_CONTROL, UBF_TASK
64
+ from metaflow.user_configs.config_options import ConfigInput
64
65
  from metaflow.util import (
65
66
  compress_list,
66
67
  dict_to_cli_options,
@@ -169,6 +170,7 @@ class ArgoWorkflows(object):
169
170
  self.enable_heartbeat_daemon = enable_heartbeat_daemon
170
171
  self.enable_error_msg_capture = enable_error_msg_capture
171
172
  self.parameters = self._process_parameters()
173
+ self.config_parameters = self._process_config_parameters()
172
174
  self.triggers, self.trigger_options = self._process_triggers()
173
175
  self._schedule, self._timezone = self._get_schedule()
174
176
 
@@ -456,6 +458,10 @@ class ArgoWorkflows(object):
456
458
  "case-insensitive." % param.name
457
459
  )
458
460
  seen.add(norm)
461
+ # NOTE: We skip config parameters as these do not have dynamic values,
462
+ # and need to be treated differently.
463
+ if param.IS_CONFIG_PARAMETER:
464
+ continue
459
465
 
460
466
  extra_attrs = {}
461
467
  if param.kwargs.get("type") == JSONType:
@@ -489,6 +495,7 @@ class ArgoWorkflows(object):
489
495
  # execution - which needs to be fixed imminently.
490
496
  if not is_required or default_value is not None:
491
497
  default_value = json.dumps(default_value)
498
+
492
499
  parameters[param.name] = dict(
493
500
  name=param.name,
494
501
  value=default_value,
@@ -499,6 +506,27 @@ class ArgoWorkflows(object):
499
506
  )
500
507
  return parameters
501
508
 
509
+ def _process_config_parameters(self):
510
+ parameters = []
511
+ seen = set()
512
+ for var, param in self.flow._get_parameters():
513
+ if not param.IS_CONFIG_PARAMETER:
514
+ continue
515
+ # Throw an exception if the parameter is specified twice.
516
+ norm = param.name.lower()
517
+ if norm in seen:
518
+ raise MetaflowException(
519
+ "Parameter *%s* is specified twice. "
520
+ "Note that parameter names are "
521
+ "case-insensitive." % param.name
522
+ )
523
+ seen.add(norm)
524
+
525
+ parameters.append(
526
+ dict(name=param.name, kv_name=ConfigInput.make_key_name(param.name))
527
+ )
528
+ return parameters
529
+
502
530
  def _process_triggers(self):
503
531
  # Impute triggers for Argo Workflow Template specified through @trigger and
504
532
  # @trigger_on_finish decorators
@@ -521,8 +549,13 @@ class ArgoWorkflows(object):
521
549
  # convert them to lower case since Metaflow parameters are case
522
550
  # insensitive.
523
551
  seen = set()
552
+ # NOTE: We skip config parameters as their values can not be set through event payloads
524
553
  params = set(
525
- [param.name.lower() for var, param in self.flow._get_parameters()]
554
+ [
555
+ param.name.lower()
556
+ for var, param in self.flow._get_parameters()
557
+ if not param.IS_CONFIG_PARAMETER
558
+ ]
526
559
  )
527
560
  trigger_deco = self.flow._flow_decorators.get("trigger")[0]
528
561
  trigger_deco.format_deploytime_value()
@@ -1721,6 +1754,13 @@ class ArgoWorkflows(object):
1721
1754
  metaflow_version["production_token"] = self.production_token
1722
1755
  env["METAFLOW_VERSION"] = json.dumps(metaflow_version)
1723
1756
 
1757
+ # map config values
1758
+ cfg_env = {
1759
+ param["name"]: param["kv_name"] for param in self.config_parameters
1760
+ }
1761
+ if cfg_env:
1762
+ env["METAFLOW_FLOW_CONFIG_VALUE"] = json.dumps(cfg_env)
1763
+
1724
1764
  # Set the template inputs and outputs for passing state. Very simply,
1725
1765
  # the container template takes in input-paths as input and outputs
1726
1766
  # the task-id (which feeds in as input-paths to the subsequent task).
@@ -1829,7 +1869,7 @@ class ArgoWorkflows(object):
1829
1869
 
1830
1870
  # get initial configs
1831
1871
  initial_configs = init_config()
1832
- for entry in ["OBP_PERIMETER", "OBP_INTEGRATIONS_SECRETS_METADATA_URL"]:
1872
+ for entry in ["OBP_PERIMETER", "OBP_INTEGRATIONS_URL"]:
1833
1873
  if entry not in initial_configs:
1834
1874
  raise ArgoWorkflowsException(
1835
1875
  f"{entry} was not found in metaflow config. Please make sure to run `outerbounds configure <...>` command which can be found on the Ourebounds UI or reach out to your Outerbounds support team."
@@ -1837,8 +1877,8 @@ class ArgoWorkflows(object):
1837
1877
 
1838
1878
  additional_obp_configs = {
1839
1879
  "OBP_PERIMETER": initial_configs["OBP_PERIMETER"],
1840
- "OBP_INTEGRATIONS_SECRETS_METADATA_URL": initial_configs[
1841
- "OBP_INTEGRATIONS_SECRETS_METADATA_URL"
1880
+ "OBP_INTEGRATIONS_URL": initial_configs[
1881
+ "OBP_INTEGRATIONS_URL"
1842
1882
  ],
1843
1883
  }
1844
1884
 
@@ -470,6 +470,7 @@ def make_flow(
470
470
  decorators._attach_decorators(
471
471
  obj.flow, [KubernetesDecorator.name, EnvironmentDecorator.name]
472
472
  )
473
+ decorators._init(obj.flow)
473
474
 
474
475
  decorators._init_step_decorators(
475
476
  obj.flow, obj.graph, obj.environment, obj.flow_datastore, obj.logger
@@ -97,6 +97,7 @@ class ArgoWorkflowsTriggeredRun(TriggeredRun):
97
97
  )
98
98
 
99
99
  command_obj = self.deployer.spm.get(pid)
100
+ command_obj.sync_wait()
100
101
  return command_obj.process.returncode == 0
101
102
 
102
103
  def unsuspend(self, **kwargs) -> bool:
@@ -131,6 +132,7 @@ class ArgoWorkflowsTriggeredRun(TriggeredRun):
131
132
  )
132
133
 
133
134
  command_obj = self.deployer.spm.get(pid)
135
+ command_obj.sync_wait()
134
136
  return command_obj.process.returncode == 0
135
137
 
136
138
  def terminate(self, **kwargs) -> bool:
@@ -165,6 +167,7 @@ class ArgoWorkflowsTriggeredRun(TriggeredRun):
165
167
  )
166
168
 
167
169
  command_obj = self.deployer.spm.get(pid)
170
+ command_obj.sync_wait()
168
171
  return command_obj.process.returncode == 0
169
172
 
170
173
  @property
@@ -319,6 +322,7 @@ class ArgoWorkflowsDeployedFlow(DeployedFlow):
319
322
  )
320
323
 
321
324
  command_obj = self.deployer.spm.get(pid)
325
+ command_obj.sync_wait()
322
326
  return command_obj.process.returncode == 0
323
327
 
324
328
  def trigger(self, **kwargs) -> ArgoWorkflowsTriggeredRun:
@@ -361,7 +365,7 @@ class ArgoWorkflowsDeployedFlow(DeployedFlow):
361
365
  content = handle_timeout(
362
366
  attribute_file_fd, command_obj, self.deployer.file_read_timeout
363
367
  )
364
-
368
+ command_obj.sync_wait()
365
369
  if command_obj.process.returncode == 0:
366
370
  return ArgoWorkflowsTriggeredRun(
367
371
  deployer=self.deployer, content=content
@@ -138,8 +138,8 @@ class BatchDecorator(StepDecorator):
138
138
  supports_conda_environment = True
139
139
  target_platform = "linux-64"
140
140
 
141
- def __init__(self, attributes=None, statically_defined=False):
142
- super(BatchDecorator, self).__init__(attributes, statically_defined)
141
+ def init(self):
142
+ super(BatchDecorator, self).init()
143
143
 
144
144
  # If no docker image is explicitly specified, impute a default image.
145
145
  if not self.attributes["image"]:
@@ -18,6 +18,7 @@ from metaflow.metaflow_config import (
18
18
  SFN_S3_DISTRIBUTED_MAP_OUTPUT_PATH,
19
19
  )
20
20
  from metaflow.parameters import deploy_time_eval
21
+ from metaflow.user_configs.config_options import ConfigInput
21
22
  from metaflow.util import dict_to_cli_options, to_pascalcase
22
23
 
23
24
  from ..batch.batch import Batch
@@ -71,6 +72,7 @@ class StepFunctions(object):
71
72
  self.username = username
72
73
  self.max_workers = max_workers
73
74
  self.workflow_timeout = workflow_timeout
75
+ self.config_parameters = self._process_config_parameters()
74
76
 
75
77
  # https://aws.amazon.com/blogs/aws/step-functions-distributed-map-a-serverless-solution-for-large-scale-parallel-data-processing/
76
78
  self.use_distributed_map = use_distributed_map
@@ -485,6 +487,10 @@ class StepFunctions(object):
485
487
  "case-insensitive." % param.name
486
488
  )
487
489
  seen.add(norm)
490
+ # NOTE: We skip config parameters as these do not have dynamic values,
491
+ # and need to be treated differently.
492
+ if param.IS_CONFIG_PARAMETER:
493
+ continue
488
494
 
489
495
  is_required = param.kwargs.get("required", False)
490
496
  # Throw an exception if a schedule is set for a flow with required
@@ -501,6 +507,27 @@ class StepFunctions(object):
501
507
  parameters.append(dict(name=param.name, value=value))
502
508
  return parameters
503
509
 
510
+ def _process_config_parameters(self):
511
+ parameters = []
512
+ seen = set()
513
+ for var, param in self.flow._get_parameters():
514
+ if not param.IS_CONFIG_PARAMETER:
515
+ continue
516
+ # Throw an exception if the parameter is specified twice.
517
+ norm = param.name.lower()
518
+ if norm in seen:
519
+ raise MetaflowException(
520
+ "Parameter *%s* is specified twice. "
521
+ "Note that parameter names are "
522
+ "case-insensitive." % param.name
523
+ )
524
+ seen.add(norm)
525
+
526
+ parameters.append(
527
+ dict(name=param.name, kv_name=ConfigInput.make_key_name(param.name))
528
+ )
529
+ return parameters
530
+
504
531
  def _batch(self, node):
505
532
  attrs = {
506
533
  # metaflow.user is only used for setting the AWS Job Name.
@@ -747,6 +774,11 @@ class StepFunctions(object):
747
774
  metaflow_version["production_token"] = self.production_token
748
775
  env["METAFLOW_VERSION"] = json.dumps(metaflow_version)
749
776
 
777
+ # map config values
778
+ cfg_env = {param["name"]: param["kv_name"] for param in self.config_parameters}
779
+ if cfg_env:
780
+ env["METAFLOW_FLOW_CONFIG_VALUE"] = json.dumps(cfg_env)
781
+
750
782
  # Set AWS DynamoDb Table Name for state tracking for for-eaches.
751
783
  # There are three instances when metaflow runtime directly interacts
752
784
  # with AWS DynamoDB.
@@ -326,6 +326,7 @@ def make_flow(
326
326
 
327
327
  # Attach AWS Batch decorator to the flow
328
328
  decorators._attach_decorators(obj.flow, [BatchDecorator.name])
329
+ decorators._init(obj.flow)
329
330
  decorators._init_step_decorators(
330
331
  obj.flow, obj.graph, obj.environment, obj.flow_datastore, obj.logger
331
332
  )
@@ -46,6 +46,7 @@ class StepFunctionsTriggeredRun(TriggeredRun):
46
46
  )
47
47
 
48
48
  command_obj = self.deployer.spm.get(pid)
49
+ command_obj.sync_wait()
49
50
  return command_obj.process.returncode == 0
50
51
 
51
52
 
@@ -174,6 +175,7 @@ class StepFunctionsDeployedFlow(DeployedFlow):
174
175
  )
175
176
 
176
177
  command_obj = self.deployer.spm.get(pid)
178
+ command_obj.sync_wait()
177
179
  return command_obj.process.returncode == 0
178
180
 
179
181
  def trigger(self, **kwargs) -> StepFunctionsTriggeredRun:
@@ -217,6 +219,7 @@ class StepFunctionsDeployedFlow(DeployedFlow):
217
219
  attribute_file_fd, command_obj, self.deployer.file_read_timeout
218
220
  )
219
221
 
222
+ command_obj.sync_wait()
220
223
  if command_obj.process.returncode == 0:
221
224
  return StepFunctionsTriggeredRun(
222
225
  deployer=self.deployer, content=content
@@ -722,8 +722,8 @@ def cli():
722
722
  pass
723
723
 
724
724
 
725
- @tracing.cli_entrypoint("s3op/list")
726
725
  @cli.command("list", help="List S3 objects")
726
+ @tracing.cli_entrypoint("s3op/list")
727
727
  @click.option(
728
728
  "--recursive/--no-recursive",
729
729
  default=False,
@@ -782,8 +782,8 @@ def lst(
782
782
  print(format_result_line(idx, url.prefix, url.url, str(size)))
783
783
 
784
784
 
785
- @tracing.cli_entrypoint("s3op/put")
786
785
  @cli.command(help="Upload files to S3")
786
+ @tracing.cli_entrypoint("s3op/put")
787
787
  @click.option(
788
788
  "--file",
789
789
  "files",
@@ -977,8 +977,8 @@ def _populate_prefixes(prefixes, inputs):
977
977
  return prefixes, is_transient_retry
978
978
 
979
979
 
980
- @tracing.cli_entrypoint("s3op/get")
981
980
  @cli.command(help="Download files from S3")
981
+ @tracing.cli_entrypoint("s3op/get")
982
982
  @click.option(
983
983
  "--recursive/--no-recursive",
984
984
  default=False,
@@ -314,7 +314,7 @@ class Kubernetes(object):
314
314
  jobset.secret(k)
315
315
 
316
316
  initial_configs = init_config()
317
- for entry in ["OBP_PERIMETER", "OBP_INTEGRATIONS_SECRETS_METADATA_URL"]:
317
+ for entry in ["OBP_PERIMETER", "OBP_INTEGRATIONS_URL"]:
318
318
  if entry not in initial_configs:
319
319
  raise KubernetesException(
320
320
  f"{entry} was not found in metaflow config. Please make sure to run `outerbounds configure <...>` command which can be found on the Ourebounds UI or reach out to your Outerbounds support team."
@@ -322,8 +322,8 @@ class Kubernetes(object):
322
322
 
323
323
  additional_obp_configs = {
324
324
  "OBP_PERIMETER": initial_configs["OBP_PERIMETER"],
325
- "OBP_INTEGRATIONS_SECRETS_METADATA_URL": initial_configs[
326
- "OBP_INTEGRATIONS_SECRETS_METADATA_URL"
325
+ "OBP_INTEGRATIONS_URL": initial_configs[
326
+ "OBP_INTEGRATIONS_URL"
327
327
  ],
328
328
  }
329
329
  for k, v in additional_obp_configs.items():
@@ -33,12 +33,12 @@ def kubernetes():
33
33
  pass
34
34
 
35
35
 
36
- @tracing.cli_entrypoint("kubernetes/step")
37
36
  @kubernetes.command(
38
37
  help="Execute a single task on Kubernetes. This command calls the top-level step "
39
38
  "command inside a Kubernetes pod with the given options. Typically you do not call "
40
39
  "this command directly; it is used internally by Metaflow."
41
40
  )
41
+ @tracing.cli_entrypoint("kubernetes/step")
42
42
  @click.argument("step-name")
43
43
  @click.argument("code-package-sha")
44
44
  @click.argument("code-package-url")
@@ -153,8 +153,8 @@ class KubernetesDecorator(StepDecorator):
153
153
  supports_conda_environment = True
154
154
  target_platform = "linux-64"
155
155
 
156
- def __init__(self, attributes=None, statically_defined=False):
157
- super(KubernetesDecorator, self).__init__(attributes, statically_defined)
156
+ def init(self):
157
+ super(KubernetesDecorator, self).init()
158
158
 
159
159
  if not self.attributes["namespace"]:
160
160
  self.attributes["namespace"] = KUBERNETES_NAMESPACE
@@ -84,7 +84,7 @@ class KubernetesJob(object):
84
84
  self._kwargs["disk"],
85
85
  )
86
86
  initial_configs = init_config()
87
- for entry in ["OBP_PERIMETER", "OBP_INTEGRATIONS_SECRETS_METADATA_URL"]:
87
+ for entry in ["OBP_PERIMETER", "OBP_INTEGRATIONS_URL"]:
88
88
  if entry not in initial_configs:
89
89
  raise KubernetesJobException(
90
90
  f"{entry} was not found in metaflow config. Please make sure to run `outerbounds configure <...>` command which can be found on the Ourebounds UI or reach out to your Outerbounds support team."
@@ -92,8 +92,8 @@ class KubernetesJob(object):
92
92
 
93
93
  additional_obp_configs = {
94
94
  "OBP_PERIMETER": initial_configs["OBP_PERIMETER"],
95
- "OBP_INTEGRATIONS_SECRETS_METADATA_URL": initial_configs[
96
- "OBP_INTEGRATIONS_SECRETS_METADATA_URL"
95
+ "OBP_INTEGRATIONS_URL": initial_configs[
96
+ "OBP_INTEGRATIONS_URL"
97
97
  ],
98
98
  }
99
99
 
@@ -50,20 +50,26 @@ class CondaStepDecorator(StepDecorator):
50
50
  # conda channels, users can specify channel::package as the package name.
51
51
 
52
52
  def __init__(self, attributes=None, statically_defined=False):
53
- self._user_defined_attributes = (
54
- attributes.copy() if attributes is not None else {}
53
+ self._attributes_with_user_values = (
54
+ set(attributes.keys()) if attributes is not None else set()
55
55
  )
56
+
56
57
  super(CondaStepDecorator, self).__init__(attributes, statically_defined)
57
58
 
59
+ def init(self):
60
+ super(CondaStepDecorator, self).init()
61
+
58
62
  # Support legacy 'libraries=' attribute for the decorator.
59
63
  self.attributes["packages"] = {
60
64
  **self.attributes["libraries"],
61
65
  **self.attributes["packages"],
62
66
  }
63
67
  del self.attributes["libraries"]
68
+ if self.attributes["packages"]:
69
+ self._attributes_with_user_values.add("packages")
64
70
 
65
71
  def is_attribute_user_defined(self, name):
66
- return name in self._user_defined_attributes
72
+ return name in self._attributes_with_user_values
67
73
 
68
74
  def step_init(self, flow, graph, step, decos, environment, flow_datastore, logger):
69
75
  # The init_environment hook for Environment creates the relevant virtual
@@ -83,10 +89,10 @@ class CondaStepDecorator(StepDecorator):
83
89
  **super_attributes["packages"],
84
90
  **self.attributes["packages"],
85
91
  }
86
- self._user_defined_attributes = {
87
- **self._user_defined_attributes,
88
- **conda_base._user_defined_attributes,
89
- }
92
+ self._attributes_with_user_values.update(
93
+ conda_base._attributes_with_user_values
94
+ )
95
+
90
96
  self.attributes["python"] = (
91
97
  self.attributes["python"] or super_attributes["python"]
92
98
  )
@@ -333,11 +339,15 @@ class CondaFlowDecorator(FlowDecorator):
333
339
  }
334
340
 
335
341
  def __init__(self, attributes=None, statically_defined=False):
336
- self._user_defined_attributes = (
337
- attributes.copy() if attributes is not None else {}
342
+ self._attributes_with_user_values = (
343
+ set(attributes.keys()) if attributes is not None else set()
338
344
  )
345
+
339
346
  super(CondaFlowDecorator, self).__init__(attributes, statically_defined)
340
347
 
348
+ def init(self):
349
+ super(CondaFlowDecorator, self).init()
350
+
341
351
  # Support legacy 'libraries=' attribute for the decorator.
342
352
  self.attributes["packages"] = {
343
353
  **self.attributes["libraries"],
@@ -348,7 +358,7 @@ class CondaFlowDecorator(FlowDecorator):
348
358
  self.attributes["python"] = str(self.attributes["python"])
349
359
 
350
360
  def is_attribute_user_defined(self, name):
351
- return name in self._user_defined_attributes
361
+ return name in self._attributes_with_user_values
352
362
 
353
363
  def flow_init(
354
364
  self, flow, graph, environment, flow_datastore, metadata, logger, echo, options
@@ -25,9 +25,10 @@ class PyPIStepDecorator(StepDecorator):
25
25
  defaults = {"packages": {}, "python": None, "disabled": None} # wheels
26
26
 
27
27
  def __init__(self, attributes=None, statically_defined=False):
28
- self._user_defined_attributes = (
29
- attributes.copy() if attributes is not None else {}
28
+ self._attributes_with_user_values = (
29
+ set(attributes.keys()) if attributes is not None else set()
30
30
  )
31
+
31
32
  super().__init__(attributes, statically_defined)
32
33
 
33
34
  def step_init(self, flow, graph, step, decos, environment, flow_datastore, logger):
@@ -42,10 +43,9 @@ class PyPIStepDecorator(StepDecorator):
42
43
  if "pypi_base" in self.flow._flow_decorators:
43
44
  pypi_base = self.flow._flow_decorators["pypi_base"][0]
44
45
  super_attributes = pypi_base.attributes
45
- self._user_defined_attributes = {
46
- **self._user_defined_attributes,
47
- **pypi_base._user_defined_attributes,
48
- }
46
+ self._attributes_with_user_values.update(
47
+ pypi_base._attributes_with_user_values
48
+ )
49
49
  self.attributes["packages"] = {
50
50
  **super_attributes["packages"],
51
51
  **self.attributes["packages"],
@@ -106,7 +106,7 @@ class PyPIStepDecorator(StepDecorator):
106
106
  environment.set_local_root(LocalStorage.get_datastore_root_from_config(logger))
107
107
 
108
108
  def is_attribute_user_defined(self, name):
109
- return name in self._user_defined_attributes
109
+ return name in self._attributes_with_user_values
110
110
 
111
111
 
112
112
  class PyPIFlowDecorator(FlowDecorator):
@@ -129,9 +129,10 @@ class PyPIFlowDecorator(FlowDecorator):
129
129
  defaults = {"packages": {}, "python": None, "disabled": None}
130
130
 
131
131
  def __init__(self, attributes=None, statically_defined=False):
132
- self._user_defined_attributes = (
133
- attributes.copy() if attributes is not None else {}
132
+ self._attributes_with_user_values = (
133
+ set(attributes.keys()) if attributes is not None else set()
134
134
  )
135
+
135
136
  super().__init__(attributes, statically_defined)
136
137
 
137
138
  def flow_init(
@@ -140,6 +141,7 @@ class PyPIFlowDecorator(FlowDecorator):
140
141
  from metaflow import decorators
141
142
 
142
143
  decorators._attach_decorators(flow, ["pypi"])
144
+ decorators._init(flow)
143
145
 
144
146
  # @pypi uses a conda environment to create a virtual environment.
145
147
  # The conda environment can be created through micromamba.
@@ -37,8 +37,8 @@ class TimeoutDecorator(StepDecorator):
37
37
  name = "timeout"
38
38
  defaults = {"seconds": 0, "minutes": 0, "hours": 0}
39
39
 
40
- def __init__(self, *args, **kwargs):
41
- super(TimeoutDecorator, self).__init__(*args, **kwargs)
40
+ def init(self):
41
+ super().init()
42
42
  # Initialize secs in __init__ so other decorators could safely use this
43
43
  # value without worrying about decorator order.
44
44
  # Convert values in attributes to type:int since they can be type:str
@@ -9,6 +9,7 @@ if sys.version_info < (3, 7):
9
9
  )
10
10
 
11
11
  import datetime
12
+ import functools
12
13
  import importlib
13
14
  import inspect
14
15
  import itertools
@@ -38,6 +39,7 @@ from metaflow.decorators import add_decorator_options
38
39
  from metaflow.exception import MetaflowException
39
40
  from metaflow.includefile import FilePathClass
40
41
  from metaflow.parameters import JSONTypeClass, flow_context
42
+ from metaflow.user_configs.config_options import LocalFileInput
41
43
 
42
44
  # Define a recursive type alias for JSON
43
45
  JSON = Union[Dict[str, "JSON"], List["JSON"], str, int, float, bool, None]
@@ -55,6 +57,7 @@ click_to_python_types = {
55
57
  File: str,
56
58
  JSONTypeClass: JSON,
57
59
  FilePathClass: str,
60
+ LocalFileInput: str,
58
61
  }
59
62
 
60
63
 
@@ -124,6 +127,37 @@ def _method_sanity_check(
124
127
  return method_params
125
128
 
126
129
 
130
+ def _lazy_load_command(
131
+ cli_collection: click.Group,
132
+ flow_parameters: Union[str, List[Parameter]],
133
+ _self,
134
+ name: str,
135
+ ):
136
+
137
+ # Context is not used in get_command so we can pass None. Since we pin click,
138
+ # this won't change from under us.
139
+
140
+ if isinstance(flow_parameters, str):
141
+ # Resolve flow_parameters -- for start, this is a function which we
142
+ # need to call to figure out the actual parameters (may be changed by configs)
143
+ flow_parameters = getattr(_self, flow_parameters)()
144
+ cmd_obj = cli_collection.get_command(None, name)
145
+ if cmd_obj:
146
+ if isinstance(cmd_obj, click.Group):
147
+ # TODO: possibly check for fake groups with cmd_obj.name in ["cli", "main"]
148
+ result = functools.partial(extract_group(cmd_obj, flow_parameters), _self)
149
+ elif isinstance(cmd_obj, click.Command):
150
+ result = functools.partial(extract_command(cmd_obj, flow_parameters), _self)
151
+ else:
152
+ raise RuntimeError(
153
+ "Cannot handle %s of type %s" % (cmd_obj.name, type(cmd_obj))
154
+ )
155
+ setattr(_self, name, result)
156
+ return result
157
+ else:
158
+ raise AttributeError()
159
+
160
+
127
161
  def get_annotation(param: Union[click.Argument, click.Option]):
128
162
  py_type = click_to_python_types[type(param.type)]
129
163
  if not param.required:
@@ -179,9 +213,11 @@ def extract_flow_class_from_file(flow_file: str) -> FlowSpec:
179
213
 
180
214
 
181
215
  class MetaflowAPI(object):
182
- def __init__(self, parent=None, **kwargs):
216
+ def __init__(self, parent=None, flow_cls=None, **kwargs):
183
217
  self._parent = parent
184
218
  self._chain = [{self._API_NAME: kwargs}]
219
+ self._flow_cls = flow_cls
220
+ self._cached_computed_parameters = None
185
221
 
186
222
  @property
187
223
  def parent(self):
@@ -200,23 +236,22 @@ class MetaflowAPI(object):
200
236
  @classmethod
201
237
  def from_cli(cls, flow_file: str, cli_collection: Callable) -> Callable:
202
238
  flow_cls = extract_flow_class_from_file(flow_file)
203
- flow_parameters = [p for _, p in flow_cls._get_parameters()]
239
+
204
240
  with flow_context(flow_cls) as _:
205
241
  add_decorator_options(cli_collection)
206
242
 
207
- class_dict = {"__module__": "metaflow", "_API_NAME": flow_file}
208
- command_groups = cli_collection.sources
209
- for each_group in command_groups:
210
- for _, cmd_obj in each_group.commands.items():
211
- if isinstance(cmd_obj, click.Group):
212
- # TODO: possibly check for fake groups with cmd_obj.name in ["cli", "main"]
213
- class_dict[cmd_obj.name] = extract_group(cmd_obj, flow_parameters)
214
- elif isinstance(cmd_obj, click.Command):
215
- class_dict[cmd_obj.name] = extract_command(cmd_obj, flow_parameters)
216
- else:
217
- raise RuntimeError(
218
- "Cannot handle %s of type %s" % (cmd_obj.name, type(cmd_obj))
219
- )
243
+ def getattr_wrapper(_self, name):
244
+ # Functools.partial do not automatically bind self (no __get__)
245
+ return _self._internal_getattr(_self, name)
246
+
247
+ class_dict = {
248
+ "__module__": "metaflow",
249
+ "_API_NAME": flow_file,
250
+ "_internal_getattr": functools.partial(
251
+ _lazy_load_command, cli_collection, "_compute_flow_parameters"
252
+ ),
253
+ "__getattr__": getattr_wrapper,
254
+ }
220
255
 
221
256
  to_return = type(flow_file, (MetaflowAPI,), class_dict)
222
257
  to_return.__name__ = flow_file
@@ -237,11 +272,11 @@ class MetaflowAPI(object):
237
272
  defaults,
238
273
  **kwargs,
239
274
  )
240
- return to_return(parent=None, **method_params)
275
+ return to_return(parent=None, flow_cls=flow_cls, **method_params)
241
276
 
242
277
  m = _method
243
- m.__name__ = cmd_obj.name
244
- m.__doc__ = getattr(cmd_obj, "help", None)
278
+ m.__name__ = cli_collection.name
279
+ m.__doc__ = getattr(cli_collection, "help", None)
245
280
  m.__signature__ = inspect.signature(_method).replace(
246
281
  parameters=params_sigs.values()
247
282
  )
@@ -287,6 +322,25 @@ class MetaflowAPI(object):
287
322
 
288
323
  return components
289
324
 
325
+ def _compute_flow_parameters(self):
326
+ if self._flow_cls is None or self._parent is not None:
327
+ raise RuntimeError(
328
+ "Computing flow-level parameters for a non start API. "
329
+ "Please report to the Metaflow team."
330
+ )
331
+ # TODO: We need to actually compute the new parameters (based on configs) which
332
+ # would involve processing the options at least partially. We will do this
333
+ # before GA but for now making it work for regular parameters
334
+ if self._cached_computed_parameters is not None:
335
+ return self._cached_computed_parameters
336
+ self._cached_computed_parameters = []
337
+ for _, param in self._flow_cls._get_parameters():
338
+ if param.IS_CONFIG_PARAMETER:
339
+ continue
340
+ param.init()
341
+ self._cached_computed_parameters.append(param)
342
+ return self._cached_computed_parameters
343
+
290
344
 
291
345
  def extract_all_params(cmd_obj: Union[click.Command, click.Group]):
292
346
  arg_params_sigs = OrderedDict()
@@ -351,7 +405,7 @@ def extract_group(cmd_obj: click.Group, flow_parameters: List[Parameter]) -> Cal
351
405
  method_params = _method_sanity_check(
352
406
  possible_arg_params, possible_opt_params, annotations, defaults, **kwargs
353
407
  )
354
- return resulting_class(parent=_self, **method_params)
408
+ return resulting_class(parent=_self, flow_cls=None, **method_params)
355
409
 
356
410
  m = _method
357
411
  m.__name__ = cmd_obj.name