flyte 2.0.0b13__py3-none-any.whl → 2.0.0b30__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.
- flyte/__init__.py +18 -2
- flyte/_bin/debug.py +38 -0
- flyte/_bin/runtime.py +62 -8
- flyte/_cache/cache.py +4 -2
- flyte/_cache/local_cache.py +216 -0
- flyte/_code_bundle/_ignore.py +12 -4
- flyte/_code_bundle/_packaging.py +13 -9
- flyte/_code_bundle/_utils.py +18 -10
- flyte/_code_bundle/bundle.py +17 -9
- flyte/_constants.py +1 -0
- flyte/_context.py +4 -1
- flyte/_custom_context.py +73 -0
- flyte/_debug/constants.py +38 -0
- flyte/_debug/utils.py +17 -0
- flyte/_debug/vscode.py +307 -0
- flyte/_deploy.py +235 -61
- flyte/_environment.py +20 -6
- flyte/_excepthook.py +1 -1
- flyte/_hash.py +1 -16
- flyte/_image.py +178 -81
- flyte/_initialize.py +132 -51
- flyte/_interface.py +39 -2
- flyte/_internal/controllers/__init__.py +4 -5
- flyte/_internal/controllers/_local_controller.py +70 -29
- flyte/_internal/controllers/_trace.py +1 -1
- flyte/_internal/controllers/remote/__init__.py +0 -2
- flyte/_internal/controllers/remote/_action.py +14 -16
- flyte/_internal/controllers/remote/_client.py +1 -1
- flyte/_internal/controllers/remote/_controller.py +68 -70
- flyte/_internal/controllers/remote/_core.py +127 -99
- flyte/_internal/controllers/remote/_informer.py +19 -10
- flyte/_internal/controllers/remote/_service_protocol.py +7 -7
- flyte/_internal/imagebuild/docker_builder.py +181 -69
- flyte/_internal/imagebuild/image_builder.py +0 -5
- flyte/_internal/imagebuild/remote_builder.py +155 -64
- flyte/_internal/imagebuild/utils.py +51 -2
- flyte/_internal/resolvers/_task_module.py +5 -38
- flyte/_internal/resolvers/default.py +2 -2
- flyte/_internal/runtime/convert.py +110 -21
- flyte/_internal/runtime/entrypoints.py +27 -1
- flyte/_internal/runtime/io.py +21 -8
- flyte/_internal/runtime/resources_serde.py +20 -6
- flyte/_internal/runtime/reuse.py +1 -1
- flyte/_internal/runtime/rusty.py +20 -5
- flyte/_internal/runtime/task_serde.py +34 -19
- flyte/_internal/runtime/taskrunner.py +22 -4
- flyte/_internal/runtime/trigger_serde.py +160 -0
- flyte/_internal/runtime/types_serde.py +1 -1
- flyte/_keyring/__init__.py +0 -0
- flyte/_keyring/file.py +115 -0
- flyte/_logging.py +201 -39
- flyte/_map.py +111 -14
- flyte/_module.py +70 -0
- flyte/_pod.py +4 -3
- flyte/_resources.py +213 -31
- flyte/_run.py +110 -39
- flyte/_task.py +75 -16
- flyte/_task_environment.py +105 -29
- flyte/_task_plugins.py +4 -2
- flyte/_trace.py +5 -0
- flyte/_trigger.py +1000 -0
- flyte/_utils/__init__.py +2 -1
- flyte/_utils/asyn.py +3 -1
- flyte/_utils/coro_management.py +2 -1
- flyte/_utils/docker_credentials.py +173 -0
- flyte/_utils/module_loader.py +17 -2
- flyte/_version.py +3 -3
- flyte/cli/_abort.py +3 -3
- flyte/cli/_build.py +3 -6
- flyte/cli/_common.py +78 -7
- flyte/cli/_create.py +182 -4
- flyte/cli/_delete.py +23 -1
- flyte/cli/_deploy.py +63 -16
- flyte/cli/_get.py +79 -34
- flyte/cli/_params.py +26 -10
- flyte/cli/_plugins.py +209 -0
- flyte/cli/_run.py +151 -26
- flyte/cli/_serve.py +64 -0
- flyte/cli/_update.py +37 -0
- flyte/cli/_user.py +17 -0
- flyte/cli/main.py +30 -4
- flyte/config/_config.py +10 -6
- flyte/config/_internal.py +1 -0
- flyte/config/_reader.py +29 -8
- flyte/connectors/__init__.py +11 -0
- flyte/connectors/_connector.py +270 -0
- flyte/connectors/_server.py +197 -0
- flyte/connectors/utils.py +135 -0
- flyte/errors.py +22 -2
- flyte/extend.py +8 -1
- flyte/extras/_container.py +6 -1
- flyte/git/__init__.py +3 -0
- flyte/git/_config.py +21 -0
- flyte/io/__init__.py +2 -0
- flyte/io/_dataframe/__init__.py +2 -0
- flyte/io/_dataframe/basic_dfs.py +17 -8
- flyte/io/_dataframe/dataframe.py +98 -132
- flyte/io/_dir.py +575 -113
- flyte/io/_file.py +582 -139
- flyte/io/_hashing_io.py +342 -0
- flyte/models.py +74 -15
- flyte/remote/__init__.py +6 -1
- flyte/remote/_action.py +34 -26
- flyte/remote/_client/_protocols.py +39 -4
- flyte/remote/_client/auth/_authenticators/device_code.py +4 -5
- flyte/remote/_client/auth/_authenticators/pkce.py +1 -1
- flyte/remote/_client/auth/_channel.py +10 -6
- flyte/remote/_client/controlplane.py +17 -5
- flyte/remote/_console.py +3 -2
- flyte/remote/_data.py +6 -6
- flyte/remote/_logs.py +3 -3
- flyte/remote/_run.py +64 -8
- flyte/remote/_secret.py +26 -17
- flyte/remote/_task.py +75 -33
- flyte/remote/_trigger.py +306 -0
- flyte/remote/_user.py +33 -0
- flyte/report/_report.py +1 -1
- flyte/storage/__init__.py +6 -1
- flyte/storage/_config.py +5 -1
- flyte/storage/_parallel_reader.py +274 -0
- flyte/storage/_storage.py +200 -103
- flyte/types/__init__.py +16 -0
- flyte/types/_interface.py +2 -2
- flyte/types/_pickle.py +35 -8
- flyte/types/_string_literals.py +8 -9
- flyte/types/_type_engine.py +40 -70
- flyte/types/_utils.py +1 -1
- flyte-2.0.0b30.data/scripts/debug.py +38 -0
- {flyte-2.0.0b13.data → flyte-2.0.0b30.data}/scripts/runtime.py +62 -8
- {flyte-2.0.0b13.dist-info → flyte-2.0.0b30.dist-info}/METADATA +11 -3
- flyte-2.0.0b30.dist-info/RECORD +192 -0
- {flyte-2.0.0b13.dist-info → flyte-2.0.0b30.dist-info}/entry_points.txt +3 -0
- flyte/_protos/common/authorization_pb2.py +0 -66
- flyte/_protos/common/authorization_pb2.pyi +0 -108
- flyte/_protos/common/authorization_pb2_grpc.py +0 -4
- flyte/_protos/common/identifier_pb2.py +0 -93
- flyte/_protos/common/identifier_pb2.pyi +0 -110
- flyte/_protos/common/identifier_pb2_grpc.py +0 -4
- flyte/_protos/common/identity_pb2.py +0 -48
- flyte/_protos/common/identity_pb2.pyi +0 -72
- flyte/_protos/common/identity_pb2_grpc.py +0 -4
- flyte/_protos/common/list_pb2.py +0 -36
- flyte/_protos/common/list_pb2.pyi +0 -71
- flyte/_protos/common/list_pb2_grpc.py +0 -4
- flyte/_protos/common/policy_pb2.py +0 -37
- flyte/_protos/common/policy_pb2.pyi +0 -27
- flyte/_protos/common/policy_pb2_grpc.py +0 -4
- flyte/_protos/common/role_pb2.py +0 -37
- flyte/_protos/common/role_pb2.pyi +0 -53
- flyte/_protos/common/role_pb2_grpc.py +0 -4
- flyte/_protos/common/runtime_version_pb2.py +0 -28
- flyte/_protos/common/runtime_version_pb2.pyi +0 -24
- flyte/_protos/common/runtime_version_pb2_grpc.py +0 -4
- flyte/_protos/imagebuilder/definition_pb2.py +0 -59
- flyte/_protos/imagebuilder/definition_pb2.pyi +0 -140
- flyte/_protos/imagebuilder/definition_pb2_grpc.py +0 -4
- flyte/_protos/imagebuilder/payload_pb2.py +0 -32
- flyte/_protos/imagebuilder/payload_pb2.pyi +0 -21
- flyte/_protos/imagebuilder/payload_pb2_grpc.py +0 -4
- flyte/_protos/imagebuilder/service_pb2.py +0 -29
- flyte/_protos/imagebuilder/service_pb2.pyi +0 -5
- flyte/_protos/imagebuilder/service_pb2_grpc.py +0 -66
- flyte/_protos/logs/dataplane/payload_pb2.py +0 -100
- flyte/_protos/logs/dataplane/payload_pb2.pyi +0 -177
- flyte/_protos/logs/dataplane/payload_pb2_grpc.py +0 -4
- flyte/_protos/secret/definition_pb2.py +0 -49
- flyte/_protos/secret/definition_pb2.pyi +0 -93
- flyte/_protos/secret/definition_pb2_grpc.py +0 -4
- flyte/_protos/secret/payload_pb2.py +0 -62
- flyte/_protos/secret/payload_pb2.pyi +0 -94
- flyte/_protos/secret/payload_pb2_grpc.py +0 -4
- flyte/_protos/secret/secret_pb2.py +0 -38
- flyte/_protos/secret/secret_pb2.pyi +0 -6
- flyte/_protos/secret/secret_pb2_grpc.py +0 -198
- flyte/_protos/secret/secret_pb2_grpc_grpc.py +0 -198
- flyte/_protos/validate/validate/validate_pb2.py +0 -76
- flyte/_protos/workflow/common_pb2.py +0 -27
- flyte/_protos/workflow/common_pb2.pyi +0 -14
- flyte/_protos/workflow/common_pb2_grpc.py +0 -4
- flyte/_protos/workflow/environment_pb2.py +0 -29
- flyte/_protos/workflow/environment_pb2.pyi +0 -12
- flyte/_protos/workflow/environment_pb2_grpc.py +0 -4
- flyte/_protos/workflow/node_execution_service_pb2.py +0 -26
- flyte/_protos/workflow/node_execution_service_pb2.pyi +0 -4
- flyte/_protos/workflow/node_execution_service_pb2_grpc.py +0 -32
- flyte/_protos/workflow/queue_service_pb2.py +0 -109
- flyte/_protos/workflow/queue_service_pb2.pyi +0 -166
- flyte/_protos/workflow/queue_service_pb2_grpc.py +0 -172
- flyte/_protos/workflow/run_definition_pb2.py +0 -121
- flyte/_protos/workflow/run_definition_pb2.pyi +0 -327
- flyte/_protos/workflow/run_definition_pb2_grpc.py +0 -4
- flyte/_protos/workflow/run_logs_service_pb2.py +0 -41
- flyte/_protos/workflow/run_logs_service_pb2.pyi +0 -28
- flyte/_protos/workflow/run_logs_service_pb2_grpc.py +0 -69
- flyte/_protos/workflow/run_service_pb2.py +0 -137
- flyte/_protos/workflow/run_service_pb2.pyi +0 -185
- flyte/_protos/workflow/run_service_pb2_grpc.py +0 -446
- flyte/_protos/workflow/state_service_pb2.py +0 -67
- flyte/_protos/workflow/state_service_pb2.pyi +0 -76
- flyte/_protos/workflow/state_service_pb2_grpc.py +0 -138
- flyte/_protos/workflow/task_definition_pb2.py +0 -79
- flyte/_protos/workflow/task_definition_pb2.pyi +0 -81
- flyte/_protos/workflow/task_definition_pb2_grpc.py +0 -4
- flyte/_protos/workflow/task_service_pb2.py +0 -60
- flyte/_protos/workflow/task_service_pb2.pyi +0 -59
- flyte/_protos/workflow/task_service_pb2_grpc.py +0 -138
- flyte-2.0.0b13.dist-info/RECORD +0 -239
- /flyte/{_protos → _debug}/__init__.py +0 -0
- {flyte-2.0.0b13.dist-info → flyte-2.0.0b30.dist-info}/WHEEL +0 -0
- {flyte-2.0.0b13.dist-info → flyte-2.0.0b30.dist-info}/licenses/LICENSE +0 -0
- {flyte-2.0.0b13.dist-info → flyte-2.0.0b30.dist-info}/top_level.txt +0 -0
flyte/_code_bundle/bundle.py
CHANGED
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from typing import ClassVar, Type
|
|
9
9
|
|
|
10
10
|
from async_lru import alru_cache
|
|
11
|
-
from
|
|
11
|
+
from flyteidl2.core.tasks_pb2 import TaskTemplate
|
|
12
12
|
|
|
13
13
|
from flyte._logging import log, logger
|
|
14
14
|
from flyte._utils import AsyncLRUCache
|
|
@@ -104,7 +104,7 @@ async def build_pkl_bundle(
|
|
|
104
104
|
import shutil
|
|
105
105
|
|
|
106
106
|
# Copy the bundle to the given path
|
|
107
|
-
shutil.copy(dest, copy_bundle_to)
|
|
107
|
+
shutil.copy(dest, copy_bundle_to, follow_symlinks=True)
|
|
108
108
|
local_path = copy_bundle_to / dest.name
|
|
109
109
|
return CodeBundle(pkl=str(local_path), computed_version=str_digest)
|
|
110
110
|
return CodeBundle(pkl=str(dest), computed_version=str_digest)
|
|
@@ -169,6 +169,8 @@ async def download_bundle(bundle: CodeBundle) -> pathlib.Path:
|
|
|
169
169
|
|
|
170
170
|
:return: The path to the downloaded code bundle.
|
|
171
171
|
"""
|
|
172
|
+
import sys
|
|
173
|
+
|
|
172
174
|
import flyte.storage as storage
|
|
173
175
|
|
|
174
176
|
dest = pathlib.Path(bundle.destination)
|
|
@@ -178,22 +180,29 @@ async def download_bundle(bundle: CodeBundle) -> pathlib.Path:
|
|
|
178
180
|
# TODO make storage apis better to accept pathlib.Path
|
|
179
181
|
if bundle.tgz:
|
|
180
182
|
downloaded_bundle = dest / os.path.basename(bundle.tgz)
|
|
183
|
+
if downloaded_bundle.exists():
|
|
184
|
+
return downloaded_bundle.absolute()
|
|
181
185
|
# Download the tgz file
|
|
182
|
-
|
|
183
|
-
downloaded_bundle = pathlib.Path(path)
|
|
186
|
+
await storage.get(bundle.tgz, str(downloaded_bundle.absolute()))
|
|
184
187
|
# NOTE the os.path.join(destination, ''). This is to ensure that the given path is in fact a directory and all
|
|
185
188
|
# downloaded data should be copied into this directory. We do this to account for a difference in behavior in
|
|
186
189
|
# fsspec, which requires a trailing slash in case of pre-existing directory.
|
|
187
|
-
|
|
188
|
-
"tar",
|
|
190
|
+
args = [
|
|
189
191
|
"-xvf",
|
|
190
192
|
str(downloaded_bundle),
|
|
191
193
|
"-C",
|
|
192
194
|
str(dest),
|
|
195
|
+
]
|
|
196
|
+
if sys.platform != "darwin":
|
|
197
|
+
args.insert(0, "--overwrite")
|
|
198
|
+
|
|
199
|
+
process = await asyncio.create_subprocess_exec(
|
|
200
|
+
"tar",
|
|
201
|
+
*args,
|
|
193
202
|
stdout=asyncio.subprocess.PIPE,
|
|
194
203
|
stderr=asyncio.subprocess.PIPE,
|
|
195
204
|
)
|
|
196
|
-
|
|
205
|
+
_stdout, stderr = await process.communicate()
|
|
197
206
|
|
|
198
207
|
if process.returncode != 0:
|
|
199
208
|
raise RuntimeError(stderr.decode())
|
|
@@ -204,8 +213,7 @@ async def download_bundle(bundle: CodeBundle) -> pathlib.Path:
|
|
|
204
213
|
|
|
205
214
|
downloaded_bundle = dest / os.path.basename(bundle.pkl)
|
|
206
215
|
# Download the tgz file
|
|
207
|
-
|
|
208
|
-
downloaded_bundle = pathlib.Path(path)
|
|
216
|
+
await storage.get(bundle.pkl, str(downloaded_bundle.absolute()))
|
|
209
217
|
return downloaded_bundle.absolute()
|
|
210
218
|
else:
|
|
211
219
|
raise ValueError("Code bundle should be either tgz or pkl, found neither.")
|
flyte/_constants.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
FLYTE_SYS_PATH = "_F_SYS_PATH" # The paths that will be appended to sys.path at runtime
|
flyte/_context.py
CHANGED
|
@@ -135,7 +135,10 @@ root_context_var = contextvars.ContextVar("root", default=Context(data=ContextDa
|
|
|
135
135
|
|
|
136
136
|
|
|
137
137
|
def ctx() -> Optional[TaskContext]:
|
|
138
|
-
"""
|
|
138
|
+
"""
|
|
139
|
+
Returns flyte.models.TaskContext if within a task context, else None
|
|
140
|
+
Note: Only use this in task code and not module level.
|
|
141
|
+
"""
|
|
139
142
|
return internal_ctx().data.task_context
|
|
140
143
|
|
|
141
144
|
|
flyte/_custom_context.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
|
|
5
|
+
from flyte._context import ctx
|
|
6
|
+
|
|
7
|
+
from ._context import internal_ctx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_custom_context() -> dict[str, str]:
|
|
11
|
+
"""
|
|
12
|
+
Get the current input context. This can be used within a task to retrieve
|
|
13
|
+
context metadata that was passed to the action.
|
|
14
|
+
|
|
15
|
+
Context will automatically propagate to sub-actions.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
```python
|
|
19
|
+
import flyte
|
|
20
|
+
|
|
21
|
+
env = flyte.TaskEnvironment(name="...")
|
|
22
|
+
|
|
23
|
+
@env.task
|
|
24
|
+
def t1():
|
|
25
|
+
# context can be retrieved with `get_custom_context`
|
|
26
|
+
ctx = flyte.get_custom_context()
|
|
27
|
+
print(ctx) # {'project': '...', 'entity': '...'}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
:return: Dictionary of context key-value pairs
|
|
31
|
+
"""
|
|
32
|
+
tctx = ctx()
|
|
33
|
+
if tctx is None or tctx.custom_context is None:
|
|
34
|
+
return {}
|
|
35
|
+
return tctx.custom_context
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@contextmanager
|
|
39
|
+
def custom_context(**context: str):
|
|
40
|
+
"""
|
|
41
|
+
Synchronous context manager to set input context for tasks spawned within this block.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
```python
|
|
45
|
+
import flyte
|
|
46
|
+
|
|
47
|
+
env = flyte.TaskEnvironment(name="...")
|
|
48
|
+
|
|
49
|
+
@env.task
|
|
50
|
+
def t1():
|
|
51
|
+
ctx = flyte.get_custom_context()
|
|
52
|
+
print(ctx)
|
|
53
|
+
|
|
54
|
+
@env.task
|
|
55
|
+
def main():
|
|
56
|
+
# context can be passed via a context manager
|
|
57
|
+
with flyte.custom_context(project="my-project"):
|
|
58
|
+
t1() # will have {'project': 'my-project'} as context
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
:param context: Key-value pairs to set as input context
|
|
62
|
+
"""
|
|
63
|
+
ctx = internal_ctx()
|
|
64
|
+
if ctx.data.task_context is None:
|
|
65
|
+
yield
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
tctx = ctx.data.task_context
|
|
69
|
+
new_tctx = tctx.replace(custom_context={**tctx.custom_context, **context})
|
|
70
|
+
|
|
71
|
+
with ctx.replace_task_context(new_tctx):
|
|
72
|
+
yield
|
|
73
|
+
# Exit the context and restore the previous context
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
# Where the code-server tar and plugins are downloaded to
|
|
5
|
+
EXECUTABLE_NAME = "code-server"
|
|
6
|
+
DOWNLOAD_DIR = Path.home() / ".code-server"
|
|
7
|
+
HOURS_TO_SECONDS = 60 * 60
|
|
8
|
+
DEFAULT_UP_SECONDS = 10 * HOURS_TO_SECONDS # 10 hours
|
|
9
|
+
DEFAULT_CODE_SERVER_REMOTE_PATHS = {
|
|
10
|
+
"amd64": "https://github.com/coder/code-server/releases/download/v4.18.0/code-server-4.18.0-linux-amd64.tar.gz",
|
|
11
|
+
"arm64": "https://github.com/coder/code-server/releases/download/v4.18.0/code-server-4.18.0-linux-arm64.tar.gz",
|
|
12
|
+
}
|
|
13
|
+
DEFAULT_CODE_SERVER_EXTENSIONS = [
|
|
14
|
+
"https://raw.githubusercontent.com/flyteorg/flytetools/master/flytekitplugins/flyin/ms-python.python-2023.20.0.vsix",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
# Duration to pause the checking of the heartbeat file until the next one
|
|
18
|
+
HEARTBEAT_CHECK_SECONDS = 60
|
|
19
|
+
MAX_IDLE_SECONDS = 180
|
|
20
|
+
|
|
21
|
+
# The path is hardcoded by code-server
|
|
22
|
+
# https://coder.com/docs/code-server/latest/FAQ#what-is-the-heartbeat-file
|
|
23
|
+
HEARTBEAT_PATH = os.path.expanduser("~/.local/share/code-server/heartbeat")
|
|
24
|
+
|
|
25
|
+
INTERACTIVE_DEBUGGING_FILE_NAME = "flyteinteractive_interactive_entrypoint.py"
|
|
26
|
+
RESUME_TASK_FILE_NAME = "flyteinteractive_resume_task.py"
|
|
27
|
+
# Config keys to store in task template
|
|
28
|
+
VSCODE_TYPE_KEY = "flyteinteractive_type"
|
|
29
|
+
VSCODE_PORT_KEY = "flyteinteractive_port"
|
|
30
|
+
|
|
31
|
+
TASK_FUNCTION_SOURCE_PATH = "TASK_FUNCTION_SOURCE_PATH"
|
|
32
|
+
|
|
33
|
+
# Default max idle seconds to terminate the flyteinteractive server
|
|
34
|
+
HOURS_TO_SECONDS = 60 * 60
|
|
35
|
+
MAX_IDLE_SECONDS = 10 * HOURS_TO_SECONDS # 10 hours
|
|
36
|
+
|
|
37
|
+
# Subprocess constants
|
|
38
|
+
EXIT_CODE_SUCCESS = 0
|
flyte/_debug/utils.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from flyte._debug.constants import EXIT_CODE_SUCCESS
|
|
4
|
+
from flyte._logging import logger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def execute_command(cmd: str):
|
|
8
|
+
"""
|
|
9
|
+
Execute a command in the shell.
|
|
10
|
+
"""
|
|
11
|
+
process = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
12
|
+
logger.info(f"cmd: {cmd}")
|
|
13
|
+
stdout, stderr = await process.communicate()
|
|
14
|
+
if process.returncode != EXIT_CODE_SUCCESS:
|
|
15
|
+
raise RuntimeError(f"Command {cmd} failed with error: {stderr!r}")
|
|
16
|
+
logger.info(f"stdout: {stdout!r}")
|
|
17
|
+
logger.info(f"stderr: {stderr!r}")
|
flyte/_debug/vscode.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import multiprocessing
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
import aiofiles
|
|
15
|
+
import click
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from flyte import storage
|
|
19
|
+
from flyte._debug.constants import (
|
|
20
|
+
DEFAULT_CODE_SERVER_EXTENSIONS,
|
|
21
|
+
DEFAULT_CODE_SERVER_REMOTE_PATHS,
|
|
22
|
+
DOWNLOAD_DIR,
|
|
23
|
+
EXECUTABLE_NAME,
|
|
24
|
+
EXIT_CODE_SUCCESS,
|
|
25
|
+
HEARTBEAT_PATH,
|
|
26
|
+
MAX_IDLE_SECONDS,
|
|
27
|
+
)
|
|
28
|
+
from flyte._debug.utils import (
|
|
29
|
+
execute_command,
|
|
30
|
+
)
|
|
31
|
+
from flyte._internal.runtime.rusty import download_tgz
|
|
32
|
+
from flyte._logging import logger
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def download_file(url: str, target_dir: str) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Downloads a file from a given URL using HTTPX and saves it locally.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
url (str): The URL of the file to download.
|
|
41
|
+
target_dir (str): The directory where the file should be saved. Defaults to current directory.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
filename = os.path.join(target_dir, os.path.basename(url))
|
|
45
|
+
if url.startswith("http"):
|
|
46
|
+
response = httpx.get(url, follow_redirects=True)
|
|
47
|
+
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
|
|
48
|
+
async with aiofiles.open(filename, "wb") as f:
|
|
49
|
+
await f.write(response.content)
|
|
50
|
+
else:
|
|
51
|
+
await storage.get(url, filename)
|
|
52
|
+
logger.info(f"File '{filename}' downloaded successfully from '{url}'.")
|
|
53
|
+
return filename
|
|
54
|
+
|
|
55
|
+
except httpx.RequestError as e:
|
|
56
|
+
raise RuntimeError(f"An error occurred while requesting '{url}': {e}")
|
|
57
|
+
except httpx.HTTPStatusError as e:
|
|
58
|
+
raise RuntimeError(f"HTTP error occurred: {e.response.status_code} - {e.response.text}")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise RuntimeError(f"An unexpected error occurred: {e}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_default_extensions() -> List[str]:
|
|
64
|
+
extensions = os.getenv("_F_CS_E")
|
|
65
|
+
if extensions is not None:
|
|
66
|
+
return extensions.split(",")
|
|
67
|
+
return DEFAULT_CODE_SERVER_EXTENSIONS
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_code_server_info() -> str:
|
|
71
|
+
"""
|
|
72
|
+
Returns the code server information based on the system's architecture.
|
|
73
|
+
|
|
74
|
+
This function checks the system's architecture and returns the corresponding
|
|
75
|
+
code server information from the provided dictionary. The function currently
|
|
76
|
+
supports AMD64 and ARM64 architectures.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
str: The code server information corresponding to the system's architecture.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If the system's architecture is not AMD64 or ARM64.
|
|
83
|
+
"""
|
|
84
|
+
code_server_path = os.getenv("_F_CS_RP")
|
|
85
|
+
if code_server_path is not None:
|
|
86
|
+
return code_server_path
|
|
87
|
+
|
|
88
|
+
machine_info = platform.machine()
|
|
89
|
+
logger.info(f"machine type: {machine_info}")
|
|
90
|
+
code_server_info_dict = DEFAULT_CODE_SERVER_REMOTE_PATHS
|
|
91
|
+
|
|
92
|
+
if "aarch64" == machine_info:
|
|
93
|
+
return code_server_info_dict["arm64"]
|
|
94
|
+
elif "x86_64" == machine_info:
|
|
95
|
+
return code_server_info_dict["amd64"]
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
"Automatic download is only supported on AMD64 and ARM64 architectures."
|
|
99
|
+
" If you are using a different architecture, please visit the code-server official website to"
|
|
100
|
+
" manually download the appropriate version for your image."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_installed_extensions() -> List[str]:
|
|
105
|
+
"""
|
|
106
|
+
Get the list of installed extensions.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List[str]: The list of installed extensions.
|
|
110
|
+
"""
|
|
111
|
+
installed_extensions = subprocess.run(
|
|
112
|
+
["code-server", "--list-extensions"], check=False, capture_output=True, text=True
|
|
113
|
+
)
|
|
114
|
+
if installed_extensions.returncode != EXIT_CODE_SUCCESS:
|
|
115
|
+
logger.info(f"Command code-server --list-extensions failed with error: {installed_extensions.stderr}")
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
return installed_extensions.stdout.splitlines()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def is_extension_installed(extension: str, installed_extensions: List[str]) -> bool:
|
|
122
|
+
return any(installed_extension in extension for installed_extension in installed_extensions)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def download_vscode():
|
|
126
|
+
"""
|
|
127
|
+
Download vscode server and extension from remote to local and add the directory of binary executable to $PATH.
|
|
128
|
+
"""
|
|
129
|
+
# If the code server already exists in the container, skip downloading
|
|
130
|
+
executable_path = shutil.which(EXECUTABLE_NAME)
|
|
131
|
+
if executable_path is not None or os.path.exists(DOWNLOAD_DIR):
|
|
132
|
+
logger.info(f"Code server binary already exists at {executable_path}")
|
|
133
|
+
logger.info("Skipping downloading code server...")
|
|
134
|
+
else:
|
|
135
|
+
logger.info("Code server is not in $PATH, start downloading code server...")
|
|
136
|
+
# Create DOWNLOAD_DIR if not exist
|
|
137
|
+
logger.info(f"DOWNLOAD_DIR: {DOWNLOAD_DIR}")
|
|
138
|
+
os.makedirs(DOWNLOAD_DIR)
|
|
139
|
+
|
|
140
|
+
logger.info(f"Start downloading files to {DOWNLOAD_DIR}")
|
|
141
|
+
# Download remote file to local
|
|
142
|
+
code_server_remote_path = get_code_server_info()
|
|
143
|
+
code_server_tar_path = await download_file(code_server_remote_path, str(DOWNLOAD_DIR))
|
|
144
|
+
|
|
145
|
+
# Extract the tarball
|
|
146
|
+
with tarfile.open(code_server_tar_path, "r:gz") as tar:
|
|
147
|
+
tar.extractall(path=DOWNLOAD_DIR)
|
|
148
|
+
|
|
149
|
+
if os.path.exists(DOWNLOAD_DIR):
|
|
150
|
+
code_server_dir_name = os.path.basename(get_code_server_info()).removesuffix(".tar.gz")
|
|
151
|
+
code_server_bin_dir = os.path.join(DOWNLOAD_DIR, code_server_dir_name, "bin")
|
|
152
|
+
# Add the directory of code-server binary to $PATH
|
|
153
|
+
os.environ["PATH"] = code_server_bin_dir + os.pathsep + os.environ["PATH"]
|
|
154
|
+
|
|
155
|
+
# If the extension already exists in the container, skip downloading
|
|
156
|
+
installed_extensions = get_installed_extensions()
|
|
157
|
+
coros = []
|
|
158
|
+
|
|
159
|
+
for extension in get_default_extensions():
|
|
160
|
+
if not is_extension_installed(extension, installed_extensions):
|
|
161
|
+
coros.append(download_file(extension, str(DOWNLOAD_DIR)))
|
|
162
|
+
extension_paths = await asyncio.gather(*coros)
|
|
163
|
+
|
|
164
|
+
coros = []
|
|
165
|
+
for p in extension_paths:
|
|
166
|
+
logger.info(f"Execute extension installation command to install extension {p}")
|
|
167
|
+
coros.append(execute_command(f"code-server --install-extension {p}"))
|
|
168
|
+
|
|
169
|
+
await asyncio.gather(*coros)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def prepare_launch_json(ctx: click.Context, pid: int):
|
|
173
|
+
"""
|
|
174
|
+
Generate the launch.json and settings.json for users to easily launch interactive debugging and task resumption.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
virtual_venv = os.getenv("VIRTUAL_ENV", str(Path(sys.executable).parent.parent))
|
|
178
|
+
if virtual_venv is None:
|
|
179
|
+
raise RuntimeError("VIRTUAL_ENV is not found in environment variables.")
|
|
180
|
+
|
|
181
|
+
run_name = ctx.params["run_name"]
|
|
182
|
+
name = ctx.params["name"]
|
|
183
|
+
# TODO: Executor should pass correct name.
|
|
184
|
+
if run_name.startswith("{{"):
|
|
185
|
+
run_name = os.getenv("RUN_NAME", "")
|
|
186
|
+
if name.startswith("{{"):
|
|
187
|
+
name = os.getenv("ACTION_NAME", "")
|
|
188
|
+
|
|
189
|
+
launch_json = {
|
|
190
|
+
"version": "0.2.0",
|
|
191
|
+
"configurations": [
|
|
192
|
+
{
|
|
193
|
+
"name": "Interactive Debugging",
|
|
194
|
+
"type": "python",
|
|
195
|
+
"request": "launch",
|
|
196
|
+
"program": f"{virtual_venv}/bin/runtime.py",
|
|
197
|
+
"console": "integratedTerminal",
|
|
198
|
+
"justMyCode": True,
|
|
199
|
+
"args": [
|
|
200
|
+
"a0",
|
|
201
|
+
"--inputs",
|
|
202
|
+
ctx.params["inputs"],
|
|
203
|
+
"--outputs-path",
|
|
204
|
+
ctx.params["outputs_path"],
|
|
205
|
+
"--version",
|
|
206
|
+
ctx.params["version"],
|
|
207
|
+
"--run-base-dir",
|
|
208
|
+
ctx.params["run_base_dir"],
|
|
209
|
+
"--name",
|
|
210
|
+
name,
|
|
211
|
+
"--run-name",
|
|
212
|
+
run_name,
|
|
213
|
+
"--project",
|
|
214
|
+
ctx.params["project"],
|
|
215
|
+
"--domain",
|
|
216
|
+
ctx.params["domain"],
|
|
217
|
+
"--org",
|
|
218
|
+
ctx.params["org"],
|
|
219
|
+
"--image-cache",
|
|
220
|
+
ctx.params["image_cache"],
|
|
221
|
+
"--debug",
|
|
222
|
+
"False",
|
|
223
|
+
"--interactive-mode",
|
|
224
|
+
"True",
|
|
225
|
+
"--tgz",
|
|
226
|
+
ctx.params["tgz"],
|
|
227
|
+
"--dest",
|
|
228
|
+
ctx.params["dest"],
|
|
229
|
+
"--resolver",
|
|
230
|
+
ctx.params["resolver"],
|
|
231
|
+
*ctx.params["resolver_args"],
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"name": "Resume Task",
|
|
236
|
+
"type": "python",
|
|
237
|
+
"request": "launch",
|
|
238
|
+
"program": f"{virtual_venv}/bin/debug.py",
|
|
239
|
+
"console": "integratedTerminal",
|
|
240
|
+
"justMyCode": True,
|
|
241
|
+
"args": ["resume", "--pid", str(pid)],
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
vscode_directory = os.path.join(os.getcwd(), ".vscode")
|
|
247
|
+
if not os.path.exists(vscode_directory):
|
|
248
|
+
os.makedirs(vscode_directory)
|
|
249
|
+
|
|
250
|
+
with open(os.path.join(vscode_directory, "launch.json"), "w") as file:
|
|
251
|
+
json.dump(launch_json, file, indent=4)
|
|
252
|
+
|
|
253
|
+
settings_json = {"python.defaultInterpreterPath": sys.executable}
|
|
254
|
+
with open(os.path.join(vscode_directory, "settings.json"), "w") as file:
|
|
255
|
+
json.dump(settings_json, file, indent=4)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def _start_vscode_server(ctx: click.Context):
|
|
259
|
+
if ctx.params["tgz"] is None:
|
|
260
|
+
await download_vscode()
|
|
261
|
+
else:
|
|
262
|
+
await asyncio.gather(
|
|
263
|
+
download_tgz(ctx.params["dest"], ctx.params["version"], ctx.params["tgz"]), download_vscode()
|
|
264
|
+
)
|
|
265
|
+
child_process = multiprocessing.Process(
|
|
266
|
+
target=lambda cmd: asyncio.run(asyncio.run(execute_command(cmd))),
|
|
267
|
+
kwargs={"cmd": f"code-server --bind-addr 0.0.0.0:6060 --disable-workspace-trust --auth none {os.getcwd()}"},
|
|
268
|
+
)
|
|
269
|
+
child_process.start()
|
|
270
|
+
if child_process.pid is None:
|
|
271
|
+
raise RuntimeError("Failed to start vscode server.")
|
|
272
|
+
|
|
273
|
+
prepare_launch_json(ctx, child_process.pid)
|
|
274
|
+
|
|
275
|
+
start_time = time.time()
|
|
276
|
+
check_interval = 60 # Interval for heartbeat checking in seconds
|
|
277
|
+
last_heartbeat_check = time.time() - check_interval
|
|
278
|
+
|
|
279
|
+
def terminate_process():
|
|
280
|
+
if child_process.is_alive():
|
|
281
|
+
child_process.terminate()
|
|
282
|
+
child_process.join()
|
|
283
|
+
|
|
284
|
+
logger.info("waiting for task to resume...")
|
|
285
|
+
while child_process.is_alive():
|
|
286
|
+
current_time = time.time()
|
|
287
|
+
if current_time - last_heartbeat_check >= check_interval:
|
|
288
|
+
last_heartbeat_check = current_time
|
|
289
|
+
if not os.path.exists(HEARTBEAT_PATH):
|
|
290
|
+
delta = current_time - start_time
|
|
291
|
+
logger.info(f"Code server has not been connected since {delta} seconds ago.")
|
|
292
|
+
logger.info("Please open the browser to connect to the running server.")
|
|
293
|
+
else:
|
|
294
|
+
delta = current_time - os.path.getmtime(HEARTBEAT_PATH)
|
|
295
|
+
logger.info(f"The latest activity on code server is {delta} seconds ago.")
|
|
296
|
+
|
|
297
|
+
# If the time from last connection is longer than max idle seconds, terminate the vscode server.
|
|
298
|
+
if delta > MAX_IDLE_SECONDS:
|
|
299
|
+
logger.info(f"VSCode server is idle for more than {MAX_IDLE_SECONDS} seconds. Terminating...")
|
|
300
|
+
terminate_process()
|
|
301
|
+
sys.exit()
|
|
302
|
+
|
|
303
|
+
await asyncio.sleep(1)
|
|
304
|
+
|
|
305
|
+
logger.info("User has resumed the task.")
|
|
306
|
+
terminate_process()
|
|
307
|
+
return
|