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,69 @@
1
+ from metaflow._vendor import click
2
+ from datetime import datetime, timezone
3
+ from metaflow.tagging_util import validate_tags
4
+ from metaflow.metadata_provider import MetaDatum
5
+
6
+
7
+ @click.group()
8
+ def cli():
9
+ pass
10
+
11
+
12
+ @cli.group(help="Commands related to spot metadata.")
13
+ def spot_metadata():
14
+ pass
15
+
16
+
17
+ @spot_metadata.command(help="Record spot termination metadata for a task.")
18
+ @click.option(
19
+ "--run-id",
20
+ required=True,
21
+ help="Run ID for which metadata is to be recorded.",
22
+ )
23
+ @click.option(
24
+ "--step-name",
25
+ required=True,
26
+ help="Step Name for which metadata is to be recorded.",
27
+ )
28
+ @click.option(
29
+ "--task-id",
30
+ required=True,
31
+ help="Task ID for which metadata is to be recorded.",
32
+ )
33
+ @click.option(
34
+ "--termination-notice-time",
35
+ required=True,
36
+ help="Spot termination notice time.",
37
+ )
38
+ @click.option(
39
+ "--tag",
40
+ "tags",
41
+ multiple=True,
42
+ required=False,
43
+ default=None,
44
+ help="List of tags.",
45
+ )
46
+ @click.pass_obj
47
+ def record(obj, run_id, step_name, task_id, termination_notice_time, tags=None):
48
+ validate_tags(tags)
49
+
50
+ tag_list = list(tags) if tags else []
51
+
52
+ entries = [
53
+ MetaDatum(
54
+ field="spot-termination-received-at",
55
+ value=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
56
+ type="spot-termination-received-at",
57
+ tags=tag_list,
58
+ ),
59
+ MetaDatum(
60
+ field="spot-termination-time",
61
+ value=termination_notice_time,
62
+ type="spot-termination-time",
63
+ tags=tag_list,
64
+ ),
65
+ ]
66
+
67
+ obj.metadata.register_metadata(
68
+ run_id=run_id, step_name=step_name, task_id=task_id, metadata=entries
69
+ )
@@ -0,0 +1,109 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import signal
5
+ import requests
6
+ import subprocess
7
+ from multiprocessing import Process
8
+ from datetime import datetime, timezone
9
+ from metaflow.sidecar import MessageTypes
10
+
11
+
12
+ class SpotTerminationMonitorSidecar(object):
13
+ EC2_TYPE_URL = "http://169.254.169.254/latest/meta-data/instance-life-cycle"
14
+ METADATA_URL = "http://169.254.169.254/latest/meta-data/spot/termination-time"
15
+ TOKEN_URL = "http://169.254.169.254/latest/api/token"
16
+ POLL_INTERVAL = 5 # seconds
17
+
18
+ def __init__(self):
19
+ self.is_alive = True
20
+ self._process = None
21
+ self._token = None
22
+ self._token_expiry = 0
23
+
24
+ if self._is_aws_spot_instance():
25
+ self._process = Process(target=self._monitor_loop)
26
+ self._process.start()
27
+
28
+ def process_message(self, msg):
29
+ if msg.msg_type == MessageTypes.SHUTDOWN:
30
+ self.is_alive = False
31
+ if self._process:
32
+ self._process.terminate()
33
+
34
+ @classmethod
35
+ def get_worker(cls):
36
+ return cls
37
+
38
+ def _get_imds_token(self):
39
+ current_time = time.time()
40
+ if current_time >= self._token_expiry - 60: # Refresh 60s before expiry
41
+ try:
42
+ response = requests.put(
43
+ url=self.TOKEN_URL,
44
+ headers={"X-aws-ec2-metadata-token-ttl-seconds": "300"},
45
+ timeout=1,
46
+ )
47
+ if response.status_code == 200:
48
+ self._token = response.text
49
+ self._token_expiry = current_time + 240 # Slightly less than TTL
50
+ except requests.exceptions.RequestException:
51
+ pass
52
+ return self._token
53
+
54
+ def _make_ec2_request(self, url, timeout):
55
+ token = self._get_imds_token()
56
+ headers = {"X-aws-ec2-metadata-token": token} if token else {}
57
+ response = requests.get(url=url, headers=headers, timeout=timeout)
58
+ return response
59
+
60
+ def _is_aws_spot_instance(self):
61
+ try:
62
+ response = self._make_ec2_request(url=self.EC2_TYPE_URL, timeout=1)
63
+ return response.status_code == 200 and response.text == "spot"
64
+ except (requests.exceptions.RequestException, requests.exceptions.Timeout):
65
+ return False
66
+
67
+ def _monitor_loop(self):
68
+ while self.is_alive:
69
+ try:
70
+ response = self._make_ec2_request(url=self.METADATA_URL, timeout=1)
71
+ if response.status_code == 200:
72
+ termination_time = response.text
73
+ self._emit_termination_metadata(termination_time)
74
+ os.kill(os.getppid(), signal.SIGTERM)
75
+ break
76
+ except (requests.exceptions.RequestException, requests.exceptions.Timeout):
77
+ pass
78
+ time.sleep(self.POLL_INTERVAL)
79
+
80
+ def _emit_termination_metadata(self, termination_time):
81
+ flow_filename = os.getenv("METAFLOW_FLOW_FILENAME")
82
+ pathspec = os.getenv("MF_PATHSPEC")
83
+ _, run_id, step_name, task_id = pathspec.split("/")
84
+ retry_count = os.getenv("MF_ATTEMPT")
85
+
86
+ with open("/tmp/spot_termination_notice", "w") as fp:
87
+ fp.write(termination_time)
88
+
89
+ command = [
90
+ sys.executable,
91
+ f"/metaflow/{flow_filename}",
92
+ "spot-metadata",
93
+ "record",
94
+ "--run-id",
95
+ run_id,
96
+ "--step-name",
97
+ step_name,
98
+ "--task-id",
99
+ task_id,
100
+ "--termination-notice-time",
101
+ termination_time,
102
+ "--tag",
103
+ "attempt_id:{}".format(retry_count),
104
+ ]
105
+
106
+ result = subprocess.run(command, capture_output=True, text=True)
107
+
108
+ if result.returncode != 0:
109
+ print(f"Failed to record spot termination metadata: {result.stderr}")
@@ -0,0 +1,359 @@
1
+ from metaflow._vendor import click
2
+ from metaflow.cli import LOGGER_TIMESTAMP
3
+
4
+ from ..exception import CommandException
5
+ from ..datastore import TaskDataStoreSet, TaskDataStore
6
+
7
+
8
+ from ..mflog import mflog, LOG_SOURCES
9
+
10
+
11
+ # main motivation from https://github.com/pallets/click/issues/430
12
+ # in order to support a default command being called for a Click group.
13
+ #
14
+ # NOTE: We need this in order to not introduce breaking changes to existing CLI, as we wanted to
15
+ # nest both existing `logs` and the new `logs scrub` under a shared group, but `logs` already has
16
+ # a well defined behavior of showing the logs.
17
+ class CustomGroup(click.Group):
18
+ def __init__(self, name=None, commands=None, default_cmd=None, **attrs):
19
+ super(CustomGroup, self).__init__(name, commands, **attrs)
20
+ self.default_cmd = default_cmd
21
+
22
+ def get_command(self, ctx, cmd_name):
23
+ if cmd_name not in self.list_commands(ctx):
24
+ # input from the CLI does not match a command, so we pass that
25
+ # as the args to the default command instead.
26
+ ctx.passed_cmd = cmd_name
27
+ cmd_name = self.default_cmd
28
+ return super(CustomGroup, self).get_command(ctx, cmd_name)
29
+
30
+ def parse_args(self, ctx, args):
31
+ # We first try to parse args as is, to determine whether we need to fall back to the default commmand
32
+ # if any options are supplied, the parse will fail, as the group does not support the options.
33
+ # In this case we fallback to the default command, inserting that as the first arg and parsing again.
34
+ # copy args as trying to parse will destroy them.
35
+ original_args = list(args)
36
+ try:
37
+ super().parse_args(ctx, args)
38
+ args_parseable = True
39
+ except Exception:
40
+ args_parseable = False
41
+ if not args or not args_parseable:
42
+ original_args.insert(0, self.default_cmd)
43
+ return super().parse_args(ctx, original_args)
44
+
45
+ def resolve_command(self, ctx, args):
46
+ cmd_name, cmd_obj, args = super(CustomGroup, self).resolve_command(ctx, args)
47
+ passed_cmd = getattr(ctx, "passed_cmd", None)
48
+ if passed_cmd is not None:
49
+ args.insert(0, passed_cmd)
50
+
51
+ return cmd_name, cmd_obj, args
52
+
53
+ def format_commands(self, ctx, formatter):
54
+ formatter = CustomFormatter(self.default_cmd, formatter)
55
+ return super(CustomGroup, self).format_commands(ctx, formatter)
56
+
57
+
58
+ class CustomFormatter:
59
+ def __init__(self, default_cmd, original_formatter) -> None:
60
+ self.default_cmd = default_cmd
61
+ self.formatter = original_formatter
62
+
63
+ def __getattr__(self, name):
64
+ return getattr(self.formatter, name)
65
+
66
+ def write_dl(self, rows):
67
+ def _format(dup):
68
+ cmd, help = dup
69
+ if cmd == self.default_cmd:
70
+ cmd = cmd + " [Default]"
71
+ return (cmd, help)
72
+
73
+ rows = [_format(dup) for dup in rows]
74
+
75
+ return self.formatter.write_dl(rows)
76
+
77
+
78
+ @click.group()
79
+ def cli():
80
+ pass
81
+
82
+
83
+ @cli.group(cls=CustomGroup, help="Commands related to logs", default_cmd="show")
84
+ @click.pass_context
85
+ def logs(ctx):
86
+ # the logger is configured in cli.py
87
+ global echo
88
+ echo = ctx.obj.echo
89
+
90
+
91
+ @logs.command(
92
+ help="Show stdout/stderr produced by a task or all tasks in a step. "
93
+ "The format for input-path is either <run_id>/<step_name> or "
94
+ "<run_id>/<step_name>/<task_id>."
95
+ )
96
+ @click.argument("input-path")
97
+ @click.option(
98
+ "--stdout/--no-stdout",
99
+ default=False,
100
+ show_default=True,
101
+ help="Show stdout of the task.",
102
+ )
103
+ @click.option(
104
+ "--stderr/--no-stderr",
105
+ default=False,
106
+ show_default=True,
107
+ help="Show stderr of the task.",
108
+ )
109
+ @click.option(
110
+ "--both/--no-both",
111
+ default=True,
112
+ show_default=True,
113
+ help="Show both stdout and stderr of the task.",
114
+ )
115
+ @click.option(
116
+ "--timestamps/--no-timestamps",
117
+ default=False,
118
+ show_default=True,
119
+ help="Show timestamps.",
120
+ )
121
+ @click.option(
122
+ "--attempt",
123
+ default=None,
124
+ type=int,
125
+ show_default=False,
126
+ help="Attempt number of a task to show, defaults to the latest attempt.",
127
+ )
128
+ @click.pass_obj
129
+ def show(
130
+ obj, input_path, stdout=None, stderr=None, both=None, timestamps=False, attempt=None
131
+ ):
132
+ types = set()
133
+ if stdout:
134
+ types.add("stdout")
135
+ both = False
136
+ if stderr:
137
+ types.add("stderr")
138
+ both = False
139
+ if both:
140
+ types.update(("stdout", "stderr"))
141
+
142
+ streams = list(sorted(types, reverse=True))
143
+
144
+ # Pathspec can either be run_id/step_name or run_id/step_name/task_id.
145
+ parts = input_path.split("/")
146
+ if len(parts) == 2:
147
+ run_id, step_name = parts
148
+ task_id = None
149
+ elif len(parts) == 3:
150
+ run_id, step_name, task_id = parts
151
+ else:
152
+ raise CommandException(
153
+ "input_path should either be run_id/step_name "
154
+ "or run_id/step_name/task_id"
155
+ )
156
+
157
+ datastore_set = TaskDataStoreSet(
158
+ obj.flow_datastore, run_id, steps=[step_name], allow_not_done=True
159
+ )
160
+ if task_id:
161
+ ds_list = [
162
+ TaskDataStore(
163
+ obj.flow_datastore,
164
+ run_id=run_id,
165
+ step_name=step_name,
166
+ task_id=task_id,
167
+ mode="r",
168
+ allow_not_done=True,
169
+ )
170
+ ]
171
+ else:
172
+ ds_list = list(datastore_set) # get all tasks
173
+
174
+ if ds_list:
175
+
176
+ def echo_unicode(line, **kwargs):
177
+ click.secho(line.decode("UTF-8", errors="replace"), **kwargs)
178
+
179
+ # old style logs are non mflog-style logs
180
+ maybe_old_style = True
181
+ for ds in ds_list:
182
+ echo(
183
+ "Dumping logs of run_id=*{run_id}* "
184
+ "step=*{step}* task_id=*{task_id}*".format(
185
+ run_id=ds.run_id, step=ds.step_name, task_id=ds.task_id
186
+ ),
187
+ fg="magenta",
188
+ )
189
+
190
+ for stream in streams:
191
+ echo(stream, bold=True)
192
+ logs = ds.load_logs(LOG_SOURCES, stream, attempt_override=attempt)
193
+ if any(data for _, data in logs):
194
+ # attempt to read new, mflog-style logs
195
+ for line in mflog.merge_logs([blob for _, blob in logs]):
196
+ if timestamps:
197
+ ts = mflog.utc_to_local(line.utc_tstamp)
198
+ tstamp = ts.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
199
+ click.secho(tstamp + " ", fg=LOGGER_TIMESTAMP, nl=False)
200
+ echo_unicode(line.msg)
201
+ maybe_old_style = False
202
+ elif maybe_old_style:
203
+ # if they are not available, we may be looking at
204
+ # a legacy run (unless we have seen new-style data already
205
+ # for another stream). This return an empty string if
206
+ # nothing is found
207
+ log = ds.load_log_legacy(stream, attempt_override=attempt)
208
+ if log and timestamps:
209
+ raise CommandException(
210
+ "We can't show --timestamps for old runs. Sorry!"
211
+ )
212
+ echo_unicode(log, nl=False)
213
+ else:
214
+ raise CommandException(
215
+ "No Tasks found at the given path -- "
216
+ "either none exist or none have started yet"
217
+ )
218
+
219
+
220
+ @logs.command(
221
+ help="Scrub stdout/stderr produced by a task or all tasks in a step. "
222
+ "The format for input-path is either <run_id>/<step_name> or "
223
+ "<run_id>/<step_name>/<task_id>."
224
+ )
225
+ @click.argument("input-path")
226
+ @click.option(
227
+ "--stdout/--no-stdout",
228
+ default=False,
229
+ show_default=True,
230
+ help="Scrub stdout of the step or task.",
231
+ )
232
+ @click.option(
233
+ "--stderr/--no-stderr",
234
+ default=False,
235
+ show_default=True,
236
+ help="Scrub stderr of the step or task.",
237
+ )
238
+ @click.option(
239
+ "--both/--no-both",
240
+ default=True,
241
+ show_default=True,
242
+ help="Scrub both stdout and stderr of the step or task.",
243
+ )
244
+ @click.option(
245
+ "--attempt",
246
+ default=None,
247
+ type=int,
248
+ show_default=False,
249
+ help="Attempt number of a task to scrub, defaults to the latest attempt.",
250
+ )
251
+ @click.option(
252
+ "--latest/--all",
253
+ default=True,
254
+ show_default=False,
255
+ help="Scrub latest/all attempts of a step or task",
256
+ )
257
+ @click.option(
258
+ "--include-not-done",
259
+ default=False,
260
+ show_default=False,
261
+ is_flag=True,
262
+ help="Also scrub steps or tasks that are not done. Use this for tasks that did not finish correctly, and could not otherwise be scrubbed.",
263
+ )
264
+ @click.pass_obj
265
+ def scrub(
266
+ obj,
267
+ input_path,
268
+ stdout=None,
269
+ stderr=None,
270
+ both=None,
271
+ attempt=None,
272
+ latest=None,
273
+ include_not_done=None,
274
+ ):
275
+ types = set()
276
+ if stdout:
277
+ types.add("stdout")
278
+ both = False
279
+ if stderr:
280
+ types.add("stderr")
281
+ both = False
282
+ if both:
283
+ types.update(("stdout", "stderr"))
284
+
285
+ streams = list(sorted(types, reverse=True))
286
+
287
+ # Pathspec can either be run_id/step_name or run_id/step_name/task_id.
288
+ parts = input_path.split("/")
289
+ if len(parts) == 2:
290
+ run_id, step_name = parts
291
+ task_id = None
292
+ elif len(parts) == 3:
293
+ run_id, step_name, task_id = parts
294
+ else:
295
+ raise CommandException(
296
+ "input_path should either be run_id/step_name "
297
+ "or run_id/step_name/task_id"
298
+ )
299
+
300
+ if task_id:
301
+ if latest:
302
+ ds_list = obj.flow_datastore.get_task_datastores(
303
+ pathspecs=[input_path],
304
+ attempt=attempt,
305
+ mode="d",
306
+ allow_not_done=include_not_done,
307
+ )
308
+ else:
309
+ ds_list = obj.flow_datastore.get_task_datastores(
310
+ pathspecs=[input_path],
311
+ attempt=attempt,
312
+ mode="d",
313
+ allow_not_done=include_not_done,
314
+ include_prior=True,
315
+ )
316
+ else:
317
+ if latest:
318
+ ds_list = obj.flow_datastore.get_task_datastores(
319
+ run_id=run_id,
320
+ steps=[step_name],
321
+ attempt=attempt,
322
+ mode="d",
323
+ allow_not_done=include_not_done,
324
+ )
325
+ else:
326
+ ds_list = obj.flow_datastore.get_task_datastores(
327
+ run_id=run_id,
328
+ steps=[step_name],
329
+ attempt=attempt,
330
+ mode="d",
331
+ allow_not_done=include_not_done,
332
+ include_prior=True,
333
+ )
334
+
335
+ if ds_list:
336
+ for ds in ds_list:
337
+ failures = []
338
+ for stream in streams:
339
+ try:
340
+ ds.scrub_logs(LOG_SOURCES, stream)
341
+ except Exception:
342
+ failures.append(stream)
343
+ if failures:
344
+ obj.echo_always(
345
+ "Failed to scrub %s - attempt %s : *%s*"
346
+ % (ds.pathspec, ds.attempt, ",".join(failures))
347
+ )
348
+ else:
349
+ echo(
350
+ "Logs have been scrubbed for %s - attempt %s"
351
+ % (ds.pathspec, ds.attempt)
352
+ )
353
+
354
+ else:
355
+ raise CommandException(
356
+ "No Tasks found at the given path -- "
357
+ "either none exist or they have not finished yet.\n"
358
+ "If you know the task has finished, you can supply --include-not-done to force scrub it."
359
+ )