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
@@ -0,0 +1,650 @@
1
+ import asyncio
2
+ import os
3
+ import time
4
+ import shutil
5
+ import signal
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ import threading
10
+ from typing import Callable, Dict, Iterator, List, Optional, Tuple
11
+
12
+ from metaflow.packaging_sys import MetaflowCodeContent
13
+ from metaflow.util import get_metaflow_root
14
+ from .utils import check_process_exited
15
+
16
+
17
+ def kill_processes_and_descendants(pids: List[str], termination_timeout: float):
18
+ # TODO: there's a race condition that new descendants might
19
+ # spawn b/w the invocations of 'pkill' and 'kill'.
20
+ # Needs to be fixed in future.
21
+ try:
22
+ subprocess.check_call(["pkill", "-TERM", "-P", *pids])
23
+ subprocess.check_call(["kill", "-TERM", *pids])
24
+ except subprocess.CalledProcessError:
25
+ pass
26
+
27
+ time.sleep(termination_timeout)
28
+
29
+ try:
30
+ subprocess.check_call(["pkill", "-KILL", "-P", *pids])
31
+ subprocess.check_call(["kill", "-KILL", *pids])
32
+ except subprocess.CalledProcessError:
33
+ pass
34
+
35
+
36
+ async def async_kill_processes_and_descendants(
37
+ pids: List[str], termination_timeout: float
38
+ ):
39
+ # TODO: there's a race condition that new descendants might
40
+ # spawn b/w the invocations of 'pkill' and 'kill'.
41
+ # Needs to be fixed in future.
42
+ try:
43
+ sub_term = await asyncio.create_subprocess_exec("pkill", "-TERM", "-P", *pids)
44
+ await sub_term.wait()
45
+ except Exception:
46
+ pass
47
+
48
+ try:
49
+ main_term = await asyncio.create_subprocess_exec("kill", "-TERM", *pids)
50
+ await main_term.wait()
51
+ except Exception:
52
+ pass
53
+
54
+ await asyncio.sleep(termination_timeout)
55
+
56
+ try:
57
+ sub_kill = await asyncio.create_subprocess_exec("pkill", "-KILL", "-P", *pids)
58
+ await sub_kill.wait()
59
+ except Exception:
60
+ pass
61
+
62
+ try:
63
+ main_kill = await asyncio.create_subprocess_exec("kill", "-KILL", *pids)
64
+ await main_kill.wait()
65
+ except Exception:
66
+ pass
67
+
68
+
69
+ class LogReadTimeoutError(Exception):
70
+ """Exception raised when reading logs times out."""
71
+
72
+
73
+ class SubprocessManager(object):
74
+ """
75
+ A manager for subprocesses. The subprocess manager manages one or more
76
+ CommandManager objects, each of which manages an individual subprocess.
77
+ """
78
+
79
+ def __init__(self):
80
+ self.commands: Dict[int, CommandManager] = {}
81
+
82
+ try:
83
+ try:
84
+ loop = asyncio.get_running_loop()
85
+ loop.add_signal_handler(
86
+ signal.SIGINT,
87
+ lambda: asyncio.create_task(self._async_handle_sigint()),
88
+ )
89
+ except RuntimeError:
90
+ signal.signal(signal.SIGINT, self._handle_sigint)
91
+ except ValueError:
92
+ sys.stderr.write(
93
+ "Warning: Unable to set signal handlers in non-main thread. "
94
+ "Interrupt handling will be limited.\n"
95
+ )
96
+
97
+ async def _async_handle_sigint(self):
98
+ pids = [
99
+ str(command.process.pid)
100
+ for command in self.commands.values()
101
+ if command.process and not check_process_exited(command)
102
+ ]
103
+ if pids:
104
+ await async_kill_processes_and_descendants(pids, termination_timeout=2)
105
+
106
+ def _handle_sigint(self, signum, frame):
107
+ pids = [
108
+ str(command.process.pid)
109
+ for command in self.commands.values()
110
+ if command.process and not check_process_exited(command)
111
+ ]
112
+ if pids:
113
+ kill_processes_and_descendants(pids, termination_timeout=2)
114
+
115
+ async def __aenter__(self) -> "SubprocessManager":
116
+ return self
117
+
118
+ async def __aexit__(self, exc_type, exc_value, traceback):
119
+ self.cleanup()
120
+
121
+ def run_command(
122
+ self,
123
+ command: List[str],
124
+ env: Optional[Dict[str, str]] = None,
125
+ cwd: Optional[str] = None,
126
+ show_output: bool = False,
127
+ ) -> int:
128
+ """
129
+ Run a command synchronously and return its process ID.
130
+
131
+ Note: in no case does this wait for the process to *finish*. Use sync_wait()
132
+ to wait for the command to finish.
133
+
134
+ Parameters
135
+ ----------
136
+ command : List[str]
137
+ The command to run in List form.
138
+ env : Optional[Dict[str, str]], default None
139
+ Environment variables to set for the subprocess; if not specified,
140
+ the current enviornment variables are used.
141
+ cwd : Optional[str], default None
142
+ The directory to run the subprocess in; if not specified, the current
143
+ directory is used.
144
+ show_output : bool, default False
145
+ Suppress the 'stdout' and 'stderr' to the console by default.
146
+ They can be accessed later by reading the files present in the
147
+ CommandManager object:
148
+ - command_obj.log_files["stdout"]
149
+ - command_obj.log_files["stderr"]
150
+ Returns
151
+ -------
152
+ int
153
+ The process ID of the subprocess.
154
+ """
155
+ env = env or {}
156
+ installed_root = os.environ.get("METAFLOW_EXTRACTED_ROOT", get_metaflow_root())
157
+
158
+ for k, v in MetaflowCodeContent.get_env_vars_for_packaged_metaflow(
159
+ installed_root
160
+ ).items():
161
+ if k.endswith(":"):
162
+ # Override
163
+ env[k[:-1]] = v
164
+ elif k in env:
165
+ env[k] = "%s:%s" % (v, env[k])
166
+ else:
167
+ env[k] = v
168
+
169
+ command_obj = CommandManager(command, env, cwd)
170
+ pid = command_obj.run(show_output=show_output)
171
+ self.commands[pid] = command_obj
172
+ return pid
173
+
174
+ async def async_run_command(
175
+ self,
176
+ command: List[str],
177
+ env: Optional[Dict[str, str]] = None,
178
+ cwd: Optional[str] = None,
179
+ ) -> int:
180
+ """
181
+ Run a command asynchronously and return its process ID.
182
+
183
+ Parameters
184
+ ----------
185
+ command : List[str]
186
+ The command to run in List form.
187
+ env : Optional[Dict[str, str]], default None
188
+ Environment variables to set for the subprocess; if not specified,
189
+ the current enviornment variables are used.
190
+ cwd : Optional[str], default None
191
+ The directory to run the subprocess in; if not specified, the current
192
+ directory is used.
193
+
194
+ Returns
195
+ -------
196
+ int
197
+ The process ID of the subprocess.
198
+ """
199
+ env = env or {}
200
+ if "PYTHONPATH" in env:
201
+ env["PYTHONPATH"] = "%s:%s" % (get_metaflow_root(), env["PYTHONPATH"])
202
+ else:
203
+ env["PYTHONPATH"] = get_metaflow_root()
204
+
205
+ command_obj = CommandManager(command, env, cwd)
206
+ pid = await command_obj.async_run()
207
+ self.commands[pid] = command_obj
208
+ return pid
209
+
210
+ def get(self, pid: int) -> Optional["CommandManager"]:
211
+ """
212
+ Get one of the CommandManager managed by this SubprocessManager.
213
+
214
+ Parameters
215
+ ----------
216
+ pid : int
217
+ The process ID of the subprocess (returned by run_command or async_run_command).
218
+
219
+ Returns
220
+ -------
221
+ Optional[CommandManager]
222
+ The CommandManager object for the given process ID, or None if not found.
223
+ """
224
+ return self.commands.get(pid, None)
225
+
226
+ def cleanup(self) -> None:
227
+ """Clean up log files for all running subprocesses."""
228
+
229
+ for v in self.commands.values():
230
+ v.cleanup()
231
+
232
+
233
+ class CommandManager(object):
234
+ """A manager for an individual subprocess."""
235
+
236
+ def __init__(
237
+ self,
238
+ command: List[str],
239
+ env: Optional[Dict[str, str]] = None,
240
+ cwd: Optional[str] = None,
241
+ ):
242
+ """
243
+ Create a new CommandManager object.
244
+ This does not run the process itself but sets it up.
245
+
246
+ Parameters
247
+ ----------
248
+ command : List[str]
249
+ The command to run in List form.
250
+ env : Optional[Dict[str, str]], default None
251
+ Environment variables to set for the subprocess; if not specified,
252
+ the current enviornment variables are used.
253
+ cwd : Optional[str], default None
254
+ The directory to run the subprocess in; if not specified, the current
255
+ directory is used.
256
+ """
257
+ self.command = command
258
+
259
+ self.env = env if env is not None else os.environ.copy()
260
+ self.cwd = cwd or os.getcwd()
261
+
262
+ self.process = None
263
+ self.stdout_thread = None
264
+ self.stderr_thread = None
265
+ self.run_called: bool = False
266
+ self.timeout: bool = False
267
+ self.log_files: Dict[str, str] = {}
268
+
269
+ async def __aenter__(self) -> "CommandManager":
270
+ return self
271
+
272
+ async def __aexit__(self, exc_type, exc_value, traceback):
273
+ self.cleanup()
274
+
275
+ async def wait(
276
+ self, timeout: Optional[float] = None, stream: Optional[str] = None
277
+ ) -> None:
278
+ """
279
+ Wait for the subprocess to finish, optionally with a timeout
280
+ and optionally streaming its output.
281
+
282
+ You can only call `wait` if `async_run` has already been called.
283
+
284
+ Parameters
285
+ ----------
286
+ timeout : Optional[float], default None
287
+ The maximum time to wait for the subprocess to finish.
288
+ If the timeout is reached, the subprocess is killed.
289
+ stream : Optional[str], default None
290
+ If specified, the specified stream is printed to stdout. `stream` can
291
+ be one of `stdout` or `stderr`.
292
+ """
293
+
294
+ if not self.run_called:
295
+ raise RuntimeError("No command run yet to wait for...")
296
+
297
+ if timeout is None:
298
+ if stream is None:
299
+ await self.process.wait()
300
+ else:
301
+ await self.emit_logs(stream)
302
+ else:
303
+ try:
304
+ if stream is None:
305
+ await asyncio.wait_for(self.process.wait(), timeout)
306
+ else:
307
+ await asyncio.wait_for(self.emit_logs(stream), timeout)
308
+ except asyncio.TimeoutError:
309
+ self.timeout = True
310
+ command_string = " ".join(self.command)
311
+ self.kill(termination_timeout=2)
312
+ print(
313
+ "Timeout: The process (PID %d; command: '%s') did not complete "
314
+ "within %s seconds." % (self.process.pid, command_string, timeout)
315
+ )
316
+
317
+ def sync_wait(self):
318
+ if not self.run_called:
319
+ raise RuntimeError("No command run yet to wait for...")
320
+
321
+ self.process.wait()
322
+ self.stdout_thread.join()
323
+ self.stderr_thread.join()
324
+
325
+ def run(self, show_output: bool = False):
326
+ """
327
+ Run the subprocess synchronously. This can only be called once.
328
+
329
+ This also waits on the process implicitly.
330
+
331
+ Parameters
332
+ ----------
333
+ show_output : bool, default False
334
+ Suppress the 'stdout' and 'stderr' to the console by default.
335
+ They can be accessed later by reading the files present in:
336
+ - self.log_files["stdout"]
337
+ - self.log_files["stderr"]
338
+ """
339
+
340
+ if not self.run_called:
341
+ self.temp_dir = tempfile.mkdtemp()
342
+ stdout_logfile = os.path.join(self.temp_dir, "stdout.log")
343
+ stderr_logfile = os.path.join(self.temp_dir, "stderr.log")
344
+
345
+ def stream_to_stdout_and_file(pipe, log_file):
346
+ with open(log_file, "w") as file:
347
+ for line in iter(pipe.readline, ""):
348
+ if show_output:
349
+ sys.stdout.write(line)
350
+ file.write(line)
351
+ pipe.close()
352
+
353
+ try:
354
+ self.process = subprocess.Popen(
355
+ self.command,
356
+ cwd=self.cwd,
357
+ env=self.env,
358
+ stdout=subprocess.PIPE,
359
+ stderr=subprocess.PIPE,
360
+ bufsize=1,
361
+ universal_newlines=True,
362
+ )
363
+
364
+ self.log_files["stdout"] = stdout_logfile
365
+ self.log_files["stderr"] = stderr_logfile
366
+
367
+ self.run_called = True
368
+
369
+ self.stdout_thread = threading.Thread(
370
+ target=stream_to_stdout_and_file,
371
+ args=(self.process.stdout, stdout_logfile),
372
+ )
373
+ self.stderr_thread = threading.Thread(
374
+ target=stream_to_stdout_and_file,
375
+ args=(self.process.stderr, stderr_logfile),
376
+ )
377
+
378
+ self.stdout_thread.start()
379
+ self.stderr_thread.start()
380
+
381
+ return self.process.pid
382
+ except Exception as e:
383
+ print("Error starting subprocess: %s" % e)
384
+ self.cleanup()
385
+ else:
386
+ command_string = " ".join(self.command)
387
+ print(
388
+ "Command '%s' has already been called. Please create another "
389
+ "CommandManager object." % command_string
390
+ )
391
+
392
+ async def async_run(self):
393
+ """
394
+ Run the subprocess asynchronously. This can only be called once.
395
+
396
+ Once this is called, you can then wait on the process (using `wait`), stream
397
+ logs (using `stream_logs`) or kill it (using `kill`).
398
+ """
399
+
400
+ if not self.run_called:
401
+ self.temp_dir = tempfile.mkdtemp()
402
+ stdout_logfile = os.path.join(self.temp_dir, "stdout.log")
403
+ stderr_logfile = os.path.join(self.temp_dir, "stderr.log")
404
+
405
+ try:
406
+ # returns when process has been started,
407
+ # not when it is finished...
408
+ self.process = await asyncio.create_subprocess_exec(
409
+ *self.command,
410
+ cwd=self.cwd,
411
+ env=self.env,
412
+ stdout=open(stdout_logfile, "w", encoding="utf-8"),
413
+ stderr=open(stderr_logfile, "w", encoding="utf-8"),
414
+ )
415
+
416
+ self.log_files["stdout"] = stdout_logfile
417
+ self.log_files["stderr"] = stderr_logfile
418
+
419
+ self.run_called = True
420
+ return self.process.pid
421
+ except Exception as e:
422
+ print("Error starting subprocess: %s" % e)
423
+ self.cleanup()
424
+ else:
425
+ command_string = " ".join(self.command)
426
+ print(
427
+ "Command '%s' has already been called. Please create another "
428
+ "CommandManager object." % command_string
429
+ )
430
+
431
+ async def stream_log(
432
+ self,
433
+ stream: str,
434
+ position: Optional[int] = None,
435
+ timeout_per_line: Optional[float] = None,
436
+ log_write_delay: float = 0.01,
437
+ ) -> Iterator[Tuple[int, str]]:
438
+ """
439
+ Stream logs from the subprocess line by line.
440
+
441
+ Parameters
442
+ ----------
443
+ stream : str
444
+ The stream to stream logs from. Can be one of "stdout" or "stderr".
445
+ position : Optional[int], default None
446
+ The position in the log file to start streaming from. If None, it starts
447
+ from the beginning of the log file. This allows resuming streaming from
448
+ a previously known position
449
+ timeout_per_line : Optional[float], default None
450
+ The time to wait for a line to be read from the log file. If None, it
451
+ waits indefinitely. If the timeout is reached, a LogReadTimeoutError
452
+ is raised. Note that this timeout is *per line* and not cumulative so this
453
+ function may take significantly more time than `timeout_per_line`
454
+ log_write_delay : float, default 0.01
455
+ Improves the probability of getting whole lines. This setting is for
456
+ advanced use cases.
457
+
458
+ Yields
459
+ ------
460
+ Tuple[int, str]
461
+ A tuple containing the position in the log file and the line read. The
462
+ position returned can be used to feed into another `stream_logs` call
463
+ for example.
464
+ """
465
+
466
+ if not self.run_called:
467
+ raise RuntimeError("No command run yet to get the logs for...")
468
+
469
+ if stream not in self.log_files:
470
+ raise ValueError(
471
+ "No log file found for '%s', valid values are: %s"
472
+ % (stream, ", ".join(self.log_files.keys()))
473
+ )
474
+
475
+ log_file = self.log_files[stream]
476
+
477
+ with open(log_file, mode="r", encoding="utf-8") as f:
478
+ if position is not None:
479
+ f.seek(position)
480
+
481
+ while True:
482
+ # wait for a small time for complete lines to be written to the file
483
+ # else, there's a possibility that a line may not be completely
484
+ # written when attempting to read it.
485
+ # This is not a problem, but improves readability.
486
+ await asyncio.sleep(log_write_delay)
487
+
488
+ try:
489
+ if timeout_per_line is None:
490
+ line = f.readline()
491
+ else:
492
+ line = await asyncio.wait_for(f.readline(), timeout_per_line)
493
+ except asyncio.TimeoutError as e:
494
+ raise LogReadTimeoutError(
495
+ "Timeout while reading a line from the log file for the "
496
+ "stream: %s" % stream
497
+ ) from e
498
+
499
+ # when we encounter an empty line
500
+ if not line:
501
+ # either the process has terminated, in which case we want to break
502
+ # and stop the reading process of the log file since no more logs
503
+ # will be written to it
504
+ if self.process.returncode is not None:
505
+ break
506
+ # or the process is still running and more logs could be written to
507
+ # the file, in which case we continue reading the log file
508
+ else:
509
+ continue
510
+
511
+ position = f.tell()
512
+ yield position, line.rstrip()
513
+
514
+ async def emit_logs(
515
+ self, stream: str = "stdout", custom_logger: Callable[..., None] = print
516
+ ):
517
+ """
518
+ Helper function that can easily emit all the logs for a given stream.
519
+
520
+ This function will only terminate when all the log has been printed.
521
+
522
+ Parameters
523
+ ----------
524
+ stream : str, default "stdout"
525
+ The stream to emit logs for. Can be one of "stdout" or "stderr".
526
+ custom_logger : Callable[..., None], default print
527
+ A custom logger function that takes in a string and "emits" it. By default,
528
+ the log is printed to stdout.
529
+ """
530
+
531
+ async for _, line in self.stream_log(stream):
532
+ custom_logger(line)
533
+
534
+ def cleanup(self):
535
+ """Clean up log files for a running subprocesses."""
536
+
537
+ if self.run_called:
538
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
539
+
540
+ def kill(self, termination_timeout: float = 2):
541
+ """
542
+ Kill the subprocess and its descendants.
543
+
544
+ Parameters
545
+ ----------
546
+ termination_timeout : float, default 2
547
+ The time to wait after sending a SIGTERM to the process and its descendants
548
+ before sending a SIGKILL.
549
+ """
550
+
551
+ if self.process is not None:
552
+ kill_processes_and_descendants([str(self.process.pid)], termination_timeout)
553
+ else:
554
+ print("No process to kill.")
555
+
556
+
557
+ async def main():
558
+ flow_file = "../try.py"
559
+ from metaflow.cli import start
560
+ from metaflow.runner.click_api import MetaflowAPI
561
+
562
+ api = MetaflowAPI.from_cli(flow_file, start)
563
+ command = api().run(alpha=5)
564
+ cmd = [sys.executable, *command]
565
+
566
+ async with SubprocessManager() as spm:
567
+ # returns immediately
568
+ pid = await spm.async_run_command(cmd)
569
+ command_obj = spm.get(pid)
570
+
571
+ print(pid)
572
+
573
+ # this is None since the process has not completed yet
574
+ print(command_obj.process.returncode)
575
+
576
+ # wait / do some other processing while the process runs in background.
577
+ # if the process finishes before this sleep period, the calls to `wait`
578
+ # below are instantaneous since it has already ended..
579
+ # time.sleep(10)
580
+
581
+ # wait for process to finish
582
+ await command_obj.wait()
583
+
584
+ # wait for process to finish with a timeout, kill if timeout expires before completion
585
+ await command_obj.wait(timeout=2)
586
+
587
+ # wait for process to finish while streaming logs
588
+ await command_obj.wait(stream="stdout")
589
+
590
+ # wait for process to finish with a timeout while streaming logs
591
+ await command_obj.wait(stream="stdout", timeout=3)
592
+
593
+ # stream logs line by line and check for existence of a string, noting down the position
594
+ interesting_position = 0
595
+ async for position, line in command_obj.stream_log(stream="stdout"):
596
+ print(line)
597
+ if "alpha is" in line:
598
+ interesting_position = position
599
+ break
600
+
601
+ print("ended streaming at: %s" % interesting_position)
602
+
603
+ # wait / do some other processing while the process runs in background
604
+ # if the process finishes before this sleep period, the streaming of logs
605
+ # below are instantaneous since it has already ended..
606
+ # time.sleep(10)
607
+
608
+ # this blocks till the process completes unless we uncomment the `time.sleep` above..
609
+ print(
610
+ "resuming streaming from: %s while process is still running..."
611
+ % interesting_position
612
+ )
613
+ async for position, line in command_obj.stream_log(
614
+ stream="stdout", position=interesting_position
615
+ ):
616
+ print(line)
617
+
618
+ # this will be instantaneous since the process has finished and we just read from the log file
619
+ print("process has ended by now... streaming again from scratch..")
620
+ async for position, line in command_obj.stream_log(stream="stdout"):
621
+ print(line)
622
+
623
+ # this will be instantaneous since the process has finished and we just read from the log file
624
+ print(
625
+ "process has ended by now... streaming again but from position of choice.."
626
+ )
627
+ async for position, line in command_obj.stream_log(
628
+ stream="stdout", position=interesting_position
629
+ ):
630
+ print(line)
631
+
632
+ # two parallel streams for stdout
633
+ tasks = [
634
+ command_obj.emit_logs(
635
+ stream="stdout", custom_logger=lambda x: print("[STREAM A]: %s" % x)
636
+ ),
637
+ # this can be another 'command_obj' too, in which case
638
+ # we stream logs from 2 different subprocesses in parallel :)
639
+ command_obj.emit_logs(
640
+ stream="stdout", custom_logger=lambda x: print("[STREAM B]: %s" % x)
641
+ ),
642
+ ]
643
+ await asyncio.gather(*tasks)
644
+
645
+ # get the location of log files..
646
+ print(command_obj.log_files)
647
+
648
+
649
+ if __name__ == "__main__":
650
+ asyncio.run(main())