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
modal/cli/volume.py CHANGED
@@ -2,26 +2,23 @@
2
2
  import os
3
3
  import sys
4
4
  from pathlib import Path
5
- from typing import List, Optional
5
+ from typing import Optional
6
6
 
7
7
  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
- from rich.table import Table
14
12
  from typer import Argument, Option, Typer
15
13
 
16
14
  import modal
17
- from modal._output import step_completed, step_progress
15
+ from modal._output import OutputManager, ProgressHandler
18
16
  from modal._utils.async_utils import synchronizer
19
17
  from modal._utils.grpc_utils import retry_transient_errors
20
18
  from modal.cli._download import _volume_download
21
- from modal.cli.utils import ENV_OPTION, display_table, timestamp_to_local
19
+ from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table, timestamp_to_local
22
20
  from modal.client import _Client
23
21
  from modal.environments import ensure_env
24
- from modal.exception import deprecation_warning
25
22
  from modal.volume import _Volume, _VolumeUploadContextManager
26
23
  from modal_proto import api_pb2
27
24
 
@@ -52,13 +49,14 @@ def humanize_filesize(value: int) -> str:
52
49
  return format % (base * bytes_ / unit) + s
53
50
 
54
51
 
55
- @volume_cli.command(name="create", help="Create a named, persistent modal.Volume.")
52
+ @volume_cli.command(name="create", help="Create a named, persistent modal.Volume.", rich_help_panel="Management")
56
53
  def create(
57
54
  name: str,
58
55
  env: Optional[str] = ENV_OPTION,
56
+ version: Optional[int] = Option(default=None, help="VolumeFS version. (Experimental)"),
59
57
  ):
60
58
  env_name = ensure_env(env)
61
- modal.Volume.create_deployed(name, environment_name=env)
59
+ modal.Volume.create_deployed(name, environment_name=env, version=version)
62
60
  usage_code = f"""
63
61
  @app.function(volumes={{"/my_vol": modal.Volume.from_name("{name}")}})
64
62
  def some_func():
@@ -66,12 +64,12 @@ def some_func():
66
64
  """
67
65
 
68
66
  console = Console()
69
- console.print(f"Created volume '{name}' in environment '{env_name}'. \n\nCode example:\n")
67
+ console.print(f"Created Volume '{name}' in environment '{env_name}'. \n\nCode example:\n")
70
68
  usage = Syntax(usage_code, "python")
71
69
  console.print(usage)
72
70
 
73
71
 
74
- @volume_cli.command(name="get")
72
+ @volume_cli.command(name="get", rich_help_panel="File operations")
75
73
  @synchronizer.create_blocking
