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.
- metaflow/R.py +10 -7
- metaflow/__init__.py +40 -25
- metaflow/_vendor/imghdr/__init__.py +186 -0
- metaflow/_vendor/importlib_metadata/__init__.py +1063 -0
- metaflow/_vendor/importlib_metadata/_adapters.py +68 -0
- metaflow/_vendor/importlib_metadata/_collections.py +30 -0
- metaflow/_vendor/importlib_metadata/_compat.py +71 -0
- metaflow/_vendor/importlib_metadata/_functools.py +104 -0
- metaflow/_vendor/importlib_metadata/_itertools.py +73 -0
- metaflow/_vendor/importlib_metadata/_meta.py +48 -0
- metaflow/_vendor/importlib_metadata/_text.py +99 -0
- metaflow/_vendor/importlib_metadata/py.typed +0 -0
- metaflow/_vendor/typeguard/__init__.py +48 -0
- metaflow/_vendor/typeguard/_checkers.py +1070 -0
- metaflow/_vendor/typeguard/_config.py +108 -0
- metaflow/_vendor/typeguard/_decorators.py +233 -0
- metaflow/_vendor/typeguard/_exceptions.py +42 -0
- metaflow/_vendor/typeguard/_functions.py +308 -0
- metaflow/_vendor/typeguard/_importhook.py +213 -0
- metaflow/_vendor/typeguard/_memo.py +48 -0
- metaflow/_vendor/typeguard/_pytest_plugin.py +127 -0
- metaflow/_vendor/typeguard/_suppression.py +86 -0
- metaflow/_vendor/typeguard/_transformer.py +1229 -0
- metaflow/_vendor/typeguard/_union_transformer.py +55 -0
- metaflow/_vendor/typeguard/_utils.py +173 -0
- metaflow/_vendor/typeguard/py.typed +0 -0
- metaflow/_vendor/typing_extensions.py +3641 -0
- metaflow/_vendor/v3_7/importlib_metadata/__init__.py +1063 -0
- metaflow/_vendor/v3_7/importlib_metadata/_adapters.py +68 -0
- metaflow/_vendor/v3_7/importlib_metadata/_collections.py +30 -0
- metaflow/_vendor/v3_7/importlib_metadata/_compat.py +71 -0
- metaflow/_vendor/v3_7/importlib_metadata/_functools.py +104 -0
- metaflow/_vendor/v3_7/importlib_metadata/_itertools.py +73 -0
- metaflow/_vendor/v3_7/importlib_metadata/_meta.py +48 -0
- metaflow/_vendor/v3_7/importlib_metadata/_text.py +99 -0
- metaflow/_vendor/v3_7/importlib_metadata/py.typed +0 -0
- metaflow/_vendor/v3_7/typeguard/__init__.py +48 -0
- metaflow/_vendor/v3_7/typeguard/_checkers.py +906 -0
- metaflow/_vendor/v3_7/typeguard/_config.py +108 -0
- metaflow/_vendor/v3_7/typeguard/_decorators.py +237 -0
- metaflow/_vendor/v3_7/typeguard/_exceptions.py +42 -0
- metaflow/_vendor/v3_7/typeguard/_functions.py +310 -0
- metaflow/_vendor/v3_7/typeguard/_importhook.py +213 -0
- metaflow/_vendor/v3_7/typeguard/_memo.py +48 -0
- metaflow/_vendor/v3_7/typeguard/_pytest_plugin.py +100 -0
- metaflow/_vendor/v3_7/typeguard/_suppression.py +88 -0
- metaflow/_vendor/v3_7/typeguard/_transformer.py +1207 -0
- metaflow/_vendor/v3_7/typeguard/_union_transformer.py +54 -0
- metaflow/_vendor/v3_7/typeguard/_utils.py +169 -0
- metaflow/_vendor/v3_7/typeguard/py.typed +0 -0
- metaflow/_vendor/v3_7/typing_extensions.py +3072 -0
- metaflow/_vendor/yaml/__init__.py +427 -0
- metaflow/_vendor/yaml/composer.py +139 -0
- metaflow/_vendor/yaml/constructor.py +748 -0
- metaflow/_vendor/yaml/cyaml.py +101 -0
- metaflow/_vendor/yaml/dumper.py +62 -0
- metaflow/_vendor/yaml/emitter.py +1137 -0
- metaflow/_vendor/yaml/error.py +75 -0
- metaflow/_vendor/yaml/events.py +86 -0
- metaflow/_vendor/yaml/loader.py +63 -0
- metaflow/_vendor/yaml/nodes.py +49 -0
- metaflow/_vendor/yaml/parser.py +589 -0
- metaflow/_vendor/yaml/reader.py +185 -0
- metaflow/_vendor/yaml/representer.py +389 -0
- metaflow/_vendor/yaml/resolver.py +227 -0
- metaflow/_vendor/yaml/scanner.py +1435 -0
- metaflow/_vendor/yaml/serializer.py +111 -0
- metaflow/_vendor/yaml/tokens.py +104 -0
- metaflow/cards.py +5 -0
- metaflow/cli.py +331 -785
- metaflow/cli_args.py +17 -0
- metaflow/cli_components/__init__.py +0 -0
- metaflow/cli_components/dump_cmd.py +96 -0
- metaflow/cli_components/init_cmd.py +52 -0
- metaflow/cli_components/run_cmds.py +546 -0
- metaflow/cli_components/step_cmd.py +334 -0
- metaflow/cli_components/utils.py +140 -0
- metaflow/client/__init__.py +1 -0
- metaflow/client/core.py +467 -73
- metaflow/client/filecache.py +75 -35
- metaflow/clone_util.py +7 -1
- metaflow/cmd/code/__init__.py +231 -0
- metaflow/cmd/develop/stub_generator.py +756 -288
- metaflow/cmd/develop/stubs.py +12 -28
- metaflow/cmd/main_cli.py +6 -4
- metaflow/cmd/make_wrapper.py +78 -0
- metaflow/datastore/__init__.py +1 -0
- metaflow/datastore/content_addressed_store.py +41 -10
- metaflow/datastore/datastore_set.py +11 -2
- metaflow/datastore/flow_datastore.py +156 -10
- metaflow/datastore/spin_datastore.py +91 -0
- metaflow/datastore/task_datastore.py +154 -39
- metaflow/debug.py +5 -0
- metaflow/decorators.py +404 -78
- metaflow/exception.py +8 -2
- metaflow/extension_support/__init__.py +527 -376
- metaflow/extension_support/_empty_file.py +2 -2
- metaflow/extension_support/plugins.py +49 -31
- metaflow/flowspec.py +482 -33
- metaflow/graph.py +210 -42
- metaflow/includefile.py +84 -40
- metaflow/lint.py +141 -22
- metaflow/meta_files.py +13 -0
- metaflow/{metadata → metadata_provider}/heartbeat.py +24 -8
- metaflow/{metadata → metadata_provider}/metadata.py +86 -1
- metaflow/metaflow_config.py +175 -28
- metaflow/metaflow_config_funcs.py +51 -3
- metaflow/metaflow_current.py +4 -10
- metaflow/metaflow_environment.py +139 -53
- metaflow/metaflow_git.py +115 -0
- metaflow/metaflow_profile.py +18 -0
- metaflow/metaflow_version.py +150 -66
- metaflow/mflog/__init__.py +4 -3
- metaflow/mflog/save_logs.py +2 -2
- metaflow/multicore_utils.py +31 -14
- metaflow/package/__init__.py +673 -0
- metaflow/packaging_sys/__init__.py +880 -0
- metaflow/packaging_sys/backend.py +128 -0
- metaflow/packaging_sys/distribution_support.py +153 -0
- metaflow/packaging_sys/tar_backend.py +99 -0
- metaflow/packaging_sys/utils.py +54 -0
- metaflow/packaging_sys/v1.py +527 -0
- metaflow/parameters.py +149 -28
- metaflow/plugins/__init__.py +74 -5
- metaflow/plugins/airflow/airflow.py +40 -25
- metaflow/plugins/airflow/airflow_cli.py +22 -5
- metaflow/plugins/airflow/airflow_decorator.py +1 -1
- metaflow/plugins/airflow/airflow_utils.py +5 -3
- metaflow/plugins/airflow/sensors/base_sensor.py +4 -4
- metaflow/plugins/airflow/sensors/external_task_sensor.py +2 -2
- metaflow/plugins/airflow/sensors/s3_sensor.py +2 -2
- metaflow/plugins/argo/argo_client.py +78 -33
- metaflow/plugins/argo/argo_events.py +6 -6
- metaflow/plugins/argo/argo_workflows.py +2410 -527
- metaflow/plugins/argo/argo_workflows_cli.py +571 -121
- metaflow/plugins/argo/argo_workflows_decorator.py +43 -12
- metaflow/plugins/argo/argo_workflows_deployer.py +106 -0
- metaflow/plugins/argo/argo_workflows_deployer_objects.py +453 -0
- metaflow/plugins/argo/capture_error.py +73 -0
- metaflow/plugins/argo/conditional_input_paths.py +35 -0
- metaflow/plugins/argo/exit_hooks.py +209 -0
- metaflow/plugins/argo/jobset_input_paths.py +15 -0
- metaflow/plugins/argo/param_val.py +19 -0
- metaflow/plugins/aws/aws_client.py +10 -3
- metaflow/plugins/aws/aws_utils.py +55 -2
- metaflow/plugins/aws/batch/batch.py +72 -5
- metaflow/plugins/aws/batch/batch_cli.py +33 -10
- metaflow/plugins/aws/batch/batch_client.py +4 -3
- metaflow/plugins/aws/batch/batch_decorator.py +102 -35
- metaflow/plugins/aws/secrets_manager/aws_secrets_manager_secrets_provider.py +13 -10
- metaflow/plugins/aws/step_functions/dynamo_db_client.py +0 -3
- metaflow/plugins/aws/step_functions/production_token.py +1 -1
- metaflow/plugins/aws/step_functions/step_functions.py +65 -8
- metaflow/plugins/aws/step_functions/step_functions_cli.py +101 -7
- metaflow/plugins/aws/step_functions/step_functions_decorator.py +1 -2
- metaflow/plugins/aws/step_functions/step_functions_deployer.py +97 -0
- metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +264 -0
- metaflow/plugins/azure/azure_exceptions.py +1 -1
- metaflow/plugins/azure/azure_secret_manager_secrets_provider.py +240 -0
- metaflow/plugins/azure/azure_tail.py +1 -1
- metaflow/plugins/azure/includefile_support.py +2 -0
- metaflow/plugins/cards/card_cli.py +66 -30
- metaflow/plugins/cards/card_creator.py +25 -1
- metaflow/plugins/cards/card_datastore.py +21 -49
- metaflow/plugins/cards/card_decorator.py +132 -8
- metaflow/plugins/cards/card_modules/basic.py +112 -17
- metaflow/plugins/cards/card_modules/bundle.css +1 -1
- metaflow/plugins/cards/card_modules/card.py +16 -1
- metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
- metaflow/plugins/cards/card_modules/components.py +665 -28
- metaflow/plugins/cards/card_modules/convert_to_native_type.py +36 -7
- metaflow/plugins/cards/card_modules/json_viewer.py +232 -0
- metaflow/plugins/cards/card_modules/main.css +1 -0
- metaflow/plugins/cards/card_modules/main.js +68 -49
- metaflow/plugins/cards/card_modules/renderer_tools.py +1 -0
- metaflow/plugins/cards/card_modules/test_cards.py +26 -12
- metaflow/plugins/cards/card_server.py +39 -14
- metaflow/plugins/cards/component_serializer.py +2 -9
- metaflow/plugins/cards/metadata.py +22 -0
- metaflow/plugins/catch_decorator.py +9 -0
- metaflow/plugins/datastores/azure_storage.py +10 -1
- metaflow/plugins/datastores/gs_storage.py +6 -2
- metaflow/plugins/datastores/local_storage.py +12 -6
- metaflow/plugins/datastores/spin_storage.py +12 -0
- metaflow/plugins/datatools/local.py +2 -0
- metaflow/plugins/datatools/s3/s3.py +126 -75
- metaflow/plugins/datatools/s3/s3op.py +254 -121
- metaflow/plugins/env_escape/__init__.py +3 -3
- metaflow/plugins/env_escape/client_modules.py +102 -72
- metaflow/plugins/env_escape/server.py +7 -0
- metaflow/plugins/env_escape/stub.py +24 -5
- metaflow/plugins/events_decorator.py +343 -185
- metaflow/plugins/exit_hook/__init__.py +0 -0
- metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
- metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
- metaflow/plugins/gcp/__init__.py +1 -1
- metaflow/plugins/gcp/gcp_secret_manager_secrets_provider.py +11 -6
- metaflow/plugins/gcp/gs_tail.py +10 -6
- metaflow/plugins/gcp/includefile_support.py +3 -0
- metaflow/plugins/kubernetes/kube_utils.py +108 -0
- metaflow/plugins/kubernetes/kubernetes.py +411 -130
- metaflow/plugins/kubernetes/kubernetes_cli.py +168 -36
- metaflow/plugins/kubernetes/kubernetes_client.py +104 -2
- metaflow/plugins/kubernetes/kubernetes_decorator.py +246 -88
- metaflow/plugins/kubernetes/kubernetes_job.py +253 -581
- metaflow/plugins/kubernetes/kubernetes_jobsets.py +1071 -0
- metaflow/plugins/kubernetes/spot_metadata_cli.py +69 -0
- metaflow/plugins/kubernetes/spot_monitor_sidecar.py +109 -0
- metaflow/plugins/logs_cli.py +359 -0
- metaflow/plugins/{metadata → metadata_providers}/local.py +144 -84
- metaflow/plugins/{metadata → metadata_providers}/service.py +103 -26
- metaflow/plugins/metadata_providers/spin.py +16 -0
- metaflow/plugins/package_cli.py +36 -24
- metaflow/plugins/parallel_decorator.py +128 -11
- metaflow/plugins/parsers.py +16 -0
- metaflow/plugins/project_decorator.py +51 -5
- metaflow/plugins/pypi/bootstrap.py +357 -105
- metaflow/plugins/pypi/conda_decorator.py +82 -81
- metaflow/plugins/pypi/conda_environment.py +187 -52
- metaflow/plugins/pypi/micromamba.py +157 -47
- metaflow/plugins/pypi/parsers.py +268 -0
- metaflow/plugins/pypi/pip.py +88 -13
- metaflow/plugins/pypi/pypi_decorator.py +37 -1
- metaflow/plugins/pypi/utils.py +48 -2
- metaflow/plugins/resources_decorator.py +2 -2
- metaflow/plugins/secrets/__init__.py +3 -0
- metaflow/plugins/secrets/secrets_decorator.py +26 -181
- metaflow/plugins/secrets/secrets_func.py +49 -0
- metaflow/plugins/secrets/secrets_spec.py +101 -0
- metaflow/plugins/secrets/utils.py +74 -0
- metaflow/plugins/tag_cli.py +4 -7
- metaflow/plugins/test_unbounded_foreach_decorator.py +41 -6
- metaflow/plugins/timeout_decorator.py +3 -3
- metaflow/plugins/uv/__init__.py +0 -0
- metaflow/plugins/uv/bootstrap.py +128 -0
- metaflow/plugins/uv/uv_environment.py +72 -0
- metaflow/procpoll.py +1 -1
- metaflow/pylint_wrapper.py +5 -1
- metaflow/runner/__init__.py +0 -0
- metaflow/runner/click_api.py +717 -0
- metaflow/runner/deployer.py +470 -0
- metaflow/runner/deployer_impl.py +201 -0
- metaflow/runner/metaflow_runner.py +714 -0
- metaflow/runner/nbdeploy.py +132 -0
- metaflow/runner/nbrun.py +225 -0
- metaflow/runner/subprocess_manager.py +650 -0
- metaflow/runner/utils.py +335 -0
- metaflow/runtime.py +1078 -260
- metaflow/sidecar/sidecar_worker.py +1 -1
- metaflow/system/__init__.py +5 -0
- metaflow/system/system_logger.py +85 -0
- metaflow/system/system_monitor.py +108 -0
- metaflow/system/system_utils.py +19 -0
- metaflow/task.py +521 -225
- metaflow/tracing/__init__.py +7 -7
- metaflow/tracing/span_exporter.py +31 -38
- metaflow/tracing/tracing_modules.py +38 -43
- metaflow/tuple_util.py +27 -0
- metaflow/user_configs/__init__.py +0 -0
- metaflow/user_configs/config_options.py +563 -0
- metaflow/user_configs/config_parameters.py +598 -0
- metaflow/user_decorators/__init__.py +0 -0
- metaflow/user_decorators/common.py +144 -0
- metaflow/user_decorators/mutable_flow.py +512 -0
- metaflow/user_decorators/mutable_step.py +424 -0
- metaflow/user_decorators/user_flow_decorator.py +264 -0
- metaflow/user_decorators/user_step_decorator.py +749 -0
- metaflow/util.py +243 -27
- metaflow/vendor.py +23 -7
- metaflow/version.py +1 -1
- ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/Makefile +355 -0
- ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/Tiltfile +726 -0
- ob_metaflow-2.19.7.1rc0.data/data/share/metaflow/devtools/pick_services.sh +105 -0
- ob_metaflow-2.19.7.1rc0.dist-info/METADATA +87 -0
- ob_metaflow-2.19.7.1rc0.dist-info/RECORD +445 -0
- {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/WHEEL +1 -1
- {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/entry_points.txt +1 -0
- metaflow/_vendor/v3_5/__init__.py +0 -1
- metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
- metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
- metaflow/package.py +0 -188
- ob_metaflow-2.11.13.1.dist-info/METADATA +0 -85
- ob_metaflow-2.11.13.1.dist-info/RECORD +0 -308
- /metaflow/_vendor/{v3_5/zipp.py → zipp.py} +0 -0
- /metaflow/{metadata → metadata_provider}/__init__.py +0 -0
- /metaflow/{metadata → metadata_provider}/util.py +0 -0
- /metaflow/plugins/{metadata → metadata_providers}/__init__.py +0 -0
- {ob_metaflow-2.11.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info/licenses}/LICENSE +0 -0
- {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
|
+
)
|