modAL 1.5.3.dev9__tar.gz → 1.5.3.dev11__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 (232) hide show
  1. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/PKG-INFO +1 -1
  2. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_function_variants.py +1 -1
  3. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_functions.py +1 -1
  4. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/task_command_router_client.py +5 -0
  5. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/client.pyi +2 -2
  6. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cloud_bucket_mount.py +15 -3
  7. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cloud_bucket_mount.pyi +2 -2
  8. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/exception.py +4 -0
  9. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/sandbox.py +156 -30
  10. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/sandbox.pyi +143 -9
  11. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/secret.py +9 -0
  12. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/secret.pyi +18 -0
  13. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/snapshot.py +17 -1
  14. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/snapshot.pyi +22 -0
  15. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal.egg-info/PKG-INFO +1 -1
  16. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/api_grpc.py +16 -0
  17. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/api_pb2.py +507 -439
  18. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/api_pb2.pyi +108 -1
  19. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/api_pb2_grpc.py +33 -0
  20. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/api_pb2_grpc.pyi +10 -0
  21. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/modal_api_grpc.py +1 -0
  22. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_version/__init__.py +1 -1
  23. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/pyproject.toml +0 -1
  24. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/LICENSE +0 -0
  25. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/README.md +0 -0
  26. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/__init__.py +0 -0
  27. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/__main__.py +0 -0
  28. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_billing.py +0 -0
  29. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_clustered_functions.py +0 -0
  30. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_clustered_functions.pyi +0 -0
  31. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_container_entrypoint.py +0 -0
  32. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_environments.py +0 -0
  33. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_grpc_client.py +0 -0
  34. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_image.py +0 -0
  35. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_ipython.py +0 -0
  36. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_load_context.py +0 -0
  37. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_location.py +0 -0
  38. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_logs.py +0 -0
  39. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_object.py +0 -0
  40. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_output/__init__.py +0 -0
  41. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_output/manager.py +0 -0
  42. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_output/pty.py +0 -0
  43. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_output/rich.py +0 -0
  44. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_output/status.py +0 -0
  45. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_partial_function.py +0 -0
  46. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_resolver.py +0 -0
  47. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_resources.py +0 -0
  48. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/__init__.py +0 -0
  49. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/asgi.py +0 -0
  50. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/container_io_manager.py +0 -0
  51. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/container_io_manager.pyi +0 -0
  52. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/execution_context.py +0 -0
  53. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/execution_context.pyi +0 -0
  54. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  55. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/task_lifecycle_manager.py +0 -0
  56. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  57. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/telemetry.py +0 -0
  58. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/user_code_event_loop.py +0 -0
  59. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_runtime/user_code_imports.py +0 -0
  60. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_serialization.py +0 -0
  61. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_server.py +0 -0
  62. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_traceback.py +0 -0
  63. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_tunnel.py +0 -0
  64. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_tunnel.pyi +0 -0
  65. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_type_manager.py +0 -0
  66. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/__init__.py +0 -0
  67. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/app_utils.py +0 -0
  68. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/async_utils.py +0 -0
  69. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/auth_token_manager.py +0 -0
  70. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/blob_utils.py +0 -0
  71. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/browser_utils.py +0 -0
  72. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/bytes_io_segment_payload.py +0 -0
  73. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/curl_utils.py +0 -0
  74. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/deprecation.py +0 -0
  75. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/docker_utils.py +0 -0
  76. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/function_utils.py +0 -0
  77. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/git_utils.py +0 -0
  78. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/grpc_testing.py +0 -0
  79. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/grpc_utils.py +0 -0
  80. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/hash_utils.py +0 -0
  81. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/http_utils.py +0 -0
  82. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/jwt_utils.py +0 -0
  83. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/logger.py +0 -0
  84. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/mount_utils.py +0 -0
  85. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/name_utils.py +0 -0
  86. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/package_utils.py +0 -0
  87. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/pattern_utils.py +0 -0
  88. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/proxy_support.py +0 -0
  89. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/rand_pb_testing.py +0 -0
  90. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/sandbox_fs_utils.py +0 -0
  91. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/shell_utils.py +0 -0
  92. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_utils/time_utils.py +0 -0
  93. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_vendor/__init__.py +0 -0
  94. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  95. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_vendor/cloudpickle.py +0 -0
  96. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_vendor/tblib.py +0 -0
  97. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_vendor/version.py +0 -0
  98. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_watcher.py +0 -0
  99. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/_workspace.py +0 -0
  100. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/app.py +0 -0
  101. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/app.pyi +0 -0
  102. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/billing.py +0 -0
  103. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/billing.pyi +0 -0
  104. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/builder/2023.12.312.txt +0 -0
  105. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/builder/2023.12.txt +0 -0
  106. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/builder/2024.04.txt +0 -0
  107. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/builder/2024.10.txt +0 -0
  108. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/builder/2025.06.txt +0 -0
  109. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/builder/PREVIEW.txt +0 -0
  110. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/builder/README.md +0 -0
  111. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/builder/base-images.json +0 -0
  112. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/call_graph.py +0 -0
  113. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/__init__.py +0 -0
  114. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/_download.py +0 -0
  115. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/_help.py +0 -0
  116. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/_traceback.py +0 -0
  117. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/app.py +0 -0
  118. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/billing.py +0 -0
  119. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/bootstrap.py +0 -0
  120. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/changelog.py +0 -0
  121. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/cluster.py +0 -0
  122. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/config.py +0 -0
  123. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/container.py +0 -0
  124. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/curl.py +0 -0
  125. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/dashboard.py +0 -0
  126. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/dict.py +0 -0
  127. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/endpoint.py +0 -0
  128. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/entry_point.py +0 -0
  129. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/environment.py +0 -0
  130. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/image.py +0 -0
  131. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/import_refs.py +0 -0
  132. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/launch.py +0 -0
  133. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/logo.py +0 -0
  134. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/network_file_system.py +0 -0
  135. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/profile.py +0 -0
  136. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/programs/__init__.py +0 -0
  137. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/programs/run_jupyter.py +0 -0
  138. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/programs/vscode.py +0 -0
  139. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/queues.py +0 -0
  140. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/run.py +0 -0
  141. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/secret.py +0 -0
  142. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/selector.py +0 -0
  143. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/shell.py +0 -0
  144. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/skills.py +0 -0
  145. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/token.py +0 -0
  146. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/utils.py +0 -0
  147. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/volume.py +0 -0
  148. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cli/workspace.py +0 -0
  149. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/client.py +0 -0
  150. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cls.py +0 -0
  151. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/cls.pyi +0 -0
  152. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/config.py +0 -0
  153. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/container_process.py +0 -0
  154. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/container_process.pyi +0 -0
  155. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/dict.py +0 -0
  156. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/dict.pyi +0 -0
  157. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/environments.py +0 -0
  158. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/environments.pyi +0 -0
  159. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/experimental/__init__.py +0 -0
  160. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/experimental/flash.py +0 -0
  161. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/experimental/flash.pyi +0 -0
  162. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/experimental/ipython.py +0 -0
  163. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/file_io.py +0 -0
  164. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/file_io.pyi +0 -0
  165. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/file_pattern_matcher.py +0 -0
  166. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/functions.py +0 -0
  167. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/functions.pyi +0 -0
  168. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/image.py +0 -0
  169. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/image.pyi +0 -0
  170. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/io_streams.py +0 -0
  171. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/io_streams.pyi +0 -0
  172. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/mount.py +0 -0
  173. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/mount.pyi +0 -0
  174. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/network_file_system.py +0 -0
  175. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/network_file_system.pyi +0 -0
  176. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/object.py +0 -0
  177. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/object.pyi +0 -0
  178. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/output.py +0 -0
  179. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/parallel_map.py +0 -0
  180. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/parallel_map.pyi +0 -0
  181. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/partial_function.py +0 -0
  182. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/partial_function.pyi +0 -0
  183. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/proxy.py +0 -0
  184. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/proxy.pyi +0 -0
  185. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/py.typed +0 -0
  186. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/queue.py +0 -0
  187. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/queue.pyi +0 -0
  188. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/retries.py +0 -0
  189. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/runner.py +0 -0
  190. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/runner.pyi +0 -0
  191. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/running_app.py +0 -0
  192. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/sandbox_fs.py +0 -0
  193. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/sandbox_fs.pyi +0 -0
  194. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/schedule.py +0 -0
  195. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/scheduler_placement.py +0 -0
  196. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/server.py +0 -0
  197. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/server.pyi +0 -0
  198. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/serving.py +0 -0
  199. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/serving.pyi +0 -0
  200. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/skills/modal/SKILL.md +0 -0
  201. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/stream_type.py +0 -0
  202. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/token_flow.py +0 -0
  203. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/token_flow.pyi +0 -0
  204. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/types.py +0 -0
  205. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/volume.py +0 -0
  206. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/volume.pyi +0 -0
  207. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/workspace.py +0 -0
  208. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal/workspace.pyi +0 -0
  209. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal.egg-info/SOURCES.txt +0 -0
  210. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal.egg-info/dependency_links.txt +0 -0
  211. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal.egg-info/entry_points.txt +0 -0
  212. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal.egg-info/requires.txt +0 -0
  213. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal.egg-info/top_level.txt +0 -0
  214. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_docs/__init__.py +0 -0
  215. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_docs/gen_cli_docs.py +0 -0
  216. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_docs/gen_cli_docs_main.py +0 -0
  217. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_docs/gen_reference_docs.py +0 -0
  218. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_docs/gen_reference_docs_main.py +0 -0
  219. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_docs/mdmd/__init__.py +0 -0
  220. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_docs/mdmd/mdmd.py +0 -0
  221. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_docs/mdmd/signatures.py +0 -0
  222. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_docs/mdmd/types.py +0 -0
  223. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_docs/reference_intro.md +0 -0
  224. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/__init__.py +0 -0
  225. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/py.typed +0 -0
  226. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/task_command_router_grpc.py +0 -0
  227. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/task_command_router_pb2.py +0 -0
  228. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/task_command_router_pb2.pyi +0 -0
  229. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  230. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  231. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/modal_version/__main__.py +0 -0
  232. {modal-1.5.3.dev9 → modal-1.5.3.dev11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.3.dev9
3
+ Version: 1.5.3.dev11
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -171,7 +171,7 @@ class _FunctionOptions:
171
171
  replace_secret_ids=bool(self.secrets),
172
172
  replace_volume_mounts=len(volume_mounts) > 0,
173
173
  volume_mounts=volume_mounts,
174
- cloud_bucket_mounts=cloud_bucket_mounts_to_proto(self.cloud_bucket_mounts),
174
+ cloud_bucket_mounts=cloud_bucket_mounts_to_proto(self.cloud_bucket_mounts)[0],
175
175
  replace_cloud_bucket_mounts=bool(self.cloud_bucket_mounts),
176
176
  resources=self.resources,
177
177
  retry_policy=self.retry_policy,
@@ -1000,7 +1000,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1000
1000
  untrusted=restrict_modal_access,
1001
1001
  single_use_containers=single_use_containers,
1002
1002
  max_inputs=int(single_use_containers), # TODO(michael) remove after worker rollover
1003
- cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
1003
+ cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts)[0],
1004
1004
  scheduler_placement=scheduler_placement,