76
74
  async def get(
77
75
  volume_name: str,
@@ -80,14 +78,14 @@ async def get(
80
78
  force: bool = False,
81
79
  env: Optional[str] = ENV_OPTION,
82
80
  ):
83
- """Download files from a Volume object.
81
+ """Download files from a modal.Volume object.
84
82
 
85
83
  If a folder is passed for REMOTE_PATH, the contents of the folder will be downloaded
86
84
  recursively, including all subdirectories.
87
85
 
88
86
  **Example**
89
87
 
90
- ```bash
88
+ ```
91
89
  modal volume get <volume_name> logs/april-12-1.txt
92
90
  modal volume get <volume_name> / volume_data_dump
93
91
  ```
@@ -97,12 +95,20 @@ async def get(
97
95
  ensure_env(env)
98
96
  destination = Path(local_destination)
99
97
  volume = await _Volume.lookup(volume_name, environment_name=env)
100
- await _volume_download(volume, remote_path, destination, force)
98
+ console = Console()
99
+ progress_handler = ProgressHandler(type="download", console=console)
100
+ with progress_handler.live:
101
+ await _volume_download(volume, remote_path, destination, force, progress_cb=progress_handler.progress)
102
+ console.print(OutputManager.step_completed("Finished downloading files to local!"))
101
103
 
102
104
 
103
- @volume_cli.command(name="list", help="List the details of all modal.Volume volumes in an environment.")
105
+ @volume_cli.command(
106
+ name="list",
107
+ help="List the details of all modal.Volume volumes in an Environment.",
108
+ rich_help_panel="Management",
109
+ )
104
110
  @synchronizer.create_blocking
105
- async def list(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
111
+ async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
106
112
  env = ensure_env(env)
107
113
  client = await _Client.from_env()
108
114
  response = await retry_transient_errors(client.stub.VolumeList, api_pb2.VolumeListRequest(environment_name=env))
@@ -114,7 +120,11 @@ async def list(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
114
120
  display_table(column_names, rows, json, title=f"Volumes{env_part}")
115
121
 
116
122
 
117
- @volume_cli.command(name="ls", help="List files and directories in a modal.Volume volume.")
123
+ @volume_cli.command(
124
+ name="ls",
125
+ help="List files and directories in a modal.Volume volume.",
126
+ rich_help_panel="File operations",
127
+ )
118
128
  @synchronizer.create_blocking
119
129
  async def ls(
120
130
  volume_name: str,
@@ -134,13 +144,12 @@ async def ls(
134
144
  raise UsageError(exc.message)
135
145
  raise
136
146
 
137
- if sys.stdout.isatty():
138
- console = Console()
139
- console.print(f"Directory listing of '{path}' in '{volume_name}'")
140
- table = Table()
141
- for name in ["filename", "type", "created/modified", "size"]:
142
- table.add_column(name)
143
-
147
+ if not json and not sys.stdout.isatty():
148
+ # Legacy behavior -- I am not sure why exactly we did this originally but I don't want to break it
149
+ for entry in entries:
150
+ print(entry.path)
151
+ else:
152
+ rows = []
144
153
  for entry in entries:
145
154
  if entry.type == api_pb2.FileEntry.FileType.DIRECTORY:
146
155
  filetype = "dir"
@@ -148,26 +157,29 @@ async def ls(
148
157
  filetype = "link"
149
158
  else:
150
159
  filetype = "file"
151
- table.add_row(
152
- entry.path,
153
- filetype,
154
- timestamp_to_local(entry.mtime, False),
155
- humanize_filesize(entry.size),
160
+ rows.append(
161
+ (
162
+ entry.path.encode("unicode_escape").decode("utf-8"),
163
+ filetype,
164
+ timestamp_to_local(entry.mtime, False),
165
+ humanize_filesize(entry.size),
166
+ )
156
167
  )
157
- console.print(table)
158
- else:
159
- for entry in entries:
160
- print(entry.path)
168
+ columns = ["Filename", "Type", "Created/Modified", "Size"]
169
+ title = f"Directory listing of '{path}' in '{volume_name}'"
170
+ display_table(columns, rows, json, title)
161
171
 
162
172
 
163
173
  @volume_cli.command(
164
174
  name="put",
165
- help="""Upload a file or directory to a volume.
175
+ help="""Upload a file or directory to a modal.Volume.
166
176
 
167
177
  Remote parent directories will be created as needed.
168
178
 
169
- 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.
179
+ Ending the REMOTE_PATH with a forward slash (/), it's assumed to be a directory
180
+ and the file will be uploaded with its current name under that directory.
170
181
  """,
182
+ rich_help_panel="File operations",
171
183
  )
172
184
  @synchronizer.create_blocking
173
185
  async def put(
@@ -185,30 +197,36 @@ async def put(
185
197
  if remote_path.endswith("/"):
186
198
  remote_path = remote_path + os.path.basename(local_path)
187
199
  console = Console()
200
+ progress_handler = ProgressHandler(type="upload", console=console)
188
201
 
189
202
  if Path(local_path).is_dir():
190
- spinner = step_progress(f"Uploading directory '{local_path}' to '{remote_path}'...")
191
- with Live(spinner, console=console):
203
+ with progress_handler.live:
192
204
  try:
193
- async with _VolumeUploadContextManager(vol.object_id, vol._client, force=force) as batch:
205
+ async with _VolumeUploadContextManager(
206
+ vol.object_id, vol._client, progress_cb=progress_handler.progress, force=force
207
+ ) as batch:
194
208
  batch.put_directory(local_path, remote_path)
195
209
  except FileExistsError as exc:
196
210
  raise UsageError(str(exc))
197
- console.print(step_completed(f"Uploaded directory '{local_path}' to '{remote_path}'"))
211
+ console.print(OutputManager.step_completed(f"Uploaded directory '{local_path}' to '{remote_path}'"))
198
212
  elif "*" in local_path:
199
213
  raise UsageError("Glob uploads are currently not supported")
200
214
  else:
201
- spinner = step_progress(f"Uploading file '{local_path}' to '{remote_path}'...")
202
- with Live(spinner, console=console):
215
+ with progress_handler.live:
203
216
  try:
204
- async with _VolumeUploadContextManager(vol.object_id, vol._client, force=force) as batch:
217
+ async with _VolumeUploadContextManager(
218
+ vol.object_id, vol._client, progress_cb=progress_handler.progress, force=force
219
+ ) as batch:
205
220
  batch.put_file(local_path, remote_path)
221
+
206
222
  except FileExistsError as exc:
207
223
  raise UsageError(str(exc))
208
- console.print(step_completed(f"Uploaded file '{local_path}' to '{remote_path}'"))
224
+ console.print(OutputManager.step_completed(f"Uploaded file '{local_path}' to '{remote_path}'"))
209
225
 
210
226
 
211
- @volume_cli.command(name="rm", help="Delete a file or directory from a volume.")
227
+ @volume_cli.command(
228
+ name="rm", help="Delete a file or directory from a modal.Volume.", rich_help_panel="File operations"
229
+ )
212
230
  @synchronizer.create_blocking
213
231
  async def rm(
214
232
  volume_name: str,
@@ -229,12 +247,17 @@ async def rm(
229
247
 
230
248
 
231
249
  @volume_cli.command(
232
- name="cp", help="Copy source file to destination file or multiple source files to destination directory."
250
+ name="cp",
251
+ help=(
252
+ "Copy within a modal.Volume. "
253
+ "Copy source file to destination file or multiple source files to destination directory."
254
+ ),
255
+ rich_help_panel="File operations",
233
256
  )
234
257
  @synchronizer.create_blocking
235
258
  async def cp(
236
259
  volume_name: str,
237
- paths: List[str], # accepts multiple paths, last path is treated as destination path
260
+ paths: list[str], # accepts multiple paths, last path is treated as destination path
238
261
  env: Optional[str] = ENV_OPTION,
239
262
  ):
240
263
  ensure_env(env)
@@ -245,27 +268,45 @@ async def cp(
245
268
  await volume.copy_files(src_paths, dst_path)
246
269
 
247
270
 
248
- @volume_cli.command(name="delete", help="Delete a named, persistent modal.Volume.")
271
+ @volume_cli.command(
272
+ name="delete",
273
+ help="Delete a named, persistent modal.Volume.",
274
+ rich_help_panel="Management",
275
+ )
249
276
  @synchronizer.create_blocking
250
277
  async def delete(
251
278
  volume_name: str = Argument(help="Name of the modal.Volume to be deleted. Case sensitive"),
252
- yes: bool = Option(default=False, help="Delete without prompting for confirmation."),
253
- confirm: bool = Option(default=False, help="DEPRECATED: See `--yes` option"),
279
+ yes: bool = YES_OPTION,
254
280
  env: Optional[str] = ENV_OPTION,
255
281
  ):
256
- if confirm:
257
- deprecation_warning(
258
- (2024, 4, 24),
259
- "The `--confirm` option is deprecated; use `--yes` to delete without prompting.",
260
- show_source=False,
282
+ if not yes:
283
+ typer.confirm(
284
+ f"Are you sure you want to irrevocably delete the modal.Volume '{volume_name}'?",
285
+ default=False,
286
+ abort=True,
261
287
  )
262
- yes = True
263
288
 
289
+ await _Volume.delete(volume_name, environment_name=env)
290
+
291
+
292
+ @volume_cli.command(
293
+ name="rename",
294
+ help="Rename a modal.Volume.",
295
+ rich_help_panel="Management",
296
+ )
297
+ @synchronizer.create_blocking
298
+ async def rename(
299
+ old_name: str,
300
+ new_name: str,
301
+ yes: bool = YES_OPTION,
302
+ env: Optional[str] = ENV_OPTION,
303
+ ):
264
304
  if not yes:
265
305
  typer.confirm(
266
- f"Are you sure you want to irrevocably delete the modal.Volume '{volume_name}'?",
306
+ f"Are you sure you want rename the modal.Volume '{old_name}'?"
307
+ " This may break any Apps currently using it.",
267
308
  default=False,
268
309
  abort=True,
269
310
  )
270
311
 
271
- await _Volume.delete(label=volume_name, environment_name=env)
312
+ await _Volume.rename(old_name, new_name, environment_name=env)