modal 1.4.3.dev30__tar.gz → 1.4.4.dev0__tar.gz

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 (214) hide show
  1. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/PKG-INFO +1 -1
  2. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_function_variants.py +2 -10
  3. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_functions.py +5 -8
  4. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_grpc_client.py +0 -7
  5. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/mount_utils.py +1 -1
  6. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/task_command_router_client.py +68 -44
  7. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/app.py +4 -0
  8. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/app.pyi +4 -0
  9. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/shell.py +37 -2
  10. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/client.pyi +2 -2
  11. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/functions.pyi +1 -0
  12. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/image.py +2 -10
  13. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/sandbox.py +2 -10
  14. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/volume.py +110 -12
  15. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/volume.pyi +100 -18
  16. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal.egg-info/PKG-INFO +1 -1
  17. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/api_pb2.pyi +2 -2
  18. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_version/__init__.py +1 -1
  19. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/LICENSE +0 -0
  20. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/README.md +0 -0
  21. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/__init__.py +0 -0
  22. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/__main__.py +0 -0
  23. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_billing.py +0 -0
  24. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_clustered_functions.py +0 -0
  25. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_clustered_functions.pyi +0 -0
  26. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_container_entrypoint.py +0 -0
  27. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_environments.py +0 -0
  28. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_ipython.py +0 -0
  29. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_load_context.py +0 -0
  30. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_location.py +0 -0
  31. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_logs.py +0 -0
  32. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_object.py +0 -0
  33. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_output/__init__.py +0 -0
  34. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_output/manager.py +0 -0
  35. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_output/pty.py +0 -0
  36. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_output/rich.py +0 -0
  37. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_output/status.py +0 -0
  38. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_partial_function.py +0 -0
  39. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_resolver.py +0 -0
  40. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_resources.py +0 -0
  41. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_runtime/__init__.py +0 -0
  42. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_runtime/asgi.py +0 -0
  43. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_runtime/container_io_manager.py +0 -0
  44. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_runtime/container_io_manager.pyi +0 -0
  45. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_runtime/execution_context.py +0 -0
  46. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_runtime/execution_context.pyi +0 -0
  47. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  48. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_runtime/telemetry.py +0 -0
  49. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_runtime/user_code_event_loop.py +0 -0
  50. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_runtime/user_code_imports.py +0 -0
  51. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_serialization.py +0 -0
  52. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_server.py +0 -0
  53. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_traceback.py +0 -0
  54. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_tunnel.py +0 -0
  55. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_tunnel.pyi +0 -0
  56. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_type_manager.py +0 -0
  57. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/__init__.py +0 -0
  58. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/app_utils.py +0 -0
  59. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/async_utils.py +0 -0
  60. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/auth_token_manager.py +0 -0
  61. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/blob_utils.py +0 -0
  62. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/browser_utils.py +0 -0
  63. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/bytes_io_segment_payload.py +0 -0
  64. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/deprecation.py +0 -0
  65. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/docker_utils.py +0 -0
  66. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/function_utils.py +0 -0
  67. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/git_utils.py +0 -0
  68. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/grpc_testing.py +0 -0
  69. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/grpc_utils.py +0 -0
  70. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/hash_utils.py +0 -0
  71. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/http_utils.py +0 -0
  72. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/jwt_utils.py +0 -0
  73. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/logger.py +0 -0
  74. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/name_utils.py +0 -0
  75. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/package_utils.py +0 -0
  76. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/pattern_utils.py +0 -0
  77. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/rand_pb_testing.py +0 -0
  78. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/sandbox_fs_utils.py +0 -0
  79. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/shell_utils.py +0 -0
  80. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_utils/time_utils.py +0 -0
  81. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_vendor/__init__.py +0 -0
  82. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  83. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_vendor/cloudpickle.py +0 -0
  84. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_vendor/tblib.py +0 -0
  85. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_vendor/version.py +0 -0
  86. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/_watcher.py +0 -0
  87. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/billing.py +0 -0
  88. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/builder/2023.12.312.txt +0 -0
  89. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/builder/2023.12.txt +0 -0
  90. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/builder/2024.04.txt +0 -0
  91. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/builder/2024.10.txt +0 -0
  92. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/builder/2025.06.txt +0 -0
  93. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/builder/PREVIEW.txt +0 -0
  94. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/builder/README.md +0 -0
  95. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/builder/base-images.json +0 -0
  96. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/call_graph.py +0 -0
  97. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/__init__.py +0 -0
  98. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/_download.py +0 -0
  99. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/_help.py +0 -0
  100. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/_traceback.py +0 -0
  101. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/app.py +0 -0
  102. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/billing.py +0 -0
  103. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/bootstrap.py +0 -0
  104. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/changelog.py +0 -0
  105. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/cluster.py +0 -0
  106. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/config.py +0 -0
  107. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/container.py +0 -0
  108. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/dashboard.py +0 -0
  109. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/dict.py +0 -0
  110. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/entry_point.py +0 -0
  111. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/environment.py +0 -0
  112. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/import_refs.py +0 -0
  113. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/launch.py +0 -0
  114. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/logo.py +0 -0
  115. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/network_file_system.py +0 -0
  116. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/profile.py +0 -0
  117. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/programs/__init__.py +0 -0
  118. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/programs/run_jupyter.py +0 -0
  119. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/programs/vscode.py +0 -0
  120. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/queues.py +0 -0
  121. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/run.py +0 -0
  122. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/secret.py +0 -0
  123. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/selector.py +0 -0
  124. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/token.py +0 -0
  125. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/utils.py +0 -0
  126. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cli/volume.py +0 -0
  127. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/client.py +0 -0
  128. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cloud_bucket_mount.py +0 -0
  129. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cloud_bucket_mount.pyi +0 -0
  130. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cls.py +0 -0
  131. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/cls.pyi +0 -0
  132. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/config.py +0 -0
  133. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/container_process.py +0 -0
  134. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/container_process.pyi +0 -0
  135. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/dict.py +0 -0
  136. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/dict.pyi +0 -0
  137. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/environments.py +0 -0
  138. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/environments.pyi +0 -0
  139. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/exception.py +0 -0
  140. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/experimental/__init__.py +0 -0
  141. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/experimental/flash.py +0 -0
  142. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/experimental/flash.pyi +0 -0
  143. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/experimental/ipython.py +0 -0
  144. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/file_io.py +0 -0
  145. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/file_io.pyi +0 -0
  146. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/file_pattern_matcher.py +0 -0
  147. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/functions.py +0 -0
  148. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/image.pyi +0 -0
  149. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/io_streams.py +0 -0
  150. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/io_streams.pyi +0 -0
  151. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/mount.py +0 -0
  152. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/mount.pyi +0 -0
  153. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/network_file_system.py +0 -0
  154. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/network_file_system.pyi +0 -0
  155. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/object.py +0 -0
  156. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/object.pyi +0 -0
  157. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/output.py +0 -0
  158. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/parallel_map.py +0 -0
  159. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/parallel_map.pyi +0 -0
  160. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/partial_function.py +0 -0
  161. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/partial_function.pyi +0 -0
  162. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/proxy.py +0 -0
  163. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/proxy.pyi +0 -0
  164. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/py.typed +0 -0
  165. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/queue.py +0 -0
  166. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/queue.pyi +0 -0
  167. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/retries.py +0 -0
  168. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/runner.py +0 -0
  169. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/runner.pyi +0 -0
  170. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/running_app.py +0 -0
  171. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/sandbox.pyi +0 -0
  172. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/sandbox_fs.py +0 -0
  173. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/sandbox_fs.pyi +0 -0
  174. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/schedule.py +0 -0
  175. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/scheduler_placement.py +0 -0
  176. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/secret.py +0 -0
  177. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/secret.pyi +0 -0
  178. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/server.py +0 -0
  179. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/server.pyi +0 -0
  180. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/serving.py +0 -0
  181. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/serving.pyi +0 -0
  182. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/snapshot.py +0 -0
  183. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/snapshot.pyi +0 -0
  184. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/stream_type.py +0 -0
  185. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/token_flow.py +0 -0
  186. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal/token_flow.pyi +0 -0
  187. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal.egg-info/SOURCES.txt +0 -0
  188. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal.egg-info/dependency_links.txt +0 -0
  189. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal.egg-info/entry_points.txt +0 -0
  190. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal.egg-info/requires.txt +0 -0
  191. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal.egg-info/top_level.txt +0 -0
  192. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_docs/__init__.py +0 -0
  193. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_docs/gen_cli_docs.py +0 -0
  194. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_docs/gen_cli_docs_main.py +0 -0
  195. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_docs/gen_reference_docs.py +0 -0
  196. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_docs/gen_reference_docs_main.py +0 -0
  197. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_docs/mdmd/__init__.py +0 -0
  198. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_docs/mdmd/mdmd.py +0 -0
  199. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_docs/mdmd/signatures.py +0 -0
  200. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/__init__.py +0 -0
  201. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/api_grpc.py +0 -0
  202. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/api_pb2.py +0 -0
  203. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/api_pb2_grpc.py +0 -0
  204. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/api_pb2_grpc.pyi +0 -0
  205. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/modal_api_grpc.py +0 -0
  206. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/py.typed +0 -0
  207. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/task_command_router_grpc.py +0 -0
  208. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/task_command_router_pb2.py +0 -0
  209. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/task_command_router_pb2.pyi +0 -0
  210. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  211. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  212. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/modal_version/__main__.py +0 -0
  213. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/pyproject.toml +0 -0
  214. {modal-1.4.3.dev30 → modal-1.4.4.dev0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.4.3.dev30
3
+ Version: 1.4.4.dev0
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -18,7 +18,7 @@ from ._utils.mount_utils import validate_volumes, validate_volumes_by_object_id
18
18
  from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
19
19
  from .retries import Retries
20
20
  from .secret import _Secret
21
- from .volume import _Volume
21
+ from .volume import _Volume, _volume_to_mount_proto
22
22
 
23
23
  if TYPE_CHECKING:
24
24
  from ._functions import _Function
@@ -162,15 +162,7 @@ class _FunctionOptions:
162
162
  # Needs to be called late so that volumes are hydrated
163
163
  validate_volumes_by_object_id(self.validated_volumes)
164
164
 
165
- volume_mounts = [
166
- api_pb2.VolumeMount(
167
- mount_path=path,
168
- volume_id=volume.object_id,
169
- allow_background_commits=True,
170
- read_only=volume._read_only,
171
- )
172
- for path, volume in self.validated_volumes
173
- ]
165
+ volume_mounts = [_volume_to_mount_proto(path, volume) for path, volume in self.validated_volumes]
174
166
  return api_pb2.FunctionOptions(
175
167
  secret_ids=[secret.object_id for secret in self.secrets],
176
168
  replace_secret_ids=bool(self.secrets),
@@ -89,7 +89,7 @@ from .proxy import _Proxy
89
89
  from .retries import Retries, RetryManager
90
90
  from .schedule import Schedule
91
91
  from .secret import _Secret
92
- from .volume import _Volume
92
+ from .volume import _Volume, _volume_to_mount_proto
93
93
 
94
94
  if TYPE_CHECKING:
95
95
  import modal.app
@@ -667,6 +667,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
667
667
  batch_wait_ms: Optional[int] = None,
668
668
  cloud: Optional[str] = None,
669
669
  region: Optional[Union[str, Sequence[str]]] = None,
670
+ routing_region: Optional[str] = None,
670
671
  nonpreemptible: bool = False,
671
672
  is_builder_function: bool = False,
672
673
  is_auto_snapshot: bool = False,
@@ -946,13 +947,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
946
947
 
947
948
  # Relies on dicts being ordered (true as of Python 3.6).
948
949
  volume_mounts = [
949
- api_pb2.VolumeMount(
950
- mount_path=path,
951
- volume_id=volume.object_id,
952
- allow_background_commits=True,
953
- read_only=volume._read_only,
954
- )
955
- for path, volume in validated_volumes_no_cloud_buckets
950
+ _volume_to_mount_proto(path, volume) for path, volume in validated_volumes_no_cloud_buckets
956
951
  ]
957
952
  loaded_mount_ids = {m.object_id for m in all_mounts} | {m.object_id for m in image._mount_layers}
958
953
 
@@ -1033,6 +1028,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1033
1028
  supported_output_formats=supported_output_formats,
1034
1029
  http_config=http_config,
1035
1030
  is_server=is_server,
1031
+ routing_region=routing_region or "",
1036
1032
  )
1037
1033
 
1038
1034
  if isinstance(gpu, list):
@@ -1072,6 +1068,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1072
1068
  supported_output_formats=supported_output_formats,
1073
1069
  http_config=http_config,
1074
1070
  is_server=function_definition.is_server,
1071
+ routing_region=function_definition.routing_region,
1075
1072
  )
