modal 0.62.115__py3-none-any.whl → 0.72.13__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 (220) hide show
  1. modal/__init__.py +13 -9
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +402 -398
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -60
  11. modal/_resources.py +26 -7
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1025 -0
  15. modal/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +3 -3
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +196 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +943 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
  128. modal-0.72.13.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
  220. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
@@ -8,18 +8,17 @@ import typer
8
8
  from click import UsageError
9
9
  from grpclib import GRPCError, Status
10
10
  from rich.console import Console
11
- from rich.live import Live
12
11
  from rich.syntax import Syntax
13
12
  from rich.table import Table
14
- from typer import Typer
13
+ from typer import Argument, Typer
15
14
 
16
15
  import modal
17
16
  from modal._location import display_location
18
- from modal._output import step_completed, step_progress
17
+ from modal._output import OutputManager, ProgressHandler
19
18
  from modal._utils.async_utils import synchronizer
20
19
  from modal._utils.grpc_utils import retry_transient_errors
21
20
  from modal.cli._download import _volume_download
22
- from modal.cli.utils import ENV_OPTION, display_table, timestamp_to_local
21
+ from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table, timestamp_to_local
23
22
  from modal.client import _Client
24
23
  from modal.environments import ensure_env
25
24
  from modal.network_file_system import _NetworkFileSystem
@@ -28,9 +27,9 @@ from modal_proto import api_pb2
28
27
  nfs_cli = Typer(name="nfs", help="Read and edit `modal.NetworkFileSystem` file systems.", no_args_is_help=True)
29
28
 
30
29
 
31
- @nfs_cli.command(name="list", help="List the names of all network file systems.")
30
+ @nfs_cli.command(name="list", help="List the names of all network file systems.", rich_help_panel="Management")
32
31
  @synchronizer.create_blocking
33
- async def list(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
32
+ async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
34
33
  env = ensure_env(env)
35
34
 
36
35
  client = await _Client.from_env()
@@ -59,7 +58,7 @@ def some_func():
59
58
  """
60
59
 
61
60
 
62
- @nfs_cli.command(name="create", help="Create a named network file system.")
61
+ @nfs_cli.command(name="create", help="Create a named network file system.", rich_help_panel="Management")
63
62
  def create(
64
63
  name: str,
65
64
  env: Optional[str] = ENV_OPTION,
@@ -81,7 +80,11 @@ async def _volume_from_name(deployment_name: str) -> _NetworkFileSystem:
81
80
  return network_file_system
82
81
 
83
82
 
84
- @nfs_cli.command(name="ls", help="List files and directories in a network file system.")
83
+ @nfs_cli.command(
84
+ name="ls",
85
+ help="List files and directories in a network file system.",
86
+ rich_help_panel="File operations",
87
+ )
85
88
  @synchronizer.create_blocking
86
89
  async def ls(
87
90
  volume_name: str,
@@ -120,8 +123,10 @@ async def ls(
120
123
 
121
124
  Remote parent directories will be created as needed.
122
125
 
123
- Ending the REMOTE_PATH with a forward slash (/), it's assumed to be a directory and the file will be uploaded with its current name under that directory.
126
+ Ending the REMOTE_PATH with a forward slash (/), it's assumed to be a directory and the file
127
+ will be uploaded with its current name under that directory.
124
128
  """,
129
+ rich_help_panel="File operations",
125
130
  )
126
131
  @synchronizer.create_blocking
