modal 0.62.16__py3-none-any.whl → 0.72.11__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 +17 -13
  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 +420 -937
  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 -59
  11. modal/_resources.py +51 -0
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1036 -0
  15. modal/_runtime/execution_context.py +89 -0
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +134 -9
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +52 -16
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +479 -100
  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 +460 -171
  29. modal/_utils/grpc_testing.py +47 -31
  30. modal/_utils/grpc_utils.py +62 -109
  31. modal/_utils/hash_utils.py +61 -19
  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 +5 -7
  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 +14 -12
  43. modal/app.py +1003 -314
  44. modal/app.pyi +540 -264
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +63 -53
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +205 -45
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +62 -14
  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 +64 -58
  55. modal/cli/launch.py +32 -18
  56. modal/cli/network_file_system.py +64 -83
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +35 -10
  59. modal/cli/programs/vscode.py +60 -10
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +234 -131
  62. modal/cli/secret.py +8 -7
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +79 -10
  65. modal/cli/volume.py +110 -109
  66. modal/client.py +250 -144
  67. modal/client.pyi +157 -118
  68. modal/cloud_bucket_mount.py +108 -34
  69. modal/cloud_bucket_mount.pyi +32 -38
  70. modal/cls.py +535 -148
  71. modal/cls.pyi +190 -146
  72. modal/config.py +41 -19
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +111 -65
  76. modal/dict.pyi +136 -131
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +34 -43
  80. modal/experimental.py +61 -2
  81. modal/extensions/ipython.py +5 -5
  82. modal/file_io.py +537 -0
  83. modal/file_io.pyi +235 -0
  84. modal/file_pattern_matcher.py +197 -0
  85. modal/functions.py +906 -911
  86. modal/functions.pyi +466 -430
  87. modal/gpu.py +57 -44
  88. modal/image.py +1089 -479
  89. modal/image.pyi +584 -228
  90. modal/io_streams.py +434 -0
  91. modal/io_streams.pyi +122 -0
  92. modal/mount.py +314 -101
  93. modal/mount.pyi +241 -235
  94. modal/network_file_system.py +92 -92
  95. modal/network_file_system.pyi +152 -110
  96. modal/object.py +67 -36
  97. modal/object.pyi +166 -143
  98. modal/output.py +63 -0
  99. modal/parallel_map.py +434 -0
  100. modal/parallel_map.pyi +75 -0
  101. modal/partial_function.py +282 -117
  102. modal/partial_function.pyi +222 -129
  103. modal/proxy.py +15 -12
  104. modal/proxy.pyi +3 -8
  105. modal/queue.py +182 -65
  106. modal/queue.pyi +218 -118
  107. modal/requirements/2024.04.txt +29 -0
  108. modal/requirements/2024.10.txt +16 -0
  109. modal/requirements/README.md +21 -0
  110. modal/requirements/base-images.json +22 -0
  111. modal/retries.py +48 -7
  112. modal/runner.py +459 -156
  113. modal/runner.pyi +135 -71
  114. modal/running_app.py +38 -0
  115. modal/sandbox.py +514 -236
  116. modal/sandbox.pyi +397 -169
  117. modal/schedule.py +4 -4
  118. modal/scheduler_placement.py +20 -3
  119. modal/secret.py +56 -31
  120. modal/secret.pyi +62 -42
  121. modal/serving.py +51 -56
  122. modal/serving.pyi +44 -36
  123. modal/stream_type.py +15 -0
  124. modal/token_flow.py +5 -3
  125. modal/token_flow.pyi +37 -32
  126. modal/volume.py +285 -157
  127. modal/volume.pyi +249 -184
  128. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
  129. modal-0.72.11.dist-info/RECORD +174 -0
  130. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  131. modal_docs/gen_reference_docs.py +3 -1
  132. modal_docs/mdmd/mdmd.py +0 -1
  133. modal_docs/mdmd/signatures.py +5 -2
  134. modal_global_objects/images/base_images.py +28 -0
  135. modal_global_objects/mounts/python_standalone.py +2 -2
  136. modal_proto/__init__.py +1 -1
  137. modal_proto/api.proto +1288 -533
  138. modal_proto/api_grpc.py +856 -456
  139. modal_proto/api_pb2.py +2165 -1157
  140. modal_proto/api_pb2.pyi +8859 -0
  141. modal_proto/api_pb2_grpc.py +1674 -855
  142. modal_proto/api_pb2_grpc.pyi +1416 -0
  143. modal_proto/modal_api_grpc.py +149 -0
  144. modal_proto/modal_options_grpc.py +3 -0
  145. modal_proto/options_pb2.pyi +20 -0
  146. modal_proto/options_pb2_grpc.pyi +7 -0
  147. modal_proto/py.typed +0 -0
  148. modal_version/__init__.py +1 -1
  149. modal_version/_version_generated.py +2 -2
  150. modal/_asgi.py +0 -370
  151. modal/_container_entrypoint.pyi +0 -378
  152. modal/_container_exec.py +0 -128
  153. modal/_sandbox_shell.py +0 -49
  154. modal/shared_volume.py +0 -23
  155. modal/shared_volume.pyi +0 -24
  156. modal/stub.py +0 -783
  157. modal/stub.pyi +0 -332
  158. modal-0.62.16.dist-info/RECORD +0 -198
  159. modal_global_objects/images/conda.py +0 -15
  160. modal_global_objects/images/debian_slim.py +0 -15
  161. modal_global_objects/images/micromamba.py +0 -15
  162. test/__init__.py +0 -1
  163. test/aio_test.py +0 -12
  164. test/async_utils_test.py +0 -262
  165. test/blob_test.py +0 -67
  166. test/cli_imports_test.py +0 -149
  167. test/cli_test.py +0 -659
  168. test/client_test.py +0 -194
  169. test/cls_test.py +0 -630
  170. test/config_test.py +0 -137
  171. test/conftest.py +0 -1420
  172. test/container_app_test.py +0 -32
  173. test/container_test.py +0 -1389
  174. test/cpu_test.py +0 -23
  175. test/decorator_test.py +0 -85
  176. test/deprecation_test.py +0 -34
  177. test/dict_test.py +0 -33
  178. test/e2e_test.py +0 -68
  179. test/error_test.py +0 -7
  180. test/function_serialization_test.py +0 -32
  181. test/function_test.py +0 -653
  182. test/function_utils_test.py +0 -101
  183. test/gpu_test.py +0 -159
  184. test/grpc_utils_test.py +0 -141
  185. test/helpers.py +0 -42
  186. test/image_test.py +0 -669
  187. test/live_reload_test.py +0 -80
  188. test/lookup_test.py +0 -70
  189. test/mdmd_test.py +0 -329
  190. test/mount_test.py +0 -162
  191. test/mounted_files_test.py +0 -329
  192. test/network_file_system_test.py +0 -181
  193. test/notebook_test.py +0 -66
  194. test/object_test.py +0 -41
  195. test/package_utils_test.py +0 -25
  196. test/queue_test.py +0 -97
  197. test/resolver_test.py +0 -58
  198. test/retries_test.py +0 -67
  199. test/runner_test.py +0 -85
  200. test/sandbox_test.py +0 -191
  201. test/schedule_test.py +0 -15
  202. test/scheduler_placement_test.py +0 -29
  203. test/secret_test.py +0 -78
  204. test/serialization_test.py +0 -42
  205. test/stub_composition_test.py +0 -10
  206. test/stub_test.py +0 -360
  207. test/test_asgi_wrapper.py +0 -234
  208. test/token_flow_test.py +0 -18
  209. test/traceback_test.py +0 -135
  210. test/tunnel_test.py +0 -29
  211. test/utils_test.py +0 -88
  212. test/version_test.py +0 -14
  213. test/volume_test.py +0 -341
  214. test/watcher_test.py +0 -30
  215. test/webhook_test.py +0 -146
  216. /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
  217. /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
  218. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
