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.
Files changed (211) hide show
  1. flyte/__init__.py +18 -2
  2. flyte/_bin/debug.py +38 -0
  3. flyte/_bin/runtime.py +62 -8
  4. flyte/_cache/cache.py +4 -2
  5. flyte/_cache/local_cache.py +216 -0
  6. flyte/_code_bundle/_ignore.py +12 -4
  7. flyte/_code_bundle/_packaging.py +13 -9
  8. flyte/_code_bundle/_utils.py +18 -10
  9. flyte/_code_bundle/bundle.py +17 -9
  10. flyte/_constants.py +1 -0
  11. flyte/_context.py +4 -1
  12. flyte/_custom_context.py +73 -0
  13. flyte/_debug/constants.py +38 -0
  14. flyte/_debug/utils.py +17 -0
  15. flyte/_debug/vscode.py +307 -0
  16. flyte/_deploy.py +235 -61
  17. flyte/_environment.py +20 -6
  18. flyte/_excepthook.py +1 -1
  19. flyte/_hash.py +1 -16
  20. flyte/_image.py +178 -81
  21. flyte/_initialize.py +132 -51
  22. flyte/_interface.py +39 -2
  23. flyte/_internal/controllers/__init__.py +4 -5
  24. flyte/_internal/controllers/_local_controller.py +70 -29
  25. flyte/_internal/controllers/_trace.py +1 -1
  26. flyte/_internal/controllers/remote/__init__.py +0 -2
  27. flyte/_internal/controllers/remote/_action.py +14 -16
  28. flyte/_internal/controllers/remote/_client.py +1 -1
  29. flyte/_internal/controllers/remote/_controller.py +68 -70
  30. flyte/_internal/controllers/remote/_core.py +127 -99
  31. flyte/_internal/controllers/remote/_informer.py +19 -10
  32. flyte/_internal/controllers/remote/_service_protocol.py +7 -7
  33. flyte/_internal/imagebuild/docker_builder.py +181 -69
  34. flyte/_internal/imagebuild/image_builder.py +0 -5
  35. flyte/_internal/imagebuild/remote_builder.py +155 -64
  36. flyte/_internal/imagebuild/utils.py +51 -2
  37. flyte/_internal/resolvers/_task_module.py +5 -38
  38. flyte/_internal/resolvers/default.py +2 -2
  39. flyte/_internal/runtime/convert.py +110 -21
  40. flyte/_internal/runtime/entrypoints.py +27 -1
  41. flyte/_internal/runtime/io.py +21 -8
  42. flyte/_internal/runtime/resources_serde.py +20 -6
  43. flyte/_internal/runtime/reuse.py +1 -1
  44. flyte/_internal/runtime/rusty.py +20 -5
  45. flyte/_internal/runtime/task_serde.py +34 -19
  46. flyte/_internal/runtime/taskrunner.py +22 -4
  47. flyte/_internal/runtime/trigger_serde.py +160 -0
  48. flyte/_internal/runtime/types_serde.py +1 -1
  49. flyte/_keyring/__init__.py +0 -0
  50. flyte/_keyring/file.py +115 -0
  51. flyte/_logging.py +201 -39
  52. flyte/_map.py +111 -14
  53. flyte/_module.py +70 -0
  54. flyte/_pod.py +4 -3
  55. flyte/_resources.py +213 -31
  56. flyte/_run.py +110 -39
  57. flyte/_task.py +75 -16
  58. flyte/_task_environment.py +105 -29
  59. flyte/_task_plugins.py +4 -2
  60. flyte/_trace.py +5 -0
  61. flyte/_trigger.py +1000 -0
  62. flyte/_utils/__init__.py +2 -1
  63. flyte/_utils/asyn.py +3 -1
  64. flyte/_utils/coro_management.py +2 -1
  65. flyte/_utils/docker_credentials.py +173 -0
  66. flyte/_utils/module_loader.py +17 -2
  67. flyte/_version.py +3 -3
  68. flyte/cli/_abort.py +3 -3
  69. flyte/cli/_build.py +3 -6
  70. flyte/cli/_common.py +78 -7
  71. flyte/cli/_create.py +182 -4
  72. flyte/cli/_delete.py +23 -1
  73. flyte/cli/_deploy.py +63 -16
  74. flyte/cli/_get.py +79 -34
  75. flyte/cli/_params.py +26 -10
  76. flyte/cli/_plugins.py +209 -0
  77. flyte/cli/_run.py +151 -26
  78. flyte/cli/_serve.py +64 -0
  79. flyte/cli/_update.py +37 -0
  80. flyte/cli/_user.py +17 -0
  81. flyte/cli/main.py +30 -4
  82. flyte/config/_config.py +10 -6
  83. flyte/config/_internal.py +1 -0
  84. flyte/config/_reader.py +29 -8
  85. flyte/connectors/__init__.py +11 -0
  86. flyte/connectors/_connector.py +270 -0
  87. flyte/connectors/_server.py +197 -0
  88. flyte/connectors/utils.py +135 -0
  89. flyte/errors.py +22 -2
  90. flyte/extend.py +8 -1
  91. flyte/extras/_container.py +6 -1
  92. flyte/git/__init__.py +3 -0
  93. flyte/git/_config.py +21 -0
  94. flyte/io/__init__.py +2 -0
  95. flyte/io/_dataframe/__init__.py +2 -0
  96. flyte/io/_dataframe/basic_dfs.py +17 -8
  97. flyte/io/_dataframe/dataframe.py +98 -132
  98. flyte/io/_dir.py +575 -113
  99. flyte/io/_file.py +582 -139
  100. flyte/io/_hashing_io.py +342 -0
  101. flyte/models.py +74 -15
  102. flyte/remote/__init__.py +6 -1
  103. flyte/remote/_action.py +34 -26
  104. flyte/remote/_client/_protocols.py +39 -4
  105. flyte/remote/_client/auth/_authenticators/device_code.py +4 -5
  106. flyte/remote/_client/auth/_authenticators/pkce.py +1 -1
  107. flyte/remote/_client/auth/_channel.py +10 -6
  108. flyte/remote/_client/controlplane.py +17 -5
  109. flyte/remote/_console.py +3 -2
  110. flyte/remote/_data.py +6 -6
  111. flyte/remote/_logs.py +3 -3
  112. flyte/remote/_run.py +64 -8
  113. flyte/remote/_secret.py +26 -17
  114. flyte/remote/_task.py +75 -33
  115. flyte/remote/_trigger.py +306 -0
  116. flyte/remote/_user.py +33 -0
  117. flyte/report/_report.py +1 -1
  118. flyte/storage/__init__.py +6 -1
  119. flyte/storage/_config.py +5 -1
  120. flyte/storage/_parallel_reader.py +274 -0
  121. flyte/storage/_storage.py +200 -103
  122. flyte/types/__init__.py +16 -0
  123. flyte/types/_interface.py +2 -2
  124. flyte/types/_pickle.py +35 -8
  125. flyte/types/_string_literals.py +8 -9
  126. flyte/types/_type_engine.py +40 -70
  127. flyte/types/_utils.py +1 -1
  128. flyte-2.0.0b30.data/scripts/debug.py +38 -0
  129. {flyte-2.0.0b13.data → flyte-2.0.0b30.data}/scripts/runtime.py +62 -8
  130. {flyte-2.0.0b13.dist-info → flyte-2.0.0b30.dist-info}/METADATA +11 -3
  131. flyte-2.0.0b30.dist-info/RECORD +192 -0
  132. {flyte-2.0.0b13.dist-info → flyte-2.0.0b30.dist-info}/entry_points.txt +3 -0
  133. flyte/_protos/common/authorization_pb2.py +0 -66
  134. flyte/_protos/common/authorization_pb2.pyi +0 -108
  135. flyte/_protos/common/authorization_pb2_grpc.py +0 -4
  136. flyte/_protos/common/identifier_pb2.py +0 -93
  137. flyte/_protos/common/identifier_pb2.pyi +0 -110
  138. flyte/_protos/common/identifier_pb2_grpc.py +0 -4
  139. flyte/_protos/common/identity_pb2.py +0 -48
  140. flyte/_protos/common/identity_pb2.pyi +0 -72
  141. flyte/_protos/common/identity_pb2_grpc.py +0 -4
  142. flyte/_protos/common/list_pb2.py +0 -36
  143. flyte/_protos/common/list_pb2.pyi +0 -71
  144. flyte/_protos/common/list_pb2_grpc.py +0 -4
  145. flyte/_protos/common/policy_pb2.py +0 -37
  146. flyte/_protos/common/policy_pb2.pyi +0 -27
  147. flyte/_protos/common/policy_pb2_grpc.py +0 -4
  148. flyte/_protos/common/role_pb2.py +0 -37
  149. flyte/_protos/common/role_pb2.pyi +0 -53
  150. flyte/_protos/common/role_pb2_grpc.py +0 -4
  151. flyte/_protos/common/runtime_version_pb2.py +0 -28
  152. flyte/_protos/common/runtime_version_pb2.pyi +0 -24
  153. flyte/_protos/common/runtime_version_pb2_grpc.py +0 -4
  154. flyte/_protos/imagebuilder/definition_pb2.py +0 -59
  155. flyte/_protos/imagebuilder/definition_pb2.pyi +0 -140
  156. flyte/_protos/imagebuilder/definition_pb2_grpc.py +0 -4
  157. flyte/_protos/imagebuilder/payload_pb2.py +0 -32
  158. flyte/_protos/imagebuilder/payload_pb2.pyi +0 -21
  159. flyte/_protos/imagebuilder/payload_pb2_grpc.py +0 -4
  160. flyte/_protos/imagebuilder/service_pb2.py +0 -29
  161. flyte/_protos/imagebuilder/service_pb2.pyi +0 -5
  162. flyte/_protos/imagebuilder/service_pb2_grpc.py +0 -66
  163. flyte/_protos/logs/dataplane/payload_pb2.py +0 -100
  164. flyte/_protos/logs/dataplane/payload_pb2.pyi +0 -177
  165. flyte/_protos/logs/dataplane/payload_pb2_grpc.py +0 -4
  166. flyte/_protos/secret/definition_pb2.py +0 -49
  167. flyte/_protos/secret/definition_pb2.pyi +0 -93
  168. flyte/_protos/secret/definition_pb2_grpc.py +0 -4
  169. flyte/_protos/secret/payload_pb2.py +0 -62
  170. flyte/_protos/secret/payload_pb2.pyi +0 -94
  171. flyte/_protos/secret/payload_pb2_grpc.py +0 -4
  172. flyte/_protos/secret/secret_pb2.py +0 -38
  173. flyte/_protos/secret/secret_pb2.pyi +0 -6
  174. flyte/_protos/secret/secret_pb2_grpc.py +0 -198
  175. flyte/_protos/secret/secret_pb2_grpc_grpc.py +0 -198
  176. flyte/_protos/validate/validate/validate_pb2.py +0 -76
  177. flyte/_protos/workflow/common_pb2.py +0 -27
  178. flyte/_protos/workflow/common_pb2.pyi +0 -14
  179. flyte/_protos/workflow/common_pb2_grpc.py +0 -4
  180. flyte/_protos/workflow/environment_pb2.py +0 -29
  181. flyte/_protos/workflow/environment_pb2.pyi +0 -12
  182. flyte/_protos/workflow/environment_pb2_grpc.py +0 -4
  183. flyte/_protos/workflow/node_execution_service_pb2.py +0 -26
  184. flyte/_protos/workflow/node_execution_service_pb2.pyi +0 -4
  185. flyte/_protos/workflow/node_execution_service_pb2_grpc.py +0 -32
  186. flyte/_protos/workflow/queue_service_pb2.py +0 -109
  187. flyte/_protos/workflow/queue_service_pb2.pyi +0 -166
  188. flyte/_protos/workflow/queue_service_pb2_grpc.py +0 -172
  189. flyte/_protos/workflow/run_definition_pb2.py +0 -121
  190. flyte/_protos/workflow/run_definition_pb2.pyi +0 -327
  191. flyte/_protos/workflow/run_definition_pb2_grpc.py +0 -4
  192. flyte/_protos/workflow/run_logs_service_pb2.py +0 -41
  193. flyte/_protos/workflow/run_logs_service_pb2.pyi +0 -28
  194. flyte/_protos/workflow/run_logs_service_pb2_grpc.py +0 -69
  195. flyte/_protos/workflow/run_service_pb2.py +0 -137
  196. flyte/_protos/workflow/run_service_pb2.pyi +0 -185
  197. flyte/_protos/workflow/run_service_pb2_grpc.py +0 -446
  198. flyte/_protos/workflow/state_service_pb2.py +0 -67
  199. flyte/_protos/workflow/state_service_pb2.pyi +0 -76
  200. flyte/_protos/workflow/state_service_pb2_grpc.py +0 -138
  201. flyte/_protos/workflow/task_definition_pb2.py +0 -79
  202. flyte/_protos/workflow/task_definition_pb2.pyi +0 -81
  203. flyte/_protos/workflow/task_definition_pb2_grpc.py +0 -4
  204. flyte/_protos/workflow/task_service_pb2.py +0 -60
  205. flyte/_protos/workflow/task_service_pb2.pyi +0 -59
  206. flyte/_protos/workflow/task_service_pb2_grpc.py +0 -138
  207. flyte-2.0.0b13.dist-info/RECORD +0 -239
  208. /flyte/{_protos → _debug}/__init__.py +0 -0
  209. {flyte-2.0.0b13.dist-info → flyte-2.0.0b30.dist-info}/WHEEL +0 -0
  210. {flyte-2.0.0b13.dist-info → flyte-2.0.0b30.dist-info}/licenses/LICENSE +0 -0
  211. {flyte-2.0.0b13.dist-info → flyte-2.0.0b30.dist-info}/top_level.txt +0 -0
@@ -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 flyteidl.core.tasks_pb2 import TaskTemplate
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
- path = await storage.get(bundle.tgz, str(downloaded_bundle.absolute()))
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
- process = await asyncio.create_subprocess_exec(
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
- stdout, stderr = await process.communicate()
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
- path = await storage.get(bundle.pkl, str(downloaded_bundle.absolute()))
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
- """Retrieve the current task context from the context variable."""
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
 
@@ -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