1005
1005
  is_class=info.is_service_class(),
1006
1006
  class_parameter_info=info.class_parameter_info(),
@@ -974,3 +974,8 @@ class TaskCommandRouterClient:
974
974
  return await self._unary_call_with_deadline(
975
975
  self._stub.TaskSnapshotFilesystem, request, timeout=timeout, **kwargs
976
976
  )
977
+
978
+ async def snapshot_memory(
979
+ self, request: sr_pb2.TaskSnapshotMemoryRequest, *, timeout: float, **kwargs
980
+ ) -> sr_pb2.TaskSnapshotMemoryResponse:
981
+ return await self._unary_call_with_deadline(self._stub.TaskSnapshotMemory, request, timeout=timeout, **kwargs)
@@ -37,7 +37,7 @@ class _Client:
37
37
  server_url: str,
38
38
  client_type: int,
39
39
  credentials: typing.Optional[tuple[str, str]],
40
- version: str = "1.5.3.dev9",
40
+ version: str = "1.5.3.dev11",
41
41
  ):
42
42
  """mdmd:hidden
43
43
  The Modal client object is not intended to be instantiated directly by users.
@@ -207,7 +207,7 @@ class Client:
207
207
  server_url: str,
208
208
  client_type: int,
209
209
  credentials: typing.Optional[tuple[str, str]],
210
- version: str = "1.5.3.dev9",
210
+ version: str = "1.5.3.dev11",
211
211
  ):
212
212
  """mdmd:hidden