@@ -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 Image, Queue, Stub, 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
17
 
18
+ app = App()
18
19
 
19
- stub = Stub()
20
- stub.image = Image.from_registry(args.get("image"), add_python=args.get("add_python")).pip_install("jupyterlab")
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
- @stub.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
  ],
@@ -58,7 +83,7 @@ def run_jupyter(q: Queue):
58
83
  q.put("done")
59
84
 
60
85
 
61
- @stub.local_entrypoint()
86
+ @app.local_entrypoint()
62
87
  def main():
63
88
  with Queue.ephemeral() as q:
64
89
  run_jupyter.spawn(q)
@@ -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 Image, Queue, Stub, 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
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"
18
23
 
19
- stub = Stub()
20
- stub.image = Image.from_registry("codercom/code-server", add_python="3.11").dockerfile_commands("ENTRYPOINT []")
21
24
 
25
+ app = App()
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
+ )
22
46
 
23
- def wait_for_port(data: Tuple[str, str], q: Queue):
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 {}
62
+
63
+
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
- @stub.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,13 +91,13 @@ 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")
48
98
 
49
99
 
50
- @stub.local_entrypoint()
100
+ @app.local_entrypoint()
51
101
  def main():
52
102
  with Queue.ephemeral() as q:
53
103
  run_vscode.spawn(q)
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))