1076
1073
 
1077
1074
  ranked_functions = []
@@ -44,10 +44,6 @@ _STATUS_TO_EXCEPTION: dict[Status, type[exception._GRPCErrorWrapper]] = {
44
44
 
45
45
 
46
46
  class grpc_error_converter:
47
- def __init__(self, *, expect_timeouts: bool = False):
48
- """expect_timeouts: if this is set we convert cancelled/deadline_exceeded statuses to exception.TimeoutError"""
49
- self._expect_timeouts = expect_timeouts
50
-
51
47
  def __enter__(self):
52
48
  pass
53
49
 
@@ -56,9 +52,6 @@ class grpc_error_converter:
56
52
  use_full_traceback = config.get("traceback")
57
53
  with suppress_tb_frame():
58
54
  if isinstance(exc, GRPCError):
59
- if self._expect_timeouts and exc.status in (Status.CANCELLED, Status.DEADLINE_EXCEEDED):
60
- raise exception.TimeoutError("Timeout expired")
61
-
62
55
  modal_exc = _STATUS_TO_EXCEPTION[exc.status](exc.message)
63
56
  modal_exc._grpc_message = exc.message
64
57
  modal_exc._grpc_status = exc.status
@@ -68,7 +68,7 @@ def validate_volumes(
68
68
  for path, volume in validated_volumes:
69
69
  if not isinstance(volume, (_Volume, _CloudBucketMount)):
70
70
  raise InvalidError(f"Object of type {type(volume)} mounted at '{path}' is not usable as a volume.")
71
- elif isinstance(volume, (_Volume)):
71
+ elif isinstance(volume, _Volume):
72
72
  volume_to_paths.setdefault(volume, []).append(path)
73
73
  for paths in volume_to_paths.values():
74
74
  if len(paths) > 1:
@@ -74,7 +74,7 @@ async def call_with_retries_on_transient_errors(
74
74
  delay_factor: float = 2,
75
75
  max_retries: Optional[int] = 10,
76
76
  exclude_status_codes: Optional[list[Status]] = None,
77
- expect_timeouts: bool = False, # when True we convert TimeoutError to exception.TimeoutError and don't retry"""
77
+ timeout_deadline: Optional[float] = None,
78
78
  ):
79
79
  """Call func() with transient error retries and exponential backoff.
80
80
 
@@ -84,6 +84,13 @@ async def call_with_retries_on_transient_errors(
84
84
  exclude_status_codes: gRPC status codes to exclude from retry logic even if
85
85
  they are in RETRYABLE_GRPC_STATUS_CODES. Use this to let certain errors
86
86
  (e.g. DEADLINE_EXCEEDED) propagate immediately rather than being retried.
87
+ timeout_deadline: Optional monotonic deadline (`time.monotonic()` value).
88
+ When set, retries are not attempted once the deadline is reached and
89
+ the backoff sleep is clamped so we don't sleep past it. It's up to
90
+ the caller to decide whether to further translate the surfaced
91
+ exception (e.g., into a TimeoutError) based on the deadline check.
92
+ The caller is also responsible for propagating the remaining budget
93
+ into `func()` (typically as the per-call gRPC timeout).
87
94
  """
88
95
  delay_secs = base_delay_secs
89
96
  num_retries = 0
@@ -92,10 +99,22 @@ async def call_with_retries_on_transient_errors(
92
99
  def is_retryable_status(status: Status) -> bool:
93
100
  return status in RETRYABLE_GRPC_STATUS_CODES and status not in exclude_status_codes
94
101
 
95
- async def sleep_and_update_delay_and_num_retries_remaining(e: Exception):
102
+ def can_retry() -> bool:
103
+ if max_retries is not None and num_retries >= max_retries:
104
+ return False
105
+ if timeout_deadline is not None and time.monotonic() >= timeout_deadline:
106
+ return False
107
+ return True
108
+
109
+ async def sleep_and_advance(e: Exception):
96
110
  nonlocal delay_secs, num_retries
97
- logger.debug(f"Retrying RPC with delay {delay_secs}s due to error: {e}")
98
- await asyncio.sleep(delay_secs)
111
+ # Clamp the backoff sleep to the remaining deadline so we don't sleep
112
+ # past it just to fail on the next iteration's deadline check.
113
+ sleep_for = delay_secs
114
+ if timeout_deadline is not None:
115
+ sleep_for = min(sleep_for, max(0.0, timeout_deadline - time.monotonic()))
116
+ logger.debug(f"Retrying RPC with delay {sleep_for}s due to error: {e}")
117
+ await asyncio.sleep(sleep_for)
99
118
  delay_secs *= delay_factor
100
119
  num_retries += 1
101
120
 
@@ -103,37 +122,28 @@ async def call_with_retries_on_transient_errors(
103
122
  try:
104
123
  return await func()
105
124
  except GRPCError as e:
106
- if (max_retries is None or num_retries < max_retries) and is_retryable_status(e.status):
107
- await sleep_and_update_delay_and_num_retries_remaining(e)
108
- else:
109
- raise e
125
+ if not is_retryable_status(e.status) or not can_retry():
126
+ raise
127
+ await sleep_and_advance(e)
110
128
  except AttributeError as e:
111
129
  # StreamTerminatedError are not properly raised in grpclib<=0.4.7
112
130
  # fixed in https://github.com/vmagamedov/grpclib/issues/185
113
131
  # TODO: update to newer version (>=0.4.8) once stable
114
- if (max_retries is None or num_retries < max_retries) and "_write_appdata" in str(e):
115
- await sleep_and_update_delay_and_num_retries_remaining(e)
116
- else:
117
- raise e
132
+ if "_write_appdata" not in str(e) or not can_retry():
133
+ raise
134
+ await sleep_and_advance(e)
118
135
  except StreamTerminatedError as e:
119
- if max_retries is None or num_retries < max_retries:
120
- await sleep_and_update_delay_and_num_retries_remaining(e)
121
- else:
122
- raise e
123
- except asyncio.TimeoutError as e:
124
- if expect_timeouts:
125
- # grpclib raises TimeoutError for the client-side timeout/deadlines
126
- raise ModalTimeoutError("Timeout expired")
127
-
128
- if max_retries is None or num_retries < max_retries:
129
- await sleep_and_update_delay_and_num_retries_remaining(e)
130
- else:
131
- raise ConnectionError(str(e))
132
- except OSError as e:
133
- if max_retries is None or num_retries < max_retries:
134
- await sleep_and_update_delay_and_num_retries_remaining(e)
135
- else:
136
+ if not can_retry():
137
+ raise
138
+ await sleep_and_advance(e)
139
+ except (asyncio.TimeoutError, OSError) as e:
140
+ if not can_retry():
141
+ # Client-side timeout / network OSError surfaces as a generic
142
+ # ConnectionError once we stop retrying. Callers that pass
143
+ # `timeout_deadline` can further translate this based on
144
+ # whether the deadline has elapsed.
136
145
  raise ConnectionError(str(e))
146
+ await sleep_and_advance(e)
137
147
 
138
148
 
139
149
  async def fetch_command_router_access(server_client, task_id: str) -> api_pb2.TaskGetCommandRouterAccessResponse:
@@ -656,20 +666,34 @@ class TaskCommandRouterClient:
656
666
  )
657
667
 
658
668
  async def snapshot_filesystem(
659
- self, request: sr_pb2.TaskSnapshotFilesystemRequest, *, timeout: Optional[int] = None, **kwargs
669
+ self, request: sr_pb2.TaskSnapshotFilesystemRequest, *, timeout: float, **kwargs
660
670
  ) -> sr_pb2.TaskSnapshotFilesystemResponse:
661
- expect_timeouts = timeout is not None
662
- with grpc_error_converter(expect_timeouts=expect_timeouts):
663
- # note: TaskSnapshotFilesystem has a timeout concept with multiple variants:
664
- # * Normally it would time out on the client side inside of grpclib - this causes an asyncio.TimeoutError
665
- # * It also propagates the timeout to the server however, which could potentially trigger, in particular
666
- # if an in-progress requests is idempotently rejoined as a retry. These errors appear to be propagated
667
- # as Status.CANCELLED at the time of writing this, but we may change the backend to return
668
- # DEADLINE_EXCEEDED in the future, so we want to make the code compatible with both here:
669
- return await call_with_retries_on_transient_errors(
670
- lambda: self._call_with_auth_retry(
671
- self._stub.TaskSnapshotFilesystem, request, timeout=timeout, **kwargs
672
- ),
673
- exclude_status_codes=[Status.DEADLINE_EXCEEDED, Status.CANCELLED],
674
- expect_timeouts=expect_timeouts, # client wanted a timeout - raise as such immediately
671
+ # Compute the overall deadline once; each retry attempt passes the
672
+ # remaining budget as the per-call gRPC timeout so we honor the
673
+ # caller-specified `timeout` across retries instead of giving each
674
+ # attempt a fresh full window.
675
+ timeout_deadline = time.monotonic() + timeout
676
+
677
+ def call():
678
+ call_timeout = timeout_deadline - time.monotonic()
679
+ if call_timeout <= 0.0:
680
+ # doesn't matter which exception type this is
681
+ # as it will be caught by the catch all below
682
+ raise ModalTimeoutError("Timeout expired")
683
+
684
+ return self._call_with_auth_retry(
685
+ self._stub.TaskSnapshotFilesystem, request, timeout=call_timeout, **kwargs
675
686
  )
687
+
688
+ # Any failure observed at or after the deadline is treated as a timeout
689
+ try:
690
+ with grpc_error_converter():
691
+ return await call_with_retries_on_transient_errors(
692
+ call,
693
+ exclude_status_codes=[Status.DEADLINE_EXCEEDED, Status.CANCELLED],
694
+ timeout_deadline=timeout_deadline,
695
+ )
696
+ except Exception:
697
+ if time.monotonic() >= timeout_deadline:
698
+ raise ModalTimeoutError("Timeout expired")
699
+ raise
@@ -743,6 +743,7 @@ class _App:
743
743
  ] = None, # Set this to True if it's a non-generator function returning a [sync/async] generator object
744
744
  cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
745
745
  region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
746
+ routing_region: Optional[str] = None, # Region to route inputs to the function through.
746
747
  nonpreemptible: bool = False, # Whether to run the function on a nonpreemptible instance.
747
748
  enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
748
749
  block_network: bool = False, # Whether to block network access
@@ -902,6 +903,7 @@ class _App:
902
903
  startup_timeout=startup_timeout or timeout,
903
904
  cloud=cloud,
904
905
  region=region,
906
+ routing_region=routing_region,
905
907
  nonpreemptible=nonpreemptible,
906
908
  webhook_config=webhook_config,
907
909
  enable_memory_snapshot=enable_memory_snapshot,
@@ -957,6 +959,7 @@ class _App:
957
959
  startup_timeout: Optional[int] = None, # Maximum startup time in seconds with higher precedence than `timeout`.
958
960
  cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
959
961
  region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
962
+ routing_region: Optional[str] = None, # Region to route inputs to the function through.
960
963
  nonpreemptible: bool = False, # Whether to run the function on a non-preemptible instance.
961
964
  enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
962
965
  block_network: bool = False, # Whether to block network access
@@ -1100,6 +1103,7 @@ class _App:
1100
1103
  startup_timeout=startup_timeout or timeout,
1101
1104
  cloud=cloud,
1102
1105
  region=region,
1106
+ routing_region=routing_region,
1103
1107
  nonpreemptible=nonpreemptible,
1104
1108
  enable_memory_snapshot=enable_memory_snapshot,
1105
1109
  block_network=block_network,
@@ -490,6 +490,7 @@ class _App:
490
490
  is_generator: typing.Optional[bool] = None,
491
491
  cloud: typing.Optional[str] = None,
492
492
  region: typing.Union[str, collections.abc.Sequence[str], None] = None,
493
+ routing_region: typing.Optional[str] = None,
493
494
  nonpreemptible: bool = False,
494
495
  enable_memory_snapshot: bool = False,
495
496
  block_network: bool = False,
@@ -537,6 +538,7 @@ class _App:
537
538
  startup_timeout: typing.Optional[int] = None,
538
539
  cloud: typing.Optional[str] = None,
539
540
  region: typing.Union[str, collections.abc.Sequence[str], None] = None,
541
+ routing_region: typing.Optional[str] = None,
540
542
  nonpreemptible: bool = False,
541
543
  enable_memory_snapshot: bool = False,
542
544
  block_network: bool = False,
@@ -1208,6 +1210,7 @@ class App:
1208
1210
  is_generator: typing.Optional[bool] = None,
1209
1211
  cloud: typing.Optional[str] = None,
1210
1212
  region: typing.Union[str, collections.abc.Sequence[str], None] = None,
1213
+ routing_region: typing.Optional[str] = None,
1211
1214
  nonpreemptible: bool = False,
1212
1215
  enable_memory_snapshot: bool = False,
1213
1216
  block_network: bool = False,
@@ -1255,6 +1258,7 @@ class App:
1255
1258
  startup_timeout: typing.Optional[int] = None,
1256
1259
  cloud: typing.Optional[str] = None,
1257
1260
  region: typing.Union[str, collections.abc.Sequence[str], None] = None,
1261
+ routing_region: typing.Optional[str] = None,
1258
1262
  nonpreemptible: bool = False,
1259
1263
  enable_memory_snapshot: bool = False,
1260
1264
  block_network: bool = False,
@@ -58,6 +58,23 @@ def _passed_forbidden_args(
58
58
  return passed_forbidden
59
59
 
60
60
 
61
+ def _parse_experimental_options(options: Iterable[str]) -> dict[str, bool]:
62
+ parsed: dict[str, bool] = {}
63
+ for option in options:
64
+ key, sep, raw_value = option.partition("=")
65
+ if not key or not sep:
66
+ raise click.BadParameter("must use KEY=VALUE")
67
+
68
+ value = raw_value.lower()
69
+ if value in {"1", "true"}:
70
+ parsed[key] = True
71
+ elif value in {"0", "false"}:
72
+ parsed[key] = False
73
+ else:
74
+ raise click.BadParameter("BOOL must be one of true, false, 1, or 0")
75
+ return parsed
76
+
77
+
61
78
  def _is_valid_modal_id(ref: str, prefix: str) -> bool:
62
79
  assert prefix.endswith("-")
63
80
  return ref.startswith(prefix) and len(ref[len(prefix) :]) > 0 and ref[len(prefix) :].isalnum()
@@ -180,6 +197,7 @@ def _start_shell_from_function_spec(
180
197
  timeout: int,
181
198
  function_spec: _FunctionSpec,
182
199
  pty: bool,
200
+ experimental_options: dict[str, bool],
183
201
  ) -> None:
184
202
  interactive_shell(
185
203
  app,
@@ -198,6 +216,7 @@ def _start_shell_from_function_spec(
198
216
  region=function_spec.scheduler_placement.regions if function_spec.scheduler_placement else None,
199
217
  pty=pty,
200
218
  proxy=function_spec.proxy,
219
+ experimental_options=experimental_options,
201
220
  )
202
221
 
203
222
 
@@ -216,6 +235,7 @@ def _start_shell_from_image(
216
235
  cloud: Optional[str],
217
236
  region: Optional[str],
218
237
  pty: bool,
238
+ experimental_options: dict[str, bool],
219
239
  ) -> None:
220
240
  volumes = {f"/mnt/{vol}": Volume.from_name(vol) for vol in volume}
221
241
  secrets = [Secret.from_name(s) for s in secret]
@@ -246,6 +266,7 @@ def _start_shell_from_image(
246
266
  secrets=secrets,
247
267
  region=region.split(",") if region else [],
248
268
  pty=pty,
269
+ experimental_options=experimental_options,
249
270
  )
250
271
 
251
272
 
@@ -298,6 +319,14 @@ def _start_shell_from_image(
298
319
  default=False,
299
320
  help="Interpret argument as a Python module path instead of a file/script path",
300
321
  )
322
+ @click.option(
323
+ "--experimental-option",
324
+ "experimental_options",
325
+ multiple=True,
326
+ default=(),
327
+ hidden=True,
328
+ metavar="KEY=VALUE",
329
+ )
301
330
  def shell(
302
331
  ref: Optional[str] = None,
303
332
  cmd: str = "/bin/bash",
@@ -314,6 +343,7 @@ def shell(
314
343
  region: Optional[str] = None,
315
344
  pty: Optional[bool] = None,
316
345
  use_module_mode: bool = False,
346
+ experimental_options: tuple[str, ...] = (),
317
347
  ):
318
348
  """Run a command or interactive shell inside a Modal container.
319
349
 
@@ -384,12 +414,16 @@ def shell(
384
414
  # NB: invoking under bash makes --cmd a lot more flexible.
385
415
  cmds = shlex.split(f'/bin/bash -c "{cmd}"')
386
416
  timeout = 3600
417
+ parsed_experimental_options = _parse_experimental_options(experimental_options)
387
418
 
388
419
  if ref is not None and not _is_valid_modal_id(ref, "im-"):
389
420
  # If ref it not a Modal Image ID, then it's a function reference, and we'll start a new container from its spec.
390
421
  ctx = click.get_current_context()
391
422
  if passed_forbidden := _passed_forbidden_args(
392
- shell.params, ctx, locals(), allowed=lambda p: p in {"cmd", "env", "pty", "ref", "use_module_mode"}
423
+ shell.params,
424
+ ctx,
425
+ locals(),
426
+ allowed=lambda p: p in {"cmd", "env", "pty", "ref", "use_module_mode", "experimental_options"},
393
427
  ):
394
428
  raise ClickException(
395
429
  f"Cannot specify container configuration arguments ({', '.join(passed_forbidden)}) "
@@ -397,7 +431,7 @@ def shell(
397
431
  )
398
432
 
399
433
  function_spec = _function_spec_from_ref(ref, use_module_mode)
400
- _start_shell_from_function_spec(app, cmds, env, timeout, function_spec, pty)
434
+ _start_shell_from_function_spec(app, cmds, env, timeout, function_spec, pty, parsed_experimental_options)
401
435
  return
402
436
 
403
437
  if ref is not None and _is_valid_modal_id(ref, "im-"):
@@ -428,4 +462,5 @@ def shell(
428
462
  cloud,
429
463
  region,
430
464
  pty,
465
+ parsed_experimental_options,
431
466
  )
@@ -35,7 +35,7 @@ class _Client:
35
35
  server_url: str,
36
36
  client_type: int,
37
37
  credentials: typing.Optional[tuple[str, str]],
38
- version: str = "1.4.3.dev30",
38
+ version: str = "1.4.4.dev0",
39
39
  ):
40
40
  """mdmd:hidden
41
41
  The Modal client object is not intended to be instantiated directly by users.
@@ -175,7 +175,7 @@ class Client:
175
175
  server_url: str,
176
176
  client_type: int,
177
177
  credentials: typing.Optional[tuple[str, str]],
178
- version: str = "1.4.3.dev30",
178
+ version: str = "1.4.4.dev0",
179
179
  ):
180
180
  """mdmd:hidden
181
181
  The Modal client object is not intended to be instantiated directly by users.
@@ -97,6 +97,7 @@ class Function(
97
97
  batch_wait_ms: typing.Optional[int] = None,
98
98
  cloud: typing.Optional[str] = None,
99
99
  region: typing.Union[str, collections.abc.Sequence[str], None] = None,
100
+ routing_region: typing.Optional[str] = None,
100
101
  nonpreemptible: bool = False,
101
102
  is_builder_function: bool = False,
102
103
  is_auto_snapshot: bool = False,
@@ -59,7 +59,7 @@ from .mount import _Mount, python_standalone_mount_name
59
59
  from .network_file_system import _NetworkFileSystem
60
60
  from .output import OutputManager
61
61
  from .secret import _Secret
62
- from .volume import _Volume
62
+ from .volume import _Volume, _volume_to_mount_proto
63
63
 
64
64
  if typing.TYPE_CHECKING:
65
65
  import modal._functions
@@ -631,15 +631,7 @@ class _Image(_Object, type_prefix="im"):
631
631
  validate_volumes_by_object_id(validated_volumes)
632
632
 
633
633
  # Relies on dicts being ordered (true as of Python 3.6).
634
- volume_mounts = [
635
- api_pb2.VolumeMount(
636
- mount_path=path,
637
- volume_id=volume.object_id,
638
- allow_background_commits=True,
639
- read_only=volume._read_only,
640
- )
641
- for path, volume in validated_volumes
642
- ]
634
+ volume_mounts = [_volume_to_mount_proto(path, volume) for path, volume in validated_volumes]
643
635
 
644
636
  image_definition = api_pb2.Image(
645
637
  base_images=base_images_pb2s,
@@ -21,7 +21,7 @@ from google.protobuf.message import Message
21
21
  from modal._tunnel import Tunnel
22
22
  from modal.cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
23
23
  from modal.mount import _Mount
24
- from modal.volume import _Volume
24
+ from modal.volume import _Volume, _volume_to_mount_proto
25
25
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
26
26
 
27
27
  from ._load_context import LoadContext
@@ -301,15 +301,7 @@ class _Sandbox(_Object, type_prefix="sb"):
301
301
  validate_volumes_by_object_id(validated_volumes)
302
302
 
303
303
  # Relies on dicts being ordered (true as of Python 3.6).
304
- volume_mounts = [
305
- api_pb2.VolumeMount(
306
- mount_path=path,
307
- volume_id=volume.object_id,
308
- allow_background_commits=True,
309
- read_only=volume._read_only,
310
- )
311
- for path, volume in validated_volumes
312
- ]
304
+ volume_mounts = [_volume_to_mount_proto(path, volume) for path, volume in validated_volumes]
313
305
 
314
306
  open_ports = [api_pb2.PortSpec(port=port, unencrypted=False) for port in encrypted_ports]
315
307
  open_ports.extend([api_pb2.PortSpec(port=port, unencrypted=True) for port in unencrypted_ports])