127
132
  async def put(
@@ -137,19 +142,23 @@ async def put(
137
142
  console = Console()
138
143
 
139
144
  if Path(local_path).is_dir():
140
- spinner = step_progress(f"Uploading directory '{local_path}' to '{remote_path}'...")
141
- with Live(spinner, console=console):
142
- await volume.add_local_dir(local_path, remote_path)
143
- console.print(step_completed(f"Uploaded directory '{local_path}' to '{remote_path}'"))
145
+ progress_handler = ProgressHandler(type="upload", console=console)
146
+ with progress_handler.live:
147
+ await volume.add_local_dir(local_path, remote_path, progress_cb=progress_handler.progress)
148
+ progress_handler.progress(complete=True)
149
+ console.print(OutputManager.step_completed(f"Uploaded directory '{local_path}' to '{remote_path}'"))
144
150
 
145
151
  elif "*" in local_path:
146
152
  raise UsageError("Glob uploads are currently not supported")
147
153
  else:
148
- spinner = step_progress(f"Uploading file '{local_path}' to '{remote_path}'...")
149
- with Live(spinner, console=console):
150
- written_bytes = await volume.add_local_file(local_path, remote_path)
154
+ progress_handler = ProgressHandler(type="upload", console=console)
155
+ with progress_handler.live:
156
+ written_bytes = await volume.add_local_file(local_path, remote_path, progress_cb=progress_handler.progress)
157
+ progress_handler.progress(complete=True)
151
158
  console.print(
152
- step_completed(f"Uploaded file '{local_path}' to '{remote_path}' ({written_bytes} bytes written)")
159
+ OutputManager.step_completed(
160
+ f"Uploaded file '{local_path}' to '{remote_path}' ({written_bytes} bytes written)"
161
+ )
153
162
  )
154
163
 
155
164
 
@@ -158,7 +167,7 @@ class CliError(Exception):
158
167
  self.message = message
159
168
 
160
169
 
161
- @nfs_cli.command(name="get")
170
+ @nfs_cli.command(name="get", rich_help_panel="File operations")
162
171
  @synchronizer.create_blocking
163
172
  async def get(
164
173
  volume_name: str,
@@ -174,7 +183,7 @@ async def get(
174
183
 
175
184
  For example, to download an entire network file system into `dump_volume`:
176
185
 
177
- ```bash
186
+ ```
178
187
  modal nfs get <volume-name> "**" dump_volume
179
188
  ```
180
189
 
@@ -183,10 +192,16 @@ async def get(
183
192
  ensure_env(env)
184
193
  destination = Path(local_destination)
185
194
  volume = await _volume_from_name(volume_name)
186
- await _volume_download(volume, remote_path, destination, force)
195
+ console = Console()
196
+ progress_handler = ProgressHandler(type="download", console=console)
197
+ with progress_handler.live:
198
+ await _volume_download(volume, remote_path, destination, force, progress_cb=progress_handler.progress)
199
+ console.print(OutputManager.step_completed("Finished downloading files to local!"))
187
200
 
188
201
 
189
- @nfs_cli.command(name="rm", help="Delete a file or directory from a network file system.")
202
+ @nfs_cli.command(
203
+ name="rm", help="Delete a file or directory from a network file system.", rich_help_panel="File operations"
204
+ )
190
205
  @synchronizer.create_blocking
191
206
  async def rm(
192
207
  volume_name: str,
@@ -202,3 +217,24 @@ async def rm(
202
217
  if exc.status in (Status.NOT_FOUND, Status.INVALID_ARGUMENT):
203
218
  raise UsageError(exc.message)
204
219
  raise
220
+
221
+
222
+ @nfs_cli.command(
223
+ name="delete",
224
+ help="Delete a named, persistent modal.NetworkFileSystem.",
225
+ rich_help_panel="Management",
226
+ )
227
+ @synchronizer.create_blocking
228
+ async def delete(
229
+ nfs_name: str = Argument(help="Name of the modal.NetworkFileSystem to be deleted. Case sensitive"),
230
+ yes: bool = YES_OPTION,
231
+ env: Optional[str] = ENV_OPTION,
232
+ ):
233
+ if not yes:
234
+ typer.confirm(
235
+ f"Are you sure you want to irrevocably delete the modal.NetworkFileSystem '{nfs_name}'?",
236
+ default=False,
237
+ abort=True,
238
+ )
239
+
240
+ await _NetworkFileSystem.delete(nfs_name, environment_name=env)
modal/cli/profile.py CHANGED
@@ -28,7 +28,7 @@ def current():
28
28
 
29
29
  @profile_cli.command(name="list", help="Show all Modal profiles and highlight the active one.")
30
30
  @synchronizer.create_blocking
31
- async def list(json: Optional[bool] = False):
31
+ async def list_(json: Optional[bool] = False):
32
32
  config = Config()
33
33
  profiles = config_profiles()
34
34
  lookup_coros = [
@@ -8,16 +8,32 @@ import subprocess
8
8
  import threading
9
9
  import time
10
10
  import webbrowser
11
- from typing import Any, Dict
11
+ from typing import Any
12
12
 
13
- from modal import App, Image, Queue, forward
14
-
15
- # Passed by `modal launch` locally via CLI, empty on remote runner.
16
- args: Dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_LOCAL_ARGS", "{}"))
13
+ from modal import App, Image, Queue, Secret, Volume, forward
17
14
 
15
+ # Passed by `modal launch` locally via CLI, plumbed to remote runner through secrets.
16
+ args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
18
17
 
19
18
  app = App()
20
- app.image = Image.from_registry(args.get("image"), add_python=args.get("add_python")).pip_install("jupyterlab")
19
+
20
+ image = Image.from_registry(args.get("image"), add_python=args.get("add_python")).pip_install("jupyterlab")
21
+
22
+ if args.get("mount"):
23
+ image = image.add_local_dir(
24
+ args.get("mount"),
25
+ remote_path="/root/lab/mount",
26
+ )
27
+
28
+ volume = (
29
+ Volume.from_name(
30
+ args.get("volume"),
31
+ create_if_missing=True,
32
+ )
33
+ if args.get("volume")
34
+ else None
35
+ )
36
+ volumes = {"/root/lab/volume": volume} if volume else {}
21
37
 
22
38
 
23
39
  def wait_for_port(url: str, q: Queue):
@@ -33,9 +49,18 @@ def wait_for_port(url: str, q: Queue):
33
49
  q.put(url)
34
50
 
35
51
 
36
- @app.function(cpu=args.get("cpu"), memory=args.get("memory"), gpu=args.get("gpu"), timeout=args.get("timeout"))
52
+ @app.function(
53
+ image=image,
54
+ cpu=args.get("cpu"),
55
+ memory=args.get("memory"),
56
+ gpu=args.get("gpu"),
57
+ timeout=args.get("timeout"),
58
+ secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
59
+ volumes=volumes,
60
+ concurrency_limit=1 if volume else None,
61
+ )
37
62
  def run_jupyter(q: Queue):
38
- os.mkdir("/lab")
63
+ os.makedirs("/root/lab", exist_ok=True)
39
64
  token = secrets.token_urlsafe(13)
40
65
  with forward(8888) as tunnel:
41
66
  url = tunnel.url + "/?token=" + token
@@ -48,7 +73,7 @@ def run_jupyter(q: Queue):
48
73
  "--allow-root",
49
74
  "--ip=0.0.0.0",
50
75
  "--port=8888",
51
- "--notebook-dir=/lab",
76
+ "--notebook-dir=/root/lab",
52
77
  "--LabApp.allow_origin='*'",
53
78
  "--LabApp.allow_remote_access=1",
54
79
  ],
@@ -8,19 +8,60 @@ import subprocess
8
8
  import threading
9
9
  import time
10
10
  import webbrowser
11
- from typing import Any, Dict, Tuple
11
+ from typing import Any
12
12
 
13
- from modal import App, Image, Queue, forward
13
+ from modal import App, Image, Queue, Secret, Volume, forward
14
14
 
15
- # Passed by `modal launch` locally via CLI, empty on remote runner.
16
- args: Dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_LOCAL_ARGS", "{}"))
15
+ # Passed by `modal launch` locally via CLI, plumbed to remote runner through secrets.
16
+ args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
17
+
18
+ CODE_SERVER_INSTALLER = "https://code-server.dev/install.sh"
19
+ CODE_SERVER_ENTRYPOINT = (
20
+ "https://raw.githubusercontent.com/coder/code-server/refs/tags/v4.96.1/ci/release-image/entrypoint.sh"
21
+ )
22
+ FIXUD_INSTALLER = "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-$ARCH.tar.gz"
17
23
 
18
24
 
19
25
  app = App()
20
- app.image = Image.from_registry("codercom/code-server", add_python="3.11").dockerfile_commands("ENTRYPOINT []")
26
+ image = (
27
+ Image.from_registry(args.get("image"), add_python="3.11")
28
+ .apt_install("curl", "dumb-init", "git", "git-lfs")
29
+ .run_commands(
30
+ f"curl -fsSL {CODE_SERVER_INSTALLER} | sh",
31
+ f"curl -fsSL {CODE_SERVER_ENTRYPOINT} > /code-server.sh",
32
+ "chmod u+x /code-server.sh",
33
+ )
34
+ .run_commands(
35
+ 'ARCH="$(dpkg --print-architecture)"'
36
+ f' && curl -fsSL "{FIXUD_INSTALLER}" | tar -C /usr/local/bin -xzf - '
37
+ " && chown root:root /usr/local/bin/fixuid"
38
+ " && chmod 4755 /usr/local/bin/fixuid"
39
+ " && mkdir -p /etc/fixuid"
40
+ ' && echo "user: root" >> /etc/fixuid/config.yml'
41
+ ' && echo "group: root" >> /etc/fixuid/config.yml'
42
+ )
43
+ .run_commands("mkdir /home/coder")
44
+ .env({"ENTRYPOINTD": ""})
45
+ )
46
+
47
+ if args.get("mount"):
48
+ image = image.add_local_dir(
49
+ args.get("mount"),
50
+ remote_path="/home/coder/mount",
51
+ )
52
+
53
+ volume = (
54
+ Volume.from_name(
55
+ args.get("volume"),
56
+ create_if_missing=True,
57
+ )
58
+ if args.get("volume")
59
+ else None
60
+ )
61
+ volumes = {"/home/coder/volume": volume} if volume else {}
21
62
 
22
63
 
23
- def wait_for_port(data: Tuple[str, str], q: Queue):
64
+ def wait_for_port(data: tuple[str, str], q: Queue):
24
65
  start_time = time.monotonic()
25
66
  while True:
26
67
  try:
@@ -33,7 +74,16 @@ def wait_for_port(data: Tuple[str, str], q: Queue):
33
74
  q.put(data)
34
75
 
35
76
 
36
- @app.function(cpu=args.get("cpu"), memory=args.get("memory"), gpu=args.get("gpu"), timeout=args.get("timeout"))
77
+ @app.function(
78
+ image=image,
79
+ cpu=args.get("cpu"),
80
+ memory=args.get("memory"),
81
+ gpu=args.get("gpu"),
82
+ timeout=args.get("timeout"),
83
+ secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
84
+ volumes=volumes,
85
+ concurrency_limit=1 if volume else None,
86
+ )
37
87
  def run_vscode(q: Queue):
38
88
  os.chdir("/home/coder")
39
89
  token = secrets.token_urlsafe(13)
@@ -41,7 +91,7 @@ def run_vscode(q: Queue):
41
91
  url = tunnel.url
42
92
  threading.Thread(target=wait_for_port, args=((url, token), q)).start()
43
93
  subprocess.run(
44
- ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."],
94
+ ["/code-server.sh", "--bind-addr", "0.0.0.0:8080", "."],
45
95
  env={**os.environ, "SHELL": "/bin/bash", "PASSWORD": token},
46
96
  )
47
97
  q.put("done")
modal/cli/queues.py ADDED
@@ -0,0 +1,131 @@
1
+ # Copyright Modal Labs 2024
2
+ from typing import Optional
3
+
4
+ import typer
5
+ from rich.console import Console
6
+ from typer import Argument, Option, Typer
7
+
8
+ from modal._resolver import Resolver
9
+ from modal._utils.async_utils import synchronizer
10
+ from modal._utils.grpc_utils import retry_transient_errors
11
+ from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table, timestamp_to_local
12
+ from modal.client import _Client
13
+ from modal.environments import ensure_env
14
+ from modal.queue import _Queue
15
+ from modal_proto import api_pb2
16
+
17
+ queue_cli = Typer(
18
+ name="queue",
19
+ no_args_is_help=True,
20
+ help="Manage `modal.Queue` objects and inspect their contents.",
21
+ )
22
+
23
+ PARTITION_OPTION = Option(
24
+ None,
25
+ "-p",
26
+ "--partition",
27
+ help="Name of the partition to use, otherwise use the default (anonymous) partition.",
28
+ )
29
+
30
+
31
+ @queue_cli.command(name="create", rich_help_panel="Management")
32
+ @synchronizer.create_blocking
33
+ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
34
+ """Create a named Queue.
35
+
36
+ Note: This is a no-op when the Queue already exists.
37
+ """
38
+ q = _Queue.from_name(name, environment_name=env, create_if_missing=True)
39
+ client = await _Client.from_env()
40
+ resolver = Resolver(client=client)
41
+ await resolver.load(q)
42
+
43
+
44
+ @queue_cli.command(name="delete", rich_help_panel="Management")
45
+ @synchronizer.create_blocking
46
+ async def delete(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_OPTION):
47
+ """Delete a named Queue and all of its data."""
48
+ # Lookup first to validate the name, even though delete is a staticmethod
49
+ await _Queue.lookup(name, environment_name=env)
50
+ if not yes:
51
+ typer.confirm(
52
+ f"Are you sure you want to irrevocably delete the modal.Queue '{name}'?",
53
+ default=False,
54
+ abort=True,
55
+ )
56
+ await _Queue.delete(name, environment_name=env)
57
+
58
+
59
+ @queue_cli.command(name="list", rich_help_panel="Management")
60
+ @synchronizer.create_blocking
61
+ async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
62
+ """List all named Queues."""
63
+ env = ensure_env(env)
64
+
65
+ max_total_size = 100_000
66
+ client = await _Client.from_env()
67
+ request = api_pb2.QueueListRequest(environment_name=env, total_size_limit=max_total_size + 1)
68
+ response = await retry_transient_errors(client.stub.QueueList, request)
69
+
70
+ rows = [
71
+ (
72
+ q.name,
73
+ timestamp_to_local(q.created_at, json),
74
+ str(q.num_partitions),
75
+ str(q.total_size) if q.total_size <= max_total_size else f">{max_total_size}",
76
+ )
77
+ for q in response.queues
78
+ ]
79
+ display_table(["Name", "Created at", "Partitions", "Total size"], rows, json)
80
+
81
+
82
+ @queue_cli.command(name="clear", rich_help_panel="Management")
83
+ @synchronizer.create_blocking
84
+ async def clear(
85
+ name: str,
86
+ partition: Optional[str] = PARTITION_OPTION,
87
+ all: bool = Option(False, "-a", "--all", help="Clear the contents of all partitions."),
88
+ yes: bool = YES_OPTION,
89
+ *,
90
+ env: Optional[str] = ENV_OPTION,
91
+ ):
92
+ """Clear the contents of a queue by removing all of its data."""
93
+ q = await _Queue.lookup(name, environment_name=env)
94
+ if not yes:
95
+ typer.confirm(
96
+ f"Are you sure you want to irrevocably delete the contents of modal.Queue '{name}'?",
97
+ default=False,
98
+ abort=True,
99
+ )
100
+ await q.clear(partition=partition, all=all)
101
+
102
+
103
+ @queue_cli.command(name="peek", rich_help_panel="Inspection")
104
+ @synchronizer.create_blocking
105
+ async def peek(
106
+ name: str, n: int = Argument(1), partition: Optional[str] = PARTITION_OPTION, *, env: Optional[str] = ENV_OPTION
107
+ ):
108
+ """Print the next N items in the queue or queue partition (without removal)."""
109
+ q = await _Queue.lookup(name, environment_name=env)
110
+ console = Console()
111
+ i = 0
112
+ async for item in q.iterate(partition=partition):
113
+ console.print(item)
114
+ i += 1
115
+ if i >= n:
116
+ break
117
+
118
+
119
+ @queue_cli.command(name="len", rich_help_panel="Inspection")
120
+ @synchronizer.create_blocking
121
+ async def len(
122
+ name: str,
123
+ partition: Optional[str] = PARTITION_OPTION,
124
+ total: bool = Option(False, "-t", "--total", help="Compute the sum of the queue lengths across all partitions"),
125
+ *,
126
+ env: Optional[str] = ENV_OPTION,
127
+ ):
128
+ """Print the length of a queue partition or the total length of all partitions."""
129
+ q = await _Queue.lookup(name, environment_name=env)
130
+ console = Console()
131
+ console.print(await q.len(partition=partition, total=total))