213
213
  The Modal client object is not intended to be instantiated directly by users.
@@ -121,11 +121,14 @@ class _CloudBucketMount:
121
121
  force_path_style: bool = False
122
122
 
123
123
 
124
- def cloud_bucket_mounts_to_proto(mounts: Sequence[tuple[str, _CloudBucketMount]]) -> list[api_pb2.CloudBucketMount]:
124
+ def cloud_bucket_mounts_to_proto(
125
+ mounts: Sequence[tuple[str, _CloudBucketMount]], split_ephemeral_credentials: bool = False
126
+ ) -> tuple[list[api_pb2.CloudBucketMount], dict[str, api_pb2.StringMap]]:
125
127
  """mdmd:hidden
126
128
  Helper function to convert `CloudBucketMount` to a list of protobufs that can be passed to the server.
127
129
  """
128
130
  cloud_bucket_mounts: list[api_pb2.CloudBucketMount] = []
131
+ cloud_bucket_credentials: dict[str, api_pb2.StringMap] = {}
129
132
 
130
133
  for path, mount in mounts:
131
134
  # crude mapping from mount arguments to type.
@@ -153,11 +156,20 @@ def cloud_bucket_mounts_to_proto(mounts: Sequence[tuple[str, _CloudBucketMount]]
153
156
  else:
154
157
  key_prefix = mount.key_prefix
155
158
 
159
+ credentials_secret_id = ""
160
+ if secret := mount.secret:
161
+ if split_ephemeral_credentials and secret._load_env_dict:
162
+ env_dict = secret._load_env_dict()
163
+ credientials = api_pb2.StringMap(contents=env_dict)
164
+ cloud_bucket_credentials[path] = credientials
165
+ else:
166
+ credentials_secret_id = secret.object_id
167
+
156
168
  cloud_bucket_mount = api_pb2.CloudBucketMount(
157
169
  bucket_name=mount.bucket_name,
158
170
  bucket_endpoint_url=mount.bucket_endpoint_url,
159
171
  mount_path=path,
160
- credentials_secret_id=mount.secret.object_id if mount.secret else "",
172
+ credentials_secret_id=credentials_secret_id,
161
173
  read_only=mount.read_only,
162
174
  bucket_type=bucket_type,
163
175
  requester_pays=mount.requester_pays,
@@ -167,7 +179,7 @@ def cloud_bucket_mounts_to_proto(mounts: Sequence[tuple[str, _CloudBucketMount]]
167
179
  )
168
180
  cloud_bucket_mounts.append(cloud_bucket_mount)
169
181
 
170
- return cloud_bucket_mounts
182
+ return cloud_bucket_mounts, cloud_bucket_credentials
171
183
 
172
184
 
173
185
  CloudBucketMount = synchronize_api(_CloudBucketMount)
@@ -126,8 +126,8 @@ class _CloudBucketMount:
126
126
  ...
127
127
 
128
128
  def cloud_bucket_mounts_to_proto(
129
- mounts: collections.abc.Sequence[tuple[str, _CloudBucketMount]],
130
- ) -> list[modal_proto.api_pb2.CloudBucketMount]:
129
+ mounts: collections.abc.Sequence[tuple[str, _CloudBucketMount]], split_ephemeral_credentials: bool = False
130
+ ) -> tuple[list[modal_proto.api_pb2.CloudBucketMount], dict[str, modal_proto.api_pb2.StringMap]]:
131
131
  """mdmd:hidden
132
132
  Helper function to convert `CloudBucketMount` to a list of protobufs that can be passed to the server.
133
133
  """
@@ -188,6 +188,10 @@ class SandboxTimeoutError(TimeoutError):
188
188
  """Raised when a Sandbox exceeds its execution duration limit and times out."""
189
189
 
190
190
 
191
+ class SnapshotCreationError(Error):
192
+ """Raised when a Sandbox fails to create an exit snapshot."""
193
+
194
+
191
195
  class ExecTimeoutError(TimeoutError):
192
196
  """Raised when a container process exceeds its execution duration limit and times out."""
193
197
 
@@ -52,10 +52,13 @@ from .exception import (
52
52
  ClientClosed,
53
53
  ConflictError,
54
54
  ExecutionError,
55
+ InternalError,
55
56
  InvalidError,
56
57
  NotFoundError,
57
58
  SandboxTerminatedError,
58
59
  SandboxTimeoutError,
60
+ SnapshotCreationError,
61
+ TimeoutError,
59
62
  )
60
63
  from .file_io import _FileIO, ls, mkdir, rm, watch
61
64
  from .io_streams import (
@@ -77,6 +80,8 @@ from .stream_type import StreamType
77
80
  from .types import FileWatchEvent, FileWatchEventType, SandboxConnectCredentials
78
81
 
79
82
  _default_image: _Image = _Image.debian_slim()
83
+ _EXIT_SNAPSHOT_POLL_INTERVAL_SECONDS = 1.0
84
+ _EXIT_SNAPSHOT_NOT_FOUND_ERROR_CODES = frozenset((api_pb2.SandboxGetExitSnapshotResponse.ERROR_CODE_TIMEOUT,))
80
85
 
81
86
 
82
87
  async def _gather_load_with_timings(
@@ -472,7 +477,7 @@ class _Sandbox(_Object, type_prefix="sb"):
472
477
  nfs_mounts=network_file_system_mount_protos(validated_network_file_systems),
473
478
  runtime=config.get("function_runtime"),
474
479
  runtime_debug=config.get("function_runtime_debug"),
475
- cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
480
+ cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts)[0],
476
481
  volume_mounts=volume_mounts,
477
482
  pty_info=pty_info,
478
483
  scheduler_placement=scheduler_placement,
@@ -845,6 +850,7 @@ class _Sandbox(_Object, type_prefix="sb"):
845
850
  verbose: bool = False,
846
851
  custom_domain: str | None = None,
847
852
  client: _Client | None = None,
853
+ _experimental_enable_snapshot: bool = False,
848
854
  ) -> "_Sandbox":
849
855
  """Create a sandbox using the V2 backend.
850
856
 
@@ -860,8 +866,9 @@ class _Sandbox(_Object, type_prefix="sb"):
860
866
  tuple that additionally sets a hard limit (fractional CPU cores; memory
861
867
  in MiB), matching `Sandbox.create()`.
862
868
 
863
- Features like memory snapshots, network file systems, and GPUs are not
864
- supported.
869
+ Pass `_experimental_enable_snapshot=True` to create a sandbox that can be
870
+ snapshotted with `._experimental_snapshot()`. Features like network
871
+ file systems and GPUs are not supported.
865
872
 
866
873
  Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
867
874
  workspace can address each other directly at their `i6pn.modal.local`
@@ -961,8 +968,9 @@ class _Sandbox(_Object, type_prefix="sb"):
961
968
  for _, vol in validated_volumes:
962
969
  dep_tasks.append(resolver.load(vol, load_context))
963
970
  for _, cloud_bucket_mount in cloud_bucket_mounts:
964
- if cloud_bucket_mount.secret:
965
- dep_tasks.append(resolver.load(cloud_bucket_mount.secret, load_context))
971
+ if secret := cloud_bucket_mount.secret:
972
+ if not secret._is_ephemeral:
973
+ dep_tasks.append(resolver.load(cloud_bucket_mount.secret, load_context))
966
974
  if proxy:
967
975
  dep_tasks.append(resolver.load(proxy, load_context))
968
976
  dep_timings = await _gather_load_with_timings(dep_tasks) if dep_tasks else []
@@ -971,6 +979,10 @@ class _Sandbox(_Object, type_prefix="sb"):
971
979
 
972
980
  volume_mounts = [_volume_to_mount_proto(path, volume) for path, volume in validated_volumes]
973
981
 
982
+ cloud_bucket_mount_protos, cloud_bucket_credientials = cloud_bucket_mounts_to_proto(
983
+ cloud_bucket_mounts, split_ephemeral_credentials=True
984
+ )
985
+
974
986
  definition = api_pb2.Sandbox(
975
987
  entrypoint_args=args,
976
988
  image_id=image.object_id,
@@ -995,12 +1007,13 @@ class _Sandbox(_Object, type_prefix="sb"):
995
1007
  inbound_cidr_allowlist=list(inbound_cidr_allowlist) if inbound_cidr_allowlist is not None else [],
996
1008
  i6pn_enabled=i6pn,
997
1009
  volume_mounts=volume_mounts,
998
- cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
1010
+ cloud_bucket_mounts=cloud_bucket_mount_protos,
999
1011
  readiness_probe=(readiness_probe._to_proto() if readiness_probe else None),
1000
1012
  experimental_options_v2=(
1001
1013
  {k: str(v) for k, v in experimental_options.items()} if experimental_options else None
1002
1014
  ),
1003
1015
  custom_domain=custom_domain,
1016
+ enable_snapshot=_experimental_enable_snapshot,
1004
1017
  )
1005
1018
 
1006
1019
  tag_protos = [api_pb2.SandboxTag(tag_name=k, tag_value=v) for k, v in tags.items()] if tags else []
@@ -1009,6 +1022,7 @@ class _Sandbox(_Object, type_prefix="sb"):
1009
1022
  definition=definition,
1010
1023
  ephemeral_secrets=api_pb2.StringMap(contents=env_dict) if env_dict else None,
1011
1024
  tags=tag_protos,
1025
+ cloud_bucket_mount_credentials=cloud_bucket_credientials,
1012
1026
  )
1013
1027
  assert load_context.client._auth_token_manager
1014
1028
  auth_token = await load_context.client._auth_token_manager.get_token()
@@ -1401,6 +1415,68 @@ class _Sandbox(_Object, type_prefix="sb"):
1401
1415
  req = sr_pb2.TaskSetNetworkAccessRequest(task_id=task_id, network_access=network_access)
1402
1416
  await command_router_client.set_network_access(req)
1403
1417
 
1418
+ async def _experimental_get_exit_snapshot(self, timeout: float | None = 60) -> _Image:
1419
+ """Get the exit filesystem snapshot image.
1420
+
1421
+ Args:
1422
+ timeout: Client-side deadline in seconds (default 60). Use `None` to
1423
+ poll until the snapshot reaches a terminal state. Use `0` to
1424
+ perform an immediate check.
1425
+
1426
+ Returns:
1427
+ The exit snapshot Image.
1428
+
1429
+ Raises:
1430
+ InvalidError: If `timeout` is negative, or if exit snapshot is not
1431
+ enabled for the sandbox.
1432
+ TimeoutError: If `timeout` elapses before the snapshot reaches a
1433
+ terminal state. This includes `timeout=0` when the snapshot is
1434
+ still pending.
1435
+ SnapshotCreationError: If no exit snapshot image will be produced.
1436
+ NotFoundError: If the sandbox does not exist.
1437
+ PermissionDeniedError: If the caller cannot access the sandbox.
1438
+ InternalError: If persisted snapshot state is malformed.
1439
+ ServiceError: If a transient client/server communication failure occurs.
1440
+ """
1441
+ if timeout is not None and timeout < 0:
1442
+ raise InvalidError("timeout must be non-negative or None")
1443
+
1444
+ deadline = None if timeout is None else time.monotonic() + timeout
1445
+ timeout_message = f"timed out waiting for exit snapshot for Sandbox {self.object_id}"
1446
+
1447
+ while True:
1448
+ resp: api_pb2.SandboxGetExitSnapshotResponse = await self._client.stub.SandboxGetExitSnapshot(
1449
+ api_pb2.SandboxGetExitSnapshotRequest(sandbox_id=self.object_id)
1450
+ )
1451
+ outcome = resp.WhichOneof("outcome")
1452
+
1453
+ if outcome == "success":
1454
+ if not resp.success.image_id:
1455
+ raise InternalError("Exit snapshot result is missing image ID")
1456
+ return _Image._new_hydrated(resp.success.image_id, self._client, None)
1457
+
1458
+ if outcome == "error":
1459
+ if resp.error.error_code in _EXIT_SNAPSHOT_NOT_FOUND_ERROR_CODES:
1460
+ message = resp.error.message or "No exit snapshot image will be produced"
1461
+ raise SnapshotCreationError(message)
1462
+ message = resp.error.message or "Exit snapshot state is malformed"
1463
+ raise InternalError(message)
1464
+
1465
+ if outcome != "pending":
1466
+ raise InternalError("Exit snapshot response is missing an outcome")
1467
+
1468
+ sleep_for = _EXIT_SNAPSHOT_POLL_INTERVAL_SECONDS
1469
+ if deadline is not None:
1470
+ remaining = deadline - time.monotonic()
1471
+ if remaining <= 0:
1472
+ raise TimeoutError(timeout_message)
1473
+ sleep_for = min(sleep_for, remaining)
1474
+
1475
+ await asyncio.sleep(sleep_for)
1476
+
1477
+ if deadline is not None and time.monotonic() >= deadline:
1478
+ raise TimeoutError(timeout_message)
1479
+
1404
1480
  async def snapshot_filesystem(
1405
1481
  self,
1406
1482
  timeout: int = 55,
@@ -2079,19 +2155,27 @@ class _Sandbox(_Object, type_prefix="sb"):
2079
2155
  )
2080
2156
 
2081
2157
  async def _experimental_snapshot(self) -> _SandboxSnapshot:
2082
- self._ensure_v1("_experimental_snapshot")
2083
- await self._get_task_id()
2084
- snap_req = api_pb2.SandboxSnapshotRequest(sandbox_id=self.object_id)
2085
- snap_resp = await self._client.stub.SandboxSnapshot(snap_req)
2158
+ if self._is_v2:
2159
+ task_id = await self._get_task_id()
2160
+ command_router_client = await self._get_command_router_client(task_id)
2161
+ snap_v2_resp = await command_router_client.snapshot_memory(
2162
+ sr_pb2.TaskSnapshotMemoryRequest(task_id=task_id, idempotency_key=str(uuid.uuid4())),
2163
+ timeout=55.0,
2164
+ )
2165
+ snapshot_id = snap_v2_resp.snapshot_id
2166
+ else:
2167
+ await self._get_task_id()
2168
+ snap_req = api_pb2.SandboxSnapshotRequest(sandbox_id=self.object_id)
2169
+ snap_resp = await self._client.stub.SandboxSnapshot(snap_req)
2086
2170
 
2087
- snapshot_id = snap_resp.snapshot_id
2171
+ snapshot_id = snap_resp.snapshot_id
2088
2172
 
2089
- # wait for the snapshot to succeed. this is implemented as a second idempotent rpc
2090
- # because the snapshot itself may take a while to complete.
2091
- wait_req = api_pb2.SandboxSnapshotWaitRequest(snapshot_id=snapshot_id, timeout=55.0)
2092
- wait_resp = await self._client.stub.SandboxSnapshotWait(wait_req)
2093
- if wait_resp.result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
2094
- raise ExecutionError(wait_resp.result.exception)
2173
+ # wait for the snapshot to succeed. this is implemented as a second idempotent rpc
2174
+ # because the snapshot itself may take a while to complete.
2175
+ wait_req = api_pb2.SandboxSnapshotWaitRequest(snapshot_id=snapshot_id, timeout=55.0)
2176
+ wait_resp = await self._client.stub.SandboxSnapshotWait(wait_req)
2177
+ if wait_resp.result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
2178
+ raise ExecutionError(wait_resp.result.exception)
2095
2179
 
2096
2180
  async def _load(
2097
2181
  self: _SandboxSnapshot, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None
@@ -2102,7 +2186,8 @@ class _Sandbox(_Object, type_prefix="sb"):
2102
2186
  rep = "SandboxSnapshot()"
2103
2187
  # TODO: use ._new_hydrated instead
2104
2188
  obj = _SandboxSnapshot._from_loader(_load, rep, hydrate_lazily=True, load_context_overrides=LoadContext.empty())
2105
- obj._hydrate(snapshot_id, self._client, None)
2189
+ metadata = api_pb2.SandboxSnapshotHandleMetadata(is_v2=self._is_v2)
2190
+ obj._hydrate(snapshot_id, self._client, metadata)
2106
2191
 
2107
2192
  return obj
2108
2193
 
@@ -2113,27 +2198,41 @@ class _Sandbox(_Object, type_prefix="sb"):
2113
2198
  *,
2114
2199
  name: str | None = _DEFAULT_SANDBOX_NAME_OVERRIDE,
2115
2200
  ):
2201
+ """Restore a Sandbox from a memory snapshot.
2202
+
2203
+ The restore targets the same backend the snapshot was taken from. A V1
2204
+ snapshot restores as a V1 sandbox, a V2 snapshot as a V2 sandbox.
2205
+ """
2116
2206
  client = client or await _Client.from_env()
2117
2207
 
2208
+ if snapshot._is_v2 is None:
2209
+ await snapshot.hydrate(client=client)
2210
+ use_v2 = bool(snapshot._is_v2)
2211
+
2118
2212
  if name is not None and name != _DEFAULT_SANDBOX_NAME_OVERRIDE:
2119
2213
  check_object_name(name, "Sandbox")
2120
2214
 
2121
2215
  if name is _DEFAULT_SANDBOX_NAME_OVERRIDE:
2122
- restore_req = api_pb2.SandboxRestoreRequest(
2123
- snapshot_id=snapshot.object_id,
2124
- sandbox_name_override_type=api_pb2.SandboxRestoreRequest.SANDBOX_NAME_OVERRIDE_TYPE_UNSPECIFIED,
2125
- )
2216
+ override_type = api_pb2.SandboxRestoreRequest.SANDBOX_NAME_OVERRIDE_TYPE_UNSPECIFIED
2217
+ name_override = None
2126
2218
  elif name is None:
2127
- restore_req = api_pb2.SandboxRestoreRequest(
2128
- snapshot_id=snapshot.object_id,
2129
- sandbox_name_override_type=api_pb2.SandboxRestoreRequest.SANDBOX_NAME_OVERRIDE_TYPE_NONE,
2130
- )
2219
+ override_type = api_pb2.SandboxRestoreRequest.SANDBOX_NAME_OVERRIDE_TYPE_NONE
2220
+ name_override = None
2131
2221
  else:
2132
- restore_req = api_pb2.SandboxRestoreRequest(
2133
- snapshot_id=snapshot.object_id,
2134
- sandbox_name_override=name,
2135
- sandbox_name_override_type=api_pb2.SandboxRestoreRequest.SANDBOX_NAME_OVERRIDE_TYPE_STRING,
2222
+ override_type = api_pb2.SandboxRestoreRequest.SANDBOX_NAME_OVERRIDE_TYPE_STRING
2223
+ name_override = name
2224
+
2225
+ if use_v2:
2226
+ return await _Sandbox._experimental_from_snapshot_v2(
2227
+ snapshot, client, override_type=override_type, name_override=name_override
2136
2228
  )
2229
+
2230
+ restore_req = api_pb2.SandboxRestoreRequest(
2231
+ snapshot_id=snapshot.object_id,
2232
+ sandbox_name_override_type=override_type,
2233
+ )
2234
+ if name_override is not None:
2235
+ restore_req.sandbox_name_override = name_override
2137
2236
  # Pin the restored Sandbox to a specific worker when MODAL_WORKER_ID is
2138
2237
  # set.
2139
2238
  if worker_id := config.get("worker_id"):
@@ -2153,6 +2252,33 @@ class _Sandbox(_Object, type_prefix="sb"):
2153
2252
  raise ExecutionError(resp.task_result.exception)
2154
2253
  return sandbox
2155
2254
 
2255
+ @staticmethod
2256
+ async def _experimental_from_snapshot_v2(
2257
+ snapshot: _SandboxSnapshot,
2258
+ client: _Client,
2259
+ *,
2260
+ override_type: "api_pb2.SandboxRestoreRequest.SandboxNameOverrideType.ValueType",
2261
+ name_override: str | None,
2262
+ ) -> "_Sandbox":
2263
+ restore_req = api_pb2.SandboxRestoreV2Request(
2264
+ snapshot_id=snapshot.object_id,
2265
+ sandbox_name_override_type=override_type,
2266
+ )
2267
+ if name_override is not None:
2268
+ restore_req.sandbox_name_override = name_override
2269
+ if worker_id := config.get("worker_id"):
2270
+ restore_req.worker_id = worker_id
2271
+
2272
+ assert client._auth_token_manager
2273
+ auth_token = await client._auth_token_manager.get_token()
2274
+ restore_resp: api_pb2.SandboxRestoreV2Response = await client.stub.SandboxRestoreV2(
2275
+ restore_req, metadata=[("x-modal-auth-token", auth_token)]
2276
+ )
2277
+
2278
+ sandbox = await _Sandbox.from_id(restore_resp.sandbox_id, client)
2279
+ sandbox._task_id = restore_resp.task_id
2280
+ return sandbox
2281
+
2156
2282
  @property
2157
2283
  def filesystem(self) -> _SandboxFilesystem:
2158
2284
  """Namespace for Sandbox filesystem APIs."""
@@ -393,6 +393,7 @@ class _Sandbox(modal._object._Object):
393
393
  verbose: bool = False,
394
394
  custom_domain: typing.Optional[str] = None,
395
395
  client: typing.Optional[modal.client._Client] = None,
396
+ _experimental_enable_snapshot: bool = False,
396
397
  ) -> _Sandbox:
397
398
  """Create a sandbox using the V2 backend.
398
399
 
@@ -408,8 +409,9 @@ class _Sandbox(modal._object._Object):
408
409
  tuple that additionally sets a hard limit (fractional CPU cores; memory
409
410
  in MiB), matching `Sandbox.create()`.
410
411
 
411
- Features like memory snapshots, network file systems, and GPUs are not
412
- supported.
412
+ Pass `_experimental_enable_snapshot=True` to create a sandbox that can be
413
+ snapshotted with `._experimental_snapshot()`. Features like network
414
+ file systems and GPUs are not supported.
413
415
 
414
416
  Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
415
417
  workspace can address each other directly at their `i6pn.modal.local`
@@ -571,6 +573,31 @@ class _Sandbox(modal._object._Object):
571
573
  """
572
574
  ...
573
575
 
576
+ async def _experimental_get_exit_snapshot(self, timeout: typing.Optional[float] = 60) -> modal._image._Image:
577
+ """Get the exit filesystem snapshot image.
578
+
579
+ Args:
580
+ timeout: Client-side deadline in seconds (default 60). Use `None` to
581
+ poll until the snapshot reaches a terminal state. Use `0` to
582
+ perform an immediate check.
583
+
584
+ Returns:
585
+ The exit snapshot Image.
586
+
587
+ Raises:
588
+ InvalidError: If `timeout` is negative, or if exit snapshot is not
589
+ enabled for the sandbox.
590
+ TimeoutError: If `timeout` elapses before the snapshot reaches a
591
+ terminal state. This includes `timeout=0` when the snapshot is
592
+ still pending.
593
+ SnapshotCreationError: If no exit snapshot image will be produced.
594
+ NotFoundError: If the sandbox does not exist.
595
+ PermissionDeniedError: If the caller cannot access the sandbox.
596
+ InternalError: If persisted snapshot state is malformed.
597
+ ServiceError: If a transient client/server communication failure occurs.
598
+ """
599
+ ...
600
+
574
601
  async def snapshot_filesystem(
575
602
  self, timeout: int = 55, *, ttl: typing.Optional[int] = 2592000
576
603
  ) -> modal._image._Image:
@@ -852,7 +879,22 @@ class _Sandbox(modal._object._Object):
852
879
  client: typing.Optional[modal.client._Client] = None,
853
880
  *,
854
881
  name: typing.Optional[str] = _DEFAULT_SANDBOX_NAME_OVERRIDE,
855
- ): ...
882
+ ):
883
+ """Restore a Sandbox from a memory snapshot.
884
+
885
+ The restore targets the same backend the snapshot was taken from. A V1
886
+ snapshot restores as a V1 sandbox, a V2 snapshot as a V2 sandbox.
887
+ """
888
+ ...
889
+
890
+ @staticmethod
891
+ async def _experimental_from_snapshot_v2(
892
+ snapshot: modal.snapshot._SandboxSnapshot,
893
+ client: modal.client._Client,
894
+ *,
895
+ override_type: int,
896
+ name_override: typing.Optional[str],
897
+ ) -> _Sandbox: ...
856
898
  @property
857
899
  def filesystem(self) -> modal.sandbox_fs._SandboxFilesystem:
858
900
  """Namespace for Sandbox filesystem APIs."""
@@ -1768,6 +1810,7 @@ class Sandbox(modal.object.Object):
1768
1810
  verbose: bool = False,
1769
1811
  custom_domain: typing.Optional[str] = None,
1770
1812
  client: typing.Optional[modal.client.Client] = None,
1813
+ _experimental_enable_snapshot: bool = False,
1771
1814
  ) -> Sandbox:
1772
1815
  """Create a sandbox using the V2 backend.
1773
1816
 
@@ -1783,8 +1826,9 @@ class Sandbox(modal.object.Object):
1783
1826
  tuple that additionally sets a hard limit (fractional CPU cores; memory
1784
1827
  in MiB), matching `Sandbox.create()`.
1785
1828
 
1786
- Features like memory snapshots, network file systems, and GPUs are not
1787
- supported.
1829
+ Pass `_experimental_enable_snapshot=True` to create a sandbox that can be
1830
+ snapshotted with `._experimental_snapshot()`. Features like network
1831
+ file systems and GPUs are not supported.
1788
1832
 
1789
1833
  Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
1790
1834
  workspace can address each other directly at their `i6pn.modal.local`
@@ -1837,6 +1881,7 @@ class Sandbox(modal.object.Object):
1837
1881
  verbose: bool = False,
1838
1882
  custom_domain: typing.Optional[str] = None,
1839
1883
  client: typing.Optional[modal.client.Client] = None,
1884
+ _experimental_enable_snapshot: bool = False,
1840
1885
  ) -> Sandbox:
