ob-metaflow 2.11.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.
Files changed (289) hide show
  1. metaflow/R.py +10 -7
  2. metaflow/__init__.py +40 -25
  3. metaflow/_vendor/imghdr/__init__.py +186 -0
  4. metaflow/_vendor/importlib_metadata/__init__.py +1063 -0
  5. metaflow/_vendor/importlib_metadata/_adapters.py +68 -0
  6. metaflow/_vendor/importlib_metadata/_collections.py +30 -0
  7. metaflow/_vendor/importlib_metadata/_compat.py +71 -0
  8. metaflow/_vendor/importlib_metadata/_functools.py +104 -0
  9. metaflow/_vendor/importlib_metadata/_itertools.py +73 -0
  10. metaflow/_vendor/importlib_metadata/_meta.py +48 -0
  11. metaflow/_vendor/importlib_metadata/_text.py +99 -0
  12. metaflow/_vendor/importlib_metadata/py.typed +0 -0
  13. metaflow/_vendor/typeguard/__init__.py +48 -0
  14. metaflow/_vendor/typeguard/_checkers.py +1070 -0
  15. metaflow/_vendor/typeguard/_config.py +108 -0
  16. metaflow/_vendor/typeguard/_decorators.py +233 -0
  17. metaflow/_vendor/typeguard/_exceptions.py +42 -0
  18. metaflow/_vendor/typeguard/_functions.py +308 -0
  19. metaflow/_vendor/typeguard/_importhook.py +213 -0
  20. metaflow/_vendor/typeguard/_memo.py +48 -0
  21. metaflow/_vendor/typeguard/_pytest_plugin.py +127 -0
  22. metaflow/_vendor/typeguard/_suppression.py +86 -0
  23. metaflow/_vendor/typeguard/_transformer.py +1229 -0
  24. metaflow/_vendor/typeguard/_union_transformer.py +55 -0
  25. metaflow/_vendor/typeguard/_utils.py +173 -0
  26. metaflow/_vendor/typeguard/py.typed +0 -0
  27. metaflow/_vendor/typing_extensions.py +3641 -0
  28. metaflow/_vendor/v3_7/importlib_metadata/__init__.py +1063 -0
  29. metaflow/_vendor/v3_7/importlib_metadata/_adapters.py +68 -0
  30. metaflow/_vendor/v3_7/importlib_metadata/_collections.py +30 -0
  31. metaflow/_vendor/v3_7/importlib_metadata/_compat.py +71 -0
  32. metaflow/_vendor/v3_7/importlib_metadata/_functools.py +104 -0
  33. metaflow/_vendor/v3_7/importlib_metadata/_itertools.py +73 -0
  34. metaflow/_vendor/v3_7/importlib_metadata/_meta.py +48 -0
  35. metaflow/_vendor/v3_7/importlib_metadata/_text.py +99 -0
  36. metaflow/_vendor/v3_7/importlib_metadata/py.typed +0 -0
  37. metaflow/_vendor/v3_7/typeguard/__init__.py +48 -0
  38. metaflow/_vendor/v3_7/typeguard/_checkers.py +906 -0
  39. metaflow/_vendor/v3_7/typeguard/_config.py +108 -0
  40. metaflow/_vendor/v3_7/typeguard/_decorators.py +237 -0
  41. metaflow/_vendor/v3_7/typeguard/_exceptions.py +42 -0
  42. metaflow/_vendor/v3_7/typeguard/_functions.py +310 -0
  43. metaflow/_vendor/v3_7/typeguard/_importhook.py +213 -0
  44. metaflow/_vendor/v3_7/typeguard/_memo.py +48 -0
  45. metaflow/_vendor/v3_7/typeguard/_pytest_plugin.py +100 -0
  46. metaflow/_vendor/v3_7/typeguard/_suppression.py +88 -0
  47. metaflow/_vendor/v3_7/typeguard/_transformer.py +1207 -0
  48. metaflow/_vendor/v3_7/typeguard/_union_transformer.py +54 -0
  49. metaflow/_vendor/v3_7/typeguard/_utils.py +169 -0
  50. metaflow/_vendor/v3_7/typeguard/py.typed +0 -0
  51. metaflow/_vendor/v3_7/typing_extensions.py +3072 -0
  52. metaflow/_vendor/yaml/__init__.py +427 -0
  53. metaflow/_vendor/yaml/composer.py +139 -0
  54. metaflow/_vendor/yaml/constructor.py +748 -0
  55. metaflow/_vendor/yaml/cyaml.py +101 -0
  56. metaflow/_vendor/yaml/dumper.py +62 -0
  57. metaflow/_vendor/yaml/emitter.py +1137 -0
  58. metaflow/_vendor/yaml/error.py +75 -0
  59. metaflow/_vendor/yaml/events.py +86 -0
  60. metaflow/_vendor/yaml/loader.py +63 -0
  61. metaflow/_vendor/yaml/nodes.py +49 -0
  62. metaflow/_vendor/yaml/parser.py +589 -0
  63. metaflow/_vendor/yaml/reader.py +185 -0
  64. metaflow/_vendor/yaml/representer.py +389 -0
  65. metaflow/_vendor/yaml/resolver.py +227 -0
  66. metaflow/_vendor/yaml/scanner.py +1435 -0
  67. metaflow/_vendor/yaml/serializer.py +111 -0
  68. metaflow/_vendor/yaml/tokens.py +104 -0
  69. metaflow/cards.py +5 -0
  70. metaflow/cli.py +331 -785
  71. metaflow/cli_args.py +17 -0
  72. metaflow/cli_components/__init__.py +0 -0
  73. metaflow/cli_components/dump_cmd.py +96 -0
  74. metaflow/cli_components/init_cmd.py +52 -0
  75. metaflow/cli_components/run_cmds.py +546 -0
  76. metaflow/cli_components/step_cmd.py +334 -0
  77. metaflow/cli_components/utils.py +140 -0
  78. metaflow/client/__init__.py +1 -0
  79. metaflow/client/core.py +467 -73
  80. metaflow/client/filecache.py +75 -35
  81. metaflow/clone_util.py +7 -1
  82. metaflow/cmd/code/__init__.py +231 -0
  83. metaflow/cmd/develop/stub_generator.py +756 -288
  84. metaflow/cmd/develop/stubs.py +12 -28
  85. metaflow/cmd/main_cli.py +6 -4
  86. metaflow/cmd/make_wrapper.py +78 -0
  87. metaflow/datastore/__init__.py +1 -0
  88. metaflow/datastore/content_addressed_store.py +41 -10
  89. metaflow/datastore/datastore_set.py +11 -2
  90. metaflow/datastore/flow_datastore.py +156 -10
  91. metaflow/datastore/spin_datastore.py +91 -0
  92. metaflow/datastore/task_datastore.py +154 -39
  93. metaflow/debug.py +5 -0
  94. metaflow/decorators.py +404 -78
  95. metaflow/exception.py +8 -2
  96. metaflow/extension_support/__init__.py +527 -376
  97. metaflow/extension_support/_empty_file.py +2 -2
  98. metaflow/extension_support/plugins.py +49 -31
  99. metaflow/flowspec.py +482 -33
  100. metaflow/graph.py +210 -42
  101. metaflow/includefile.py +84 -40
  102. metaflow/lint.py +141 -22
  103. metaflow/meta_files.py +13 -0
  104. metaflow/{metadata → metadata_provider}/heartbeat.py +24 -8
  105. metaflow/{metadata → metadata_provider}/metadata.py +86 -1
  106. metaflow/metaflow_config.py +175 -28
  107. metaflow/metaflow_config_funcs.py +51 -3
  108. metaflow/metaflow_current.py +4 -10
  109. metaflow/metaflow_environment.py +139 -53
  110. metaflow/metaflow_git.py +115 -0
  111. metaflow/metaflow_profile.py +18 -0
  112. metaflow/metaflow_version.py +150 -66
  113. metaflow/mflog/__init__.py +4 -3
  114. metaflow/mflog/save_logs.py +2 -2
  115. metaflow/multicore_utils.py +31 -14
  116. metaflow/package/__init__.py +673 -0
  117. metaflow/packaging_sys/__init__.py +880 -0
  118. metaflow/packaging_sys/backend.py +128 -0
  119. metaflow/packaging_sys/distribution_support.py +153 -0
  120. metaflow/packaging_sys/tar_backend.py +99 -0
  121. metaflow/packaging_sys/utils.py +54 -0
  122. metaflow/packaging_sys/v1.py +527 -0
  123. metaflow/parameters.py +149 -28
  124. metaflow/plugins/__init__.py +74 -5
  125. metaflow/plugins/airflow/airflow.py +40 -25
  126. metaflow/plugins/airflow/airflow_cli.py +22 -5
  127. metaflow/plugins/airflow/airflow_decorator.py +1 -1
  128. metaflow/plugins/airflow/airflow_utils.py +5 -3
  129. metaflow/plugins/airflow/sensors/base_sensor.py +4 -4
  130. metaflow/plugins/airflow/sensors/external_task_sensor.py +2 -2
  131. metaflow/plugins/airflow/sensors/s3_sensor.py +2 -2
  132. metaflow/plugins/argo/argo_client.py +78 -33
  133. metaflow/plugins/argo/argo_events.py +6 -6
  134. metaflow/plugins/argo/argo_workflows.py +2410 -527
  135. metaflow/plugins/argo/argo_workflows_cli.py +571 -121
  136. metaflow/plugins/argo/argo_workflows_decorator.py +43 -12
  137. metaflow/plugins/argo/argo_workflows_deployer.py +106 -0
  138. metaflow/plugins/argo/argo_workflows_deployer_objects.py +453 -0
  139. metaflow/plugins/argo/capture_error.py +73 -0
  140. metaflow/plugins/argo/conditional_input_paths.py +35 -0
  141. metaflow/plugins/argo/exit_hooks.py +209 -0
  142. metaflow/plugins/argo/jobset_input_paths.py +15 -0
  143. metaflow/plugins/argo/param_val.py +19 -0
  144. metaflow/plugins/aws/aws_client.py +10 -3
  145. metaflow/plugins/aws/aws_utils.py +55 -2
  146. metaflow/plugins/aws/batch/batch.py +72 -5
  147. metaflow/plugins/aws/batch/batch_cli.py +33 -10
  148. metaflow/plugins/aws/batch/batch_client.py +4 -3
  149. metaflow/plugins/aws/batch/batch_decorator.py +102 -35
  150. metaflow/plugins/aws/secrets_manager/aws_secrets_manager_secrets_provider.py +13 -10
  151. metaflow/plugins/aws/step_functions/dynamo_db_client.py +0 -3
  152. metaflow/plugins/aws/step_functions/production_token.py +1 -1
  153. metaflow/plugins/aws/step_functions/step_functions.py +65 -8
  154. metaflow/plugins/aws/step_functions/step_functions_cli.py +101 -7
  155. metaflow/plugins/aws/step_functions/step_functions_decorator.py +1 -2
  156. metaflow/plugins/aws/step_functions/step_functions_deployer.py +97 -0
  157. metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +264 -0
  158. metaflow/plugins/azure/azure_exceptions.py +1 -1
  159. metaflow/plugins/azure/azure_secret_manager_secrets_provider.py +240 -0
  160. metaflow/plugins/azure/azure_tail.py +1 -1
  161. metaflow/plugins/azure/includefile_support.py +2 -0
  162. metaflow/plugins/cards/card_cli.py +66 -30
  163. metaflow/plugins/cards/card_creator.py +25 -1
  164. metaflow/plugins/cards/card_datastore.py +21 -49
  165. metaflow/plugins/cards/card_decorator.py +132 -8
  166. metaflow/plugins/cards/card_modules/basic.py +112 -17
  167. metaflow/plugins/cards/card_modules/bundle.css +1 -1
  168. metaflow/plugins/cards/card_modules/card.py +16 -1
  169. metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
  170. metaflow/plugins/cards/card_modules/components.py +665 -28
  171. metaflow/plugins/cards/card_modules/convert_to_native_type.py +36 -7
  172. metaflow/plugins/cards/card_modules/json_viewer.py +232 -0
  173. metaflow/plugins/cards/card_modules/main.css +1 -0
  174. metaflow/plugins/cards/card_modules/main.js +68 -49
  175. metaflow/plugins/cards/card_modules/renderer_tools.py +1 -0
  176. metaflow/plugins/cards/card_modules/test_cards.py +26 -12
  177. metaflow/plugins/cards/card_server.py +39 -14
  178. metaflow/plugins/cards/component_serializer.py +2 -9
  179. metaflow/plugins/cards/metadata.py +22 -0
  180. metaflow/plugins/catch_decorator.py +9 -0
  181. metaflow/plugins/datastores/azure_storage.py +10 -1
  182. metaflow/plugins/datastores/gs_storage.py +6 -2
  183. metaflow/plugins/datastores/local_storage.py +12 -6
  184. metaflow/plugins/datastores/spin_storage.py +12 -0
  185. metaflow/plugins/datatools/local.py +2 -0
  186. metaflow/plugins/datatools/s3/s3.py +126 -75
  187. metaflow/plugins/datatools/s3/s3op.py +254 -121
  188. metaflow/plugins/env_escape/__init__.py +3 -3
  189. metaflow/plugins/env_escape/client_modules.py +102 -72
  190. metaflow/plugins/env_escape/server.py +7 -0
  191. metaflow/plugins/env_escape/stub.py +24 -5
  192. metaflow/plugins/events_decorator.py +343 -185
  193. metaflow/plugins/exit_hook/__init__.py +0 -0
  194. metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
  195. metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
  196. metaflow/plugins/gcp/__init__.py +1 -1
  197. metaflow/plugins/gcp/gcp_secret_manager_secrets_provider.py +11 -6
  198. metaflow/plugins/gcp/gs_tail.py +10 -6
  199. metaflow/plugins/gcp/includefile_support.py +3 -0
  200. metaflow/plugins/kubernetes/kube_utils.py +108 -0
  201. metaflow/plugins/kubernetes/kubernetes.py +411 -130
  202. metaflow/plugins/kubernetes/kubernetes_cli.py +168 -36
  203. metaflow/plugins/kubernetes/kubernetes_client.py +104 -2
  204. metaflow/plugins/kubernetes/kubernetes_decorator.py +246 -88
  205. metaflow/plugins/kubernetes/kubernetes_job.py +253 -581
  206. metaflow/plugins/kubernetes/kubernetes_jobsets.py +1071 -0
  207. metaflow/plugins/kubernetes/spot_metadata_cli.py +69 -0
  208. metaflow/plugins/kubernetes/spot_monitor_sidecar.py +109 -0
  209. metaflow/plugins/logs_cli.py +359 -0
  210. metaflow/plugins/{metadata → metadata_providers}/local.py +144 -84
  211. metaflow/plugins/{metadata → metadata_providers}/service.py +103 -26
  212. metaflow/plugins/metadata_providers/spin.py +16 -0
  213. metaflow/plugins/package_cli.py +36 -24
  214. metaflow/plugins/parallel_decorator.py +128 -11
  215. metaflow/plugins/parsers.py +16 -0
  216. metaflow/plugins/project_decorator.py +51 -5
  217. metaflow/plugins/pypi/bootstrap.py +357 -105
  218. metaflow/plugins/pypi/conda_decorator.py +82 -81
  219. metaflow/plugins/pypi/conda_environment.py +187 -52
  220. metaflow/plugins/pypi/micromamba.py +157 -47
  221. metaflow/plugins/pypi/parsers.py +268 -0
  222. metaflow/plugins/pypi/pip.py +88 -13
  223. metaflow/plugins/pypi/pypi_decorator.py +37 -1
  224. metaflow/plugins/pypi/utils.py +48 -2
  225. metaflow/plugins/resources_decorator.py +2 -2
  226. metaflow/plugins/secrets/__init__.py +3 -0
  227. metaflow/plugins/secrets/secrets_decorator.py +26 -181
  228. metaflow/plugins/secrets/secrets_func.py +49 -0
  229. metaflow/plugins/secrets/secrets_spec.py +101 -0
  230. metaflow/plugins/secrets/utils.py +74 -0
  231. metaflow/plugins/tag_cli.py +4 -7
  232. metaflow/plugins/test_unbounded_foreach_decorator.py +41 -6
  233. metaflow/plugins/timeout_decorator.py +3 -3
  234. metaflow/plugins/uv/__init__.py +0 -0
  235. metaflow/plugins/uv/bootstrap.py +128 -0
  236. metaflow/plugins/uv/uv_environment.py +72 -0
  237. metaflow/procpoll.py +1 -1
  238. metaflow/pylint_wrapper.py +5 -1
  239. metaflow/runner/__init__.py +0 -0
  240. metaflow/runner/click_api.py +717 -0
  241. metaflow/runner/deployer.py +470 -0
  242. metaflow/runner/deployer_impl.py +201 -0
  243. metaflow/runner/metaflow_runner.py +714 -0
  244. metaflow/runner/nbdeploy.py +132 -0
  245. metaflow/runner/nbrun.py +225 -0
  246. metaflow/runner/subprocess_manager.py +650 -0
  247. metaflow/runner/utils.py +335 -0
  248. metaflow/runtime.py +1078 -260
  249. metaflow/sidecar/sidecar_worker.py +1 -1
  250. metaflow/system/__init__.py +5 -0
  251. metaflow/system/system_logger.py +85 -0
  252. metaflow/system/system_monitor.py +108 -0
  253. metaflow/system/system_utils.py +19 -0
  254. metaflow/task.py +521 -225
  255. metaflow/tracing/__init__.py +7 -7
  256. metaflow/tracing/span_exporter.py +31 -38
  257. metaflow/tracing/tracing_modules.py +38 -43
  258. metaflow/tuple_util.py +27 -0
  259. metaflow/user_configs/__init__.py +0 -0
  260. metaflow/user_configs/config_options.py +563 -0
  261. metaflow/user_configs/config_parameters.py +598 -0
  262. metaflow/user_decorators/__init__.py +0 -0
  263. metaflow/user_decorators/common.py +144 -0
  264. metaflow/user_decorators/mutable_flow.py +512 -0
  265. metaflow/user_decorators/mutable_step.py +424 -0
  266. metaflow/user_decorators/user_flow_decorator.py +264 -0
  267. metaflow/user_decorators/user_step_decorator.py +749 -0
  268. metaflow/util.py +243 -27
  269. metaflow/vendor.py +23 -7
  270. metaflow/version.py +1 -1
  271. ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/Makefile +355 -0
  272. ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/Tiltfile +726 -0
  273. ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/pick_services.sh +105 -0
  274. ob_metaflow-2.19.7.1rc0.dist-info/METADATA +87 -0
  275. ob_metaflow-2.19.7.1rc0.dist-info/RECORD +445 -0
  276. {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/WHEEL +1 -1
  277. {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/entry_points.txt +1 -0
  278. metaflow/_vendor/v3_5/__init__.py +0 -1
  279. metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
  280. metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
  281. metaflow/package.py +0 -188
  282. ob_metaflow-2.11.13.1.dist-info/METADATA +0 -85
  283. ob_metaflow-2.11.13.1.dist-info/RECORD +0 -308
  284. /metaflow/_vendor/{v3_5/zipp.py → zipp.py} +0 -0
  285. /metaflow/{metadata → metadata_provider}/__init__.py +0 -0
  286. /metaflow/{metadata → metadata_provider}/util.py +0 -0
  287. /metaflow/plugins/{metadata → metadata_providers}/__init__.py +0 -0
  288. {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info/licenses}/LICENSE +0 -0
  289. {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/top_level.txt +0 -0
metaflow/parameters.py CHANGED
@@ -1,5 +1,9 @@
1
1
  import json
2
- from typing import Any, Callable, Dict, NamedTuple, Optional, Type, Union
2
+
3
+ from contextlib import contextmanager
4
+ from threading import local
5
+
6
+ from typing import Any, Callable, Dict, NamedTuple, Optional, TYPE_CHECKING, Type, Union
3
7
 
4
8
  from metaflow._vendor import click
5
9
 
@@ -10,6 +14,9 @@ from .exception import (
10
14
  MetaflowException,
11
15
  )
12
16
 
17
+ if TYPE_CHECKING:
18
+ from .user_configs.config_parameters import ConfigValue
19
+
13
20
  try:
14
21
  # Python2
15
22
  strtype = basestring
@@ -28,13 +35,57 @@ ParameterContext = NamedTuple(
28
35
  ("parameter_name", str),
29
36
  ("logger", Callable[..., None]),
30
37
  ("ds_type", str),
38
+ ("configs", Optional["ConfigValue"]),
31
39
  ],
32
40
  )
33
41
 
34
- parameters = [] # Set by FlowSpec.__init__()
42
+
43
+ # When we launch a flow, we need to know the parameters so we can
44
+ # attach them with add_custom_parameters to commands. This used to be a global
45
+ # but causes problems when multiple FlowSpec are loaded (as can happen when using
46
+ # the Runner or just if multiple Flows are defined and instantiated). To minimally
47
+ # impact code, we now create the CLI with a thread local value of the FlowSpec
48
+ # that is being used to create the CLI which enables us to extract the parameters
49
+ # directly from the Flow.
50
+ current_flow = local()
51
+
52
+
53
+ @contextmanager
54
+ def flow_context(flow_cls):
55
+ """
56
+ Context manager to set the current flow for the thread. This is used
57
+ to extract the parameters from the FlowSpec that is being used to create
58
+ the CLI.
59
+ """
60
+ # Use a stack because with the runner this can get called multiple times in
61
+ # a nested fashion
62
+ current_flow.flow_cls_stack = getattr(current_flow, "flow_cls_stack", [])
63
+ current_flow.flow_cls_stack.insert(0, flow_cls)
64
+ current_flow.flow_cls = current_flow.flow_cls_stack[0]
65
+ try:
66
+ yield
67
+ finally:
68
+ current_flow.flow_cls_stack = current_flow.flow_cls_stack[1:]
69
+ if len(current_flow.flow_cls_stack) == 0:
70
+ del current_flow.flow_cls_stack
71
+ del current_flow.flow_cls
72
+ else:
73
+ current_flow.flow_cls = current_flow.flow_cls_stack[0]
74
+
75
+
35
76
  context_proto = None
36
77
 
37
78
 
79
+ def replace_flow_context(flow_cls):
80
+ """
81
+ Replace the current flow context with a new flow class. This is used
82
+ when we change the current flow class after having run user configuration functions
83
+ """
84
+ current_flow.flow_cls_stack = current_flow.flow_cls_stack[1:]
85
+ current_flow.flow_cls_stack.insert(0, flow_cls)
86
+ current_flow.flow_cls = current_flow.flow_cls_stack[0]
87
+
88
+
38
89
  class JSONTypeClass(click.ParamType):
39
90
  name = "JSON"
40
91
 
@@ -78,9 +129,9 @@ class DeployTimeField(object):
78
129
  self.parameter_name = parameter_name
79
130
  self.parameter_type = parameter_type
80
131
  self.return_str = return_str
81
- self.print_representation = (
82
- self.user_print_representation
83
- ) = print_representation
132
+ self.print_representation = self.user_print_representation = (
133
+ print_representation
134
+ )
84
135
  if self.print_representation is None:
85
136
  self.print_representation = str(self.fun)
86
137
 
@@ -114,6 +165,7 @@ class DeployTimeField(object):
114
165
  return self._check_type(val, deploy_time)
115
166
 
116
167
  def _check_type(self, val, deploy_time):
168
+
117
169
  # it is easy to introduce a deploy-time function that accidentally
118
170
  # returns a value whose type is not compatible with what is defined
119
171
  # in Parameter. Let's catch those mistakes early here, instead of
@@ -121,7 +173,7 @@ class DeployTimeField(object):
121
173
 
122
174
  # note: this doesn't work with long in Python2 or types defined as
123
175
  # click types, e.g. click.INT
124
- TYPES = {bool: "bool", int: "int", float: "float", list: "list"}
176
+ TYPES = {bool: "bool", int: "int", float: "float", list: "list", dict: "dict"}
125
177
 
126
178
  msg = (
127
179
  "The value returned by the deploy-time function for "
@@ -129,7 +181,12 @@ class DeployTimeField(object):
129
181
  % (self.parameter_name, self.field)
130
182
  )
131
183
 
132
- if self.parameter_type in TYPES:
184
+ if isinstance(self.parameter_type, list):
185
+ if not any(isinstance(val, x) for x in self.parameter_type):
186
+ msg += "Expected one of the following %s." % TYPES[self.parameter_type]
187
+ raise ParameterFieldTypeMismatch(msg)
188
+ return str(val) if self.return_str else val
189
+ elif self.parameter_type in TYPES:
133
190
  if type(val) != self.parameter_type:
134
191
  msg += "Expected a %s." % TYPES[self.parameter_type]
135
192
  raise ParameterFieldTypeMismatch(msg)
@@ -167,12 +224,18 @@ class DeployTimeField(object):
167
224
  def deploy_time_eval(value):
168
225
  if isinstance(value, DeployTimeField):
169
226
  return value(deploy_time=True)
227
+ elif isinstance(value, DelayedEvaluationParameter):
228
+ return value(return_str=True)
170
229
  else:
171
230
  return value
172
231
 
173
232
 
174
233
  # this is called by cli.main
175
- def set_parameter_context(flow_name, echo, datastore):
234
+ def set_parameter_context(flow_name, echo, datastore, configs):
235
+ from .user_configs.config_parameters import (
236
+ ConfigValue,
237
+ ) # Prevent circular dependency
238
+
176
239
  global context_proto
177
240
  context_proto = ParameterContext(
178
241
  flow_name=flow_name,
@@ -180,6 +243,7 @@ def set_parameter_context(flow_name, echo, datastore):
180
243
  parameter_name=None,
181
244
  logger=echo,
182
245
  ds_type=datastore.TYPE,
246
+ configs=ConfigValue(dict(configs)),
183
247
  )
184
248
 
185
249
 
@@ -236,7 +300,11 @@ class Parameter(object):
236
300
  ----------
237
301
  name : str
238
302
  User-visible parameter name.
239
- default : str or float or int or bool or `JSONType` or a function.
303
+ default : Union[str, float, int, bool, Dict[str, Any],
304
+ Callable[
305
+ [ParameterContext], Union[str, float, int, bool, Dict[str, Any]]
306
+ ],
307
+ ], optional, default None
240
308
  Default value for the parameter. Use a special `JSONType` class to
241
309
  indicate that the value must be a valid JSON object. A function
242
310
  implies that the parameter corresponds to a *deploy-time parameter*.
@@ -245,15 +313,19 @@ class Parameter(object):
245
313
  If `default` is not specified, define the parameter type. Specify
246
314
  one of `str`, `float`, `int`, `bool`, or `JSONType`. If None, defaults
247
315
  to the type of `default` or `str` if none specified.
248
- help : str, optional
316
+ help : str, optional, default None
249
317
  Help text to show in `run --help`.
250
- required : bool, default False
251
- Require that the user specified a value for the parameter.
252
- `required=True` implies that the `default` is not used.
253
- show_default : bool, default True
254
- If True, show the default value in the help text.
318
+ required : bool, optional, default None
319
+ Require that the user specifies a value for the parameter. Note that if
320
+ a default is provide, the required flag is ignored.
321
+ A value of None is equivalent to False.
322
+ show_default : bool, optional, default None
323
+ If True, show the default value in the help text. A value of None is equivalent
324
+ to True.
255
325
  """
256
326
 
327
+ IS_CONFIG_PARAMETER = False
328
+
257
329
  def __init__(
258
330
  self,
259
331
  name: str,
@@ -264,31 +336,62 @@ class Parameter(object):
264
336
  int,
265
337
  bool,
266
338
  Dict[str, Any],
267
- Callable[[], Union[str, float, int, bool, Dict[str, Any]]],
339
+ Callable[
340
+ [ParameterContext], Union[str, float, int, bool, Dict[str, Any]]
341
+ ],
268
342
  ]
269
343
  ] = None,
270
344
  type: Optional[
271
345
  Union[Type[str], Type[float], Type[int], Type[bool], JSONTypeClass]
272
346
  ] = None,
273
347
  help: Optional[str] = None,
274
- required: bool = False,
275
- show_default: bool = True,
348
+ required: Optional[bool] = None,
349
+ show_default: Optional[bool] = None,
276
350
  **kwargs: Dict[str, Any]
277
351
  ):
278
352
  self.name = name
279
353
  self.kwargs = kwargs
280
- for k, v in {
354
+ self._override_kwargs = {
281
355
  "default": default,
282
356
  "type": type,
283
357
  "help": help,
284
358
  "required": required,
285
359
  "show_default": show_default,
286
- }.items():
287
- if v is not None:
288
- self.kwargs[k] = v
360
+ }
361
+
362
+ def init(self, ignore_errors=False):
363
+ # Prevent circular import
364
+ from .user_configs.config_parameters import (
365
+ resolve_delayed_evaluator,
366
+ unpack_delayed_evaluator,
367
+ )
368
+
369
+ # Resolve any value from configurations
370
+ self.kwargs, _ = unpack_delayed_evaluator(
371
+ self.kwargs, ignore_errors=ignore_errors
372
+ )
373
+ # Do it one item at a time so errors are ignored at that level (as opposed to
374
+ # at the entire kwargs level)
375
+ self.kwargs = {
376
+ k: resolve_delayed_evaluator(v, ignore_errors=ignore_errors, to_dict=True)
377
+ for k, v in self.kwargs.items()
378
+ }
379
+
380
+ # This was the behavior before configs: values specified in args would override
381
+ # stuff in kwargs which is what we implement here as well
382
+ for key, value in self._override_kwargs.items():
383
+ if value is not None:
384
+ self.kwargs[key] = resolve_delayed_evaluator(
385
+ value, ignore_errors=ignore_errors, to_dict=True
386
+ )
387
+ # Set two default values if no-one specified them
388
+ self.kwargs.setdefault("required", False)
389
+ self.kwargs.setdefault("show_default", True)
390
+
391
+ # Continue processing kwargs free of any configuration values :)
289
392
 
290
393
  # TODO: check that the type is one of the supported types
291
- param_type = self.kwargs["type"] = self._get_type(kwargs)
394
+ param_type = self.kwargs["type"] = self._get_type(self.kwargs)
292
395
 
293
396
  reserved_params = [
294
397
  "params",
@@ -313,23 +416,27 @@ class Parameter(object):
313
416
  raise MetaflowException(
314
417
  "Parameter name '%s' is a reserved "
315
418
  "word. Please use a different "
316
- "name for your parameter." % (name)
419
+ "name for your parameter." % (self.name)
317
420
  )
318
421
 
319
422
  # make sure the user is not trying to pass a function in one of the
320
423
  # fields that don't support function-values yet
321
424
  for field in ("show_default", "separator", "required"):
322
- if callable(kwargs.get(field)):
425
+ if callable(self.kwargs.get(field)):
323
426
  raise MetaflowException(
324
427
  "Parameter *%s*: Field '%s' cannot "
325
- "have a function as its value" % (name, field)
428
+ "have a function as its value" % (self.name, field)
326
429
  )
327
430
 
328
431
  # default can be defined as a function
329
432
  default_field = self.kwargs.get("default")
330
433
  if callable(default_field) and not isinstance(default_field, DeployTimeField):
331
434
  self.kwargs["default"] = DeployTimeField(
332
- name, param_type, "default", self.kwargs["default"], return_str=True
435
+ self.name,
436
+ param_type,
437
+ "default",
438
+ self.kwargs["default"],
439
+ return_str=True,
333
440
  )
334
441
 
335
442
  # note that separator doesn't work with DeployTimeFields unless you
@@ -338,7 +445,7 @@ class Parameter(object):
338
445
  if self.separator and not self.is_string_type:
339
446
  raise MetaflowException(
340
447
  "Parameter *%s*: Separator is only allowed "
341
- "for string parameters." % name
448
+ "for string parameters." % self.name
342
449
  )
343
450
 
344
451
  def __repr__(self):
@@ -388,11 +495,25 @@ def add_custom_parameters(deploy_mode=False):
388
495
  # deploy_mode determines whether deploy-time functions should or should
389
496
  # not be evaluated for this command
390
497
  def wrapper(cmd):
498
+ # Save the original params once, if they haven't been saved before.
499
+ if not hasattr(cmd, "original_params"):
500
+ cmd.original_params = list(cmd.params)
501
+
502
+ cmd.has_flow_params = True
391
503
  # Iterate over parameters in reverse order so cmd.params lists options
392
504
  # in the order they are defined in the FlowSpec subclass
505
+ flow_cls = getattr(current_flow, "flow_cls", None)
506
+ if flow_cls is None:
507
+ return cmd
508
+ parameters = [
509
+ p for _, p in flow_cls._get_parameters() if not p.IS_CONFIG_PARAMETER
510
+ ]
393
511
  for arg in parameters[::-1]:
394
512
  kwargs = arg.option_kwargs(deploy_mode)
395
513
  cmd.params.insert(0, click.Option(("--" + arg.name,), **kwargs))
396
514
  return cmd
397
515
 
398
516
  return wrapper
517
+
518
+
519
+ JSONType = JSONTypeClass()
@@ -1,3 +1,5 @@
1
+ import sys
2
+
1
3
  from metaflow.extension_support.plugins import (
2
4
  merge_lists,
3
5
  process_plugins,
@@ -14,8 +16,15 @@ CLIS_DESC = [
14
16
  ("argo-workflows", ".argo.argo_workflows_cli.cli"),
15
17
  ("card", ".cards.card_cli.cli"),
16
18
  ("tag", ".tag_cli.cli"),
19
+ ("spot-metadata", ".kubernetes.spot_metadata_cli.cli"),
20
+ ("logs", ".logs_cli.cli"),
17
21
  ]
18
22
 
23
+ # Add additional commands to the runner here
24
+ # These will be accessed using Runner().<command>()
25
+ RUNNER_CLIS_DESC = []
26
+
27
+
19
28
  from .test_unbounded_foreach_decorator import InternalTestUnboundedForeachInput
20
29
 
21
30
  # Add new step decorators here
@@ -60,35 +69,51 @@ FLOW_DECORATORS_DESC = [
60
69
  ("trigger_on_finish", ".events_decorator.TriggerOnFinishDecorator"),
61
70
  ("pypi_base", ".pypi.pypi_decorator.PyPIFlowDecorator"),
62
71
  ("conda_base", ".pypi.conda_decorator.CondaFlowDecorator"),
72
+ ("exit_hook", ".exit_hook.exit_hook_decorator.ExitHookDecorator"),
63
73
  ]
64
74
 
65
75
  # Add environments here
66
76
  ENVIRONMENTS_DESC = [
67
77
  ("conda", ".pypi.conda_environment.CondaEnvironment"),
68
78
  ("pypi", ".pypi.pypi_environment.PyPIEnvironment"),
79
+ ("uv", ".uv.uv_environment.UVEnvironment"),
69
80
  ]
70
81
 
71
82
  # Add metadata providers here
72
83
  METADATA_PROVIDERS_DESC = [
73
- ("service", ".metadata.service.ServiceMetadataProvider"),
74
- ("local", ".metadata.local.LocalMetadataProvider"),
84
+ ("service", ".metadata_providers.service.ServiceMetadataProvider"),
85
+ ("local", ".metadata_providers.local.LocalMetadataProvider"),
86
+ ("spin", ".metadata_providers.spin.SpinMetadataProvider"),
75
87
  ]
76
88
 
77
89
  # Add datastore here
78
90
  DATASTORES_DESC = [
79
91
  ("local", ".datastores.local_storage.LocalStorage"),
92
+ ("spin", ".datastores.spin_storage.SpinStorage"),
80
93
  ("s3", ".datastores.s3_storage.S3Storage"),
81
94
  ("azure", ".datastores.azure_storage.AzureStorage"),
82
95
  ("gs", ".datastores.gs_storage.GSStorage"),
83
96
  ]
84
97
 
98
+ # Dataclients are used for IncludeFile
99
+ DATACLIENTS_DESC = [
100
+ ("local", ".datatools.Local"),
101
+ ("s3", ".datatools.S3"),
102
+ ("azure", ".azure.includefile_support.Azure"),
103
+ ("gs", ".gcp.includefile_support.GS"),
104
+ ]
105
+
85
106
  # Add non monitoring/logging sidecars here
86
107
  SIDECARS_DESC = [
87
108
  (
88
109
  "save_logs_periodically",
89
110
  "..mflog.save_logs_periodically.SaveLogsPeriodicallySidecar",
90
111
  ),
91
- ("heartbeat", "metaflow.metadata.heartbeat.MetadataHeartBeat"),
112
+ (
113
+ "spot_termination_monitor",
114
+ ".kubernetes.spot_monitor_sidecar.SpotTerminationMonitorSidecar",
115
+ ),
116
+ ("heartbeat", "metaflow.metadata_provider.heartbeat.MetadataHeartBeat"),
92
117
  ]
93
118
 
94
119
  # Add logging sidecars here
@@ -124,16 +149,34 @@ SECRETS_PROVIDERS_DESC = [
124
149
  "gcp-secret-manager",
125
150
  ".gcp.gcp_secret_manager_secrets_provider.GcpSecretManagerSecretsProvider",
126
151
  ),
152
+ (
153
+ "az-key-vault",
154
+ ".azure.azure_secret_manager_secrets_provider.AzureKeyVaultSecretsProvider",
155
+ ),
156
+ ]
157
+
158
+ GCP_CLIENT_PROVIDERS_DESC = [
159
+ ("gcp-default", ".gcp.gs_storage_client_factory.GcpDefaultClientProvider")
127
160
  ]
128
161
 
129
162
  AZURE_CLIENT_PROVIDERS_DESC = [
130
163
  ("azure-default", ".azure.azure_credential.AzureDefaultClientProvider")
131
164
  ]
132
165
 
133
- GCP_CLIENT_PROVIDERS_DESC = [
134
- ("gcp-default", ".gcp.gs_storage_client_factory.GcpDefaultClientProvider")
166
+ DEPLOYER_IMPL_PROVIDERS_DESC = [
167
+ ("argo-workflows", ".argo.argo_workflows_deployer.ArgoWorkflowsDeployer"),
168
+ (
169
+ "step-functions",
170
+ ".aws.step_functions.step_functions_deployer.StepFunctionsDeployer",
171
+ ),
135
172
  ]
136
173
 
174
+ TL_PLUGINS_DESC = [
175
+ ("yaml_parser", ".parsers.yaml_parser"),
176
+ ("requirements_txt_parser", ".pypi.parsers.requirements_txt_parser"),
177
+ ("pyproject_toml_parser", ".pypi.parsers.pyproject_toml_parser"),
178
+ ("conda_environment_yml_parser", ".pypi.parsers.conda_environment_yml_parser"),
179
+ ]
137
180
 
138
181
  process_plugins(globals())
139
182
 
@@ -142,11 +185,24 @@ def get_plugin_cli():
142
185
  return resolve_plugins("cli")
143
186
 
144
187
 
188
+ def get_plugin_cli_path():
189
+ return resolve_plugins("cli", path_only=True)
190
+
191
+
192
+ def get_runner_cli():
193
+ return resolve_plugins("runner_cli")
194
+
195
+
196
+ def get_runner_cli_path():
197
+ return resolve_plugins("runner_cli", path_only=True)
198
+
199
+
145
200
  STEP_DECORATORS = resolve_plugins("step_decorator")
146
201
  FLOW_DECORATORS = resolve_plugins("flow_decorator")
147
202
  ENVIRONMENTS = resolve_plugins("environment")
148
203
  METADATA_PROVIDERS = resolve_plugins("metadata_provider")
149
204
  DATASTORES = resolve_plugins("datastore")
205
+ DATACLIENTS = resolve_plugins("dataclient")
150
206
  SIDECARS = resolve_plugins("sidecar")
151
207
  LOGGING_SIDECARS = resolve_plugins("logging_sidecar")
152
208
  MONITOR_SIDECARS = resolve_plugins("monitor_sidecar")
@@ -159,6 +215,11 @@ SECRETS_PROVIDERS = resolve_plugins("secrets_provider")
159
215
  AZURE_CLIENT_PROVIDERS = resolve_plugins("azure_client_provider")
160
216
  GCP_CLIENT_PROVIDERS = resolve_plugins("gcp_client_provider")
161
217
 
218
+ if sys.version_info >= (3, 7):
219
+ DEPLOYER_IMPL_PROVIDERS = resolve_plugins("deployer_impl_provider")
220
+
221
+ TL_PLUGINS = resolve_plugins("tl_plugin")
222
+
162
223
  from .cards.card_modules import MF_EXTERNAL_CARDS
163
224
 
164
225
  # Cards; due to the way cards were designed, it is harder to make them fit
@@ -183,6 +244,7 @@ from .cards.card_modules.test_cards import (
183
244
  TestTimeoutCard,
184
245
  TestRefreshCard,
185
246
  TestRefreshComponentCard,
247
+ TestImageCard,
186
248
  )
187
249
 
188
250
  CARDS = [
@@ -201,5 +263,12 @@ CARDS = [
201
263
  DefaultCardJSON,
202
264
  TestRefreshCard,
203
265
  TestRefreshComponentCard,
266
+ TestImageCard,
204
267
  ]
205
268
  merge_lists(CARDS, MF_EXTERNAL_CARDS, "type")
269
+
270
+
271
+ def _import_tl_plugins(globals_dict):
272
+
273
+ for name, p in TL_PLUGINS.items():
274
+ globals_dict[name] = p
@@ -32,6 +32,7 @@ from metaflow.metaflow_config import (
32
32
  S3_ENDPOINT_URL,
33
33
  SERVICE_HEADERS,
34
34
  SERVICE_INTERNAL_URL,
35
+ AZURE_KEY_VAULT_PREFIX,
35
36
  )
36
37
 
37
38
  from metaflow.metaflow_config_funcs import config_values
@@ -45,6 +46,7 @@ from metaflow.parameters import (
45
46
  # TODO: Move chevron to _vendor
46
47
  from metaflow.plugins.cards.card_modules import chevron
47
48
  from metaflow.plugins.kubernetes.kubernetes import Kubernetes
49
+ from metaflow.plugins.kubernetes.kube_utils import qos_requests_and_limits
48
50
  from metaflow.plugins.timeout_decorator import get_run_time_limit_for_task
49
51
  from metaflow.util import compress_list, dict_to_cli_options, get_username
50
52
 
@@ -64,6 +66,7 @@ class Airflow(object):
64
66
  name,
65
67
  graph,
66
68
  flow,
69
+ code_package_metadata,
67
70
  code_package_sha,
68
71
  code_package_url,
69
72
  metadata,
@@ -85,6 +88,7 @@ class Airflow(object):
85
88
  self.name = name
86
89
  self.graph = graph
87
90
  self.flow = flow
91
+ self.code_package_metadata = code_package_metadata
88
92
  self.code_package_sha = code_package_sha
89
93
  self.code_package_url = code_package_url
90
94
  self.metadata = metadata
@@ -370,6 +374,7 @@ class Airflow(object):
370
374
  # Technically the "user" is the stakeholder but should these labels be present.
371
375
  }
372
376
  additional_mf_variables = {
377
+ "METAFLOW_CODE_METADATA": self.code_package_metadata,
373
378
  "METAFLOW_CODE_SHA": self.code_package_sha,
374
379
  "METAFLOW_CODE_URL": self.code_package_url,
375
380
  "METAFLOW_CODE_DS": self.flow_datastore.TYPE,
@@ -398,20 +403,23 @@ class Airflow(object):
398
403
  "METAFLOW_CARD_GSROOT": CARD_GSROOT,
399
404
  "METAFLOW_S3_ENDPOINT_URL": S3_ENDPOINT_URL,
400
405
  }
401
- env[
402
- "METAFLOW_AZURE_STORAGE_BLOB_SERVICE_ENDPOINT"
403
- ] = AZURE_STORAGE_BLOB_SERVICE_ENDPOINT
406
+ env["METAFLOW_AZURE_STORAGE_BLOB_SERVICE_ENDPOINT"] = (
407
+ AZURE_STORAGE_BLOB_SERVICE_ENDPOINT
408
+ )
404
409
  env["METAFLOW_DATASTORE_SYSROOT_AZURE"] = DATASTORE_SYSROOT_AZURE
405
410
  env["METAFLOW_CARD_AZUREROOT"] = CARD_AZUREROOT
406
411
  if DEFAULT_SECRETS_BACKEND_TYPE:
407
412
  env["METAFLOW_DEFAULT_SECRETS_BACKEND_TYPE"] = DEFAULT_SECRETS_BACKEND_TYPE
408
413
  if AWS_SECRETS_MANAGER_DEFAULT_REGION:
409
- env[
410
- "METAFLOW_AWS_SECRETS_MANAGER_DEFAULT_REGION"
411
- ] = AWS_SECRETS_MANAGER_DEFAULT_REGION
414
+ env["METAFLOW_AWS_SECRETS_MANAGER_DEFAULT_REGION"] = (
415
+ AWS_SECRETS_MANAGER_DEFAULT_REGION
416
+ )
412
417
  if GCP_SECRET_MANAGER_PREFIX:
413
418
  env["METAFLOW_GCP_SECRET_MANAGER_PREFIX"] = GCP_SECRET_MANAGER_PREFIX
414
419
 
420
+ if AZURE_KEY_VAULT_PREFIX:
421
+ env["METAFLOW_AZURE_KEY_VAULT_PREFIX"] = AZURE_KEY_VAULT_PREFIX
422
+
415
423
  env.update(additional_mf_variables)
416
424
 
417
425
  service_account = (
@@ -424,25 +432,25 @@ class Airflow(object):
424
432
  if k8s_deco.attributes["namespace"] is not None
425
433
  else "default"
426
434
  )
427
-
435
+ qos_requests, qos_limits = qos_requests_and_limits(
436
+ k8s_deco.attributes["qos"],
437
+ k8s_deco.attributes["cpu"],
438
+ k8s_deco.attributes["memory"],
439
+ k8s_deco.attributes["disk"],
440
+ )
428
441
  resources = dict(
429
- requests={
430
- "cpu": k8s_deco.attributes["cpu"],
431
- "memory": "%sM" % str(k8s_deco.attributes["memory"]),
432
- "ephemeral-storage": str(k8s_deco.attributes["disk"]),
433
- }
442
+ requests=qos_requests,
443
+ limits={
444
+ **qos_limits,
445
+ **{
446
+ "%s.com/gpu".lower()
447
+ % k8s_deco.attributes["gpu_vendor"]: str(k8s_deco.attributes["gpu"])
448
+ for k in [0]
449
+ # Don't set GPU limits if gpu isn't specified.
450
+ if k8s_deco.attributes["gpu"] is not None
451
+ },
452
+ },
434
453
  )
435
- if k8s_deco.attributes["gpu"] is not None:
436
- resources.update(
437
- dict(
438
- limits={
439
- "%s.com/gpu".lower()
440
- % k8s_deco.attributes["gpu_vendor"]: str(
441
- k8s_deco.attributes["gpu"]
442
- )
443
- }
444
- )
445
- )
446
454
 
447
455
  annotations = {
448
456
  "metaflow/production_token": self.production_token,
@@ -471,6 +479,7 @@ class Airflow(object):
471
479
  node.name,
472
480
  AIRFLOW_MACROS.create_task_id(self.contains_foreach),
473
481
  AIRFLOW_MACROS.ATTEMPT,
482
+ code_package_metadata=self.code_package_metadata,
474
483
  code_package_url=self.code_package_url,
475
484
  step_cmds=self._step_cli(
476
485
  node, input_paths, self.code_package_url, user_code_retries
@@ -529,13 +538,13 @@ class Airflow(object):
529
538
  "with": [
530
539
  decorator.make_decorator_spec()
531
540
  for decorator in node.decorators
532
- if not decorator.statically_defined
541
+ if not decorator.statically_defined and decorator.inserted_by is None
533
542
  ]
534
543
  }
535
544
  # FlowDecorators can define their own top-level options. They are
536
545
  # responsible for adding their own top-level options and values through
537
546
  # the get_top_level_options() hook. See similar logic in runtime.py.
538
- for deco in flow_decorators():
547
+ for deco in flow_decorators(self.flow):
539
548
  top_opts_dict.update(deco.get_top_level_options())
540
549
 
541
550
  top_opts = list(dict_to_cli_options(top_opts_dict))
@@ -649,6 +658,12 @@ class Airflow(object):
649
658
  "to Airflow is not supported currently."
650
659
  )
651
660
 
661
+ if self.flow._flow_decorators.get("exit_hook"):
662
+ raise AirflowException(
663
+ "Deploying flows with the @exit_hook decorator "
664
+ "to Airflow is not currently supported."
665
+ )
666
+
652
667
  # Visit every node of the flow and recursively build the state machine.
653
668
  def _visit(node, workflow, exit_node=None):
654
669
  kube_deco = dict(