1841
1886
  """Create a sandbox using the V2 backend.
1842
1887
 
@@ -1852,8 +1897,9 @@ class Sandbox(modal.object.Object):
1852
1897
  tuple that additionally sets a hard limit (fractional CPU cores; memory
1853
1898
  in MiB), matching `Sandbox.create()`.
1854
1899
 
1855
- Features like memory snapshots, network file systems, and GPUs are not
1856
- supported.
1900
+ Pass `_experimental_enable_snapshot=True` to create a sandbox that can be
1901
+ snapshotted with `._experimental_snapshot()`. Features like network
1902
+ file systems and GPUs are not supported.
1857
1903
 
1858
1904
  Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
1859
1905
  workspace can address each other directly at their `i6pn.modal.local`
@@ -2177,6 +2223,59 @@ class Sandbox(modal.object.Object):
2177
2223
 
2178
2224
  _experimental_set_outbound_network_policy: ___experimental_set_outbound_network_policy_spec
2179
2225
 
2226
+ class ___experimental_get_exit_snapshot_spec(typing_extensions.Protocol):
2227
+ def __call__(self, /, timeout: typing.Optional[float] = 60) -> modal.image.Image:
2228
+ """Get the exit filesystem snapshot image.
2229
+
2230
+ Args:
2231
+ timeout: Client-side deadline in seconds (default 60). Use `None` to
2232
+ poll until the snapshot reaches a terminal state. Use `0` to
2233
+ perform an immediate check.
2234
+
2235
+ Returns:
2236
+ The exit snapshot Image.
2237
+
2238
+ Raises:
2239
+ InvalidError: If `timeout` is negative, or if exit snapshot is not
2240
+ enabled for the sandbox.
2241
+ TimeoutError: If `timeout` elapses before the snapshot reaches a
2242
+ terminal state. This includes `timeout=0` when the snapshot is
2243
+ still pending.
2244
+ SnapshotCreationError: If no exit snapshot image will be produced.
2245
+ NotFoundError: If the sandbox does not exist.
2246
+ PermissionDeniedError: If the caller cannot access the sandbox.
2247
+ InternalError: If persisted snapshot state is malformed.
2248
+ ServiceError: If a transient client/server communication failure occurs.
2249
+ """
2250
+ ...
2251
+
2252
+ async def aio(self, /, timeout: typing.Optional[float] = 60) -> modal.image.Image:
2253
+ """Get the exit filesystem snapshot image.
2254
+
2255
+ Args:
2256
+ timeout: Client-side deadline in seconds (default 60). Use `None` to
2257
+ poll until the snapshot reaches a terminal state. Use `0` to
2258
+ perform an immediate check.
2259
+
2260
+ Returns:
2261
+ The exit snapshot Image.
2262
+
2263
+ Raises:
2264
+ InvalidError: If `timeout` is negative, or if exit snapshot is not
2265
+ enabled for the sandbox.
2266
+ TimeoutError: If `timeout` elapses before the snapshot reaches a
2267
+ terminal state. This includes `timeout=0` when the snapshot is
2268
+ still pending.
2269
+ SnapshotCreationError: If no exit snapshot image will be produced.
2270
+ NotFoundError: If the sandbox does not exist.
2271
+ PermissionDeniedError: If the caller cannot access the sandbox.
2272
+ InternalError: If persisted snapshot state is malformed.
2273
+ ServiceError: If a transient client/server communication failure occurs.
2274
+ """
2275
+ ...
2276
+
2277
+ _experimental_get_exit_snapshot: ___experimental_get_exit_snapshot_spec
2278
+
2180
2279
  class __snapshot_filesystem_spec(typing_extensions.Protocol):
2181
2280
  def __call__(self, /, timeout: int = 55, *, ttl: typing.Optional[int] = 2592000) -> modal.image.Image:
2182
2281
  """Snapshot the filesystem of the Sandbox.
@@ -2802,7 +2901,14 @@ class Sandbox(modal.object.Object):
2802
2901
  client: typing.Optional[modal.client.Client] = None,
2803
2902
  *,
2804
2903
  name: typing.Optional[str] = _DEFAULT_SANDBOX_NAME_OVERRIDE,
2805
- ): ...
2904
+ ):
2905
+ """Restore a Sandbox from a memory snapshot.
2906
+
2907
+ The restore targets the same backend the snapshot was taken from. A V1
2908
+ snapshot restores as a V1 sandbox, a V2 snapshot as a V2 sandbox.
2909
+ """
2910
+ ...
2911
+
2806
2912
  async def aio(
2807
2913
  self,
2808
2914
  /,
@@ -2810,10 +2916,38 @@ class Sandbox(modal.object.Object):
2810
2916
  client: typing.Optional[modal.client.Client] = None,
2811
2917
  *,
2812
2918
  name: typing.Optional[str] = _DEFAULT_SANDBOX_NAME_OVERRIDE,
2813
- ): ...
2919
+ ):
2920
+ """Restore a Sandbox from a memory snapshot.
2921
+
2922
+ The restore targets the same backend the snapshot was taken from. A V1
2923
+ snapshot restores as a V1 sandbox, a V2 snapshot as a V2 sandbox.
2924
+ """
2925
+ ...
2814
2926
 
2815
2927
  _experimental_from_snapshot: typing.ClassVar[___experimental_from_snapshot_spec]
2816
2928
 
2929
+ class ___experimental_from_snapshot_v2_spec(typing_extensions.Protocol):
2930
+ def __call__(
2931
+ self,
2932
+ /,
2933
+ snapshot: modal.snapshot.SandboxSnapshot,
2934
+ client: modal.client.Client,
2935
+ *,
2936
+ override_type: int,
2937
+ name_override: typing.Optional[str],
2938
+ ) -> Sandbox: ...
2939
+ async def aio(
2940
+ self,
2941
+ /,
2942
+ snapshot: modal.snapshot.SandboxSnapshot,
2943
+ client: modal.client.Client,
2944
+ *,
2945
+ override_type: int,
2946
+ name_override: typing.Optional[str],
2947
+ ) -> Sandbox: ...
2948
+
2949
+ _experimental_from_snapshot_v2: typing.ClassVar[___experimental_from_snapshot_v2_spec]
2950
+
2817
2951
  @property
2818
2952
  def filesystem(self) -> modal.sandbox_fs.SandboxFilesystem:
2819
2953
  """Namespace for Sandbox filesystem APIs."""