modAL 1.6.3.dev5__tar.gz → 1.6.3.dev6__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 (249) hide show
  1. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/PKG-INFO +1 -1
  2. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/grpc_testing.py +3 -1
  3. modal-1.6.3.dev6/modal/builder/PREVIEW.txt +18 -0
  4. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/client.pyi +2 -2
  5. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_common.py +17 -2
  6. modal-1.6.3.dev6/modal/sandbox/_impl_persistent.py +470 -0
  7. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_impl_v1.py +10 -0
  8. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_impl_v2.py +10 -0
  9. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_sandbox.py +21 -3
  10. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/sandbox.pyi +38 -2
  11. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/PKG-INFO +1 -1
  12. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/SOURCES.txt +1 -0
  13. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_version/__init__.py +1 -1
  14. modal-1.6.3.dev5/modal/builder/PREVIEW.txt +0 -21
  15. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/LICENSE +0 -0
  16. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/README.md +0 -0
  17. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/__init__.py +0 -0
  18. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/__main__.py +0 -0
  19. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_billing.py +0 -0
  20. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_cluster.py +0 -0
  21. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_cluster.pyi +0 -0
  22. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_container_entrypoint.py +0 -0
  23. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_environments.py +0 -0
  24. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_function_variants.py +0 -0
  25. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_functions.py +0 -0
  26. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_grpc_client.py +0 -0
  27. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_image.py +0 -0
  28. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_ipython.py +0 -0
  29. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_load_context.py +0 -0
  30. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_location.py +0 -0
  31. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_logs.py +0 -0
  32. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_logs_manager.py +0 -0
  33. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_logs_manager.pyi +0 -0
  34. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_metrics.py +0 -0
  35. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_metrics.pyi +0 -0
  36. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_object.py +0 -0
  37. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_outbound_policy.py +0 -0
  38. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_outbound_policy.pyi +0 -0
  39. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_output/__init__.py +0 -0
  40. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_output/manager.py +0 -0
  41. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_output/pty.py +0 -0
  42. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_output/rich.py +0 -0
  43. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_output/status.py +0 -0
  44. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_partial_function.py +0 -0
  45. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_resolver.py +0 -0
  46. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_resources.py +0 -0
  47. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/__init__.py +0 -0
  48. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/asgi.py +0 -0
  49. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/container_io_manager.py +0 -0
  50. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/container_io_manager.pyi +0 -0
  51. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/execution_context.py +0 -0
  52. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/execution_context.pyi +0 -0
  53. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  54. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/task_lifecycle_manager.py +0 -0
  55. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  56. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/telemetry.py +0 -0
  57. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/user_code_event_loop.py +0 -0
  58. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_runtime/user_code_imports.py +0 -0
  59. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_serialization.py +0 -0
  60. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_server.py +0 -0
  61. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_server.pyi +0 -0
  62. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_supports_logs.py +0 -0
  63. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_traceback.py +0 -0
  64. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_tunnel.py +0 -0
  65. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_tunnel.pyi +0 -0
  66. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_type_manager.py +0 -0
  67. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/__init__.py +0 -0
  68. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/app_utils.py +0 -0
  69. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/async_utils.py +0 -0
  70. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/auth_token_manager.py +0 -0
  71. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/blob_utils.py +0 -0
  72. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/browser_utils.py +0 -0
  73. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/bytes_io_segment_payload.py +0 -0
  74. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/curl_utils.py +0 -0
  75. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/deprecation.py +0 -0
  76. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/display_utils.py +0 -0
  77. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/docker_utils.py +0 -0
  78. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/function_utils.py +0 -0
  79. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/git_utils.py +0 -0
  80. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/grpc_utils.py +0 -0
  81. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/hash_utils.py +0 -0
  82. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/http_utils.py +0 -0
  83. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/idle_countdown.py +0 -0
  84. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/jwt_utils.py +0 -0
  85. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/logger.py +0 -0
  86. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/mount_utils.py +0 -0
  87. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/name_utils.py +0 -0
  88. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/package_utils.py +0 -0
  89. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/pattern_utils.py +0 -0
  90. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/proxy_support.py +0 -0
  91. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/rand_pb_testing.py +0 -0
  92. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/sandbox_fs_utils.py +0 -0
  93. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/shell_utils.py +0 -0
  94. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_utils/time_utils.py +0 -0
  95. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_vendor/__init__.py +0 -0
  96. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  97. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_vendor/cloudpickle.py +0 -0
  98. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_vendor/tblib.py +0 -0
  99. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_vendor/version.py +0 -0
  100. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_watcher.py +0 -0
  101. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/_workspace.py +0 -0
  102. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/app.py +0 -0
  103. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/app.pyi +0 -0
  104. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/billing.py +0 -0
  105. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/billing.pyi +0 -0
  106. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/2023.12.312.txt +0 -0
  107. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/2023.12.txt +0 -0
  108. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/2024.04.txt +0 -0
  109. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/2024.10.txt +0 -0
  110. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/2025.06.txt +0 -0
  111. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/README.md +0 -0
  112. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/builder/base-images.json +0 -0
  113. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/call_graph.py +0 -0
  114. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/__init__.py +0 -0
  115. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/_download.py +0 -0
  116. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/_help.py +0 -0
  117. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/_logs.py +0 -0
  118. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/_stats.py +0 -0
  119. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/_traceback.py +0 -0
  120. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/app.py +0 -0
  121. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/billing.py +0 -0
  122. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/changelog.py +0 -0
  123. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/config.py +0 -0
  124. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/container.py +0 -0
  125. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/curl.py +0 -0
  126. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/dashboard.py +0 -0
  127. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/dict.py +0 -0
  128. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/endpoint.py +0 -0
  129. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/entry_point.py +0 -0
  130. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/environment.py +0 -0
  131. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/function.py +0 -0
  132. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/image.py +0 -0
  133. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/import_refs.py +0 -0
  134. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/logo.py +0 -0
  135. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/network_file_system.py +0 -0
  136. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/profile.py +0 -0
  137. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/queues.py +0 -0
  138. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/run.py +0 -0
  139. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/secret.py +0 -0
  140. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/selector.py +0 -0
  141. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/server.py +0 -0
  142. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/shell.py +0 -0
  143. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/skills.py +0 -0
  144. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/token.py +0 -0
  145. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/utils.py +0 -0
  146. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/volume.py +0 -0
  147. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cli/workspace.py +0 -0
  148. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/client.py +0 -0
  149. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cloud_bucket_mount.py +0 -0
  150. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cloud_bucket_mount.pyi +0 -0
  151. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cls.py +0 -0
  152. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cls.pyi +0 -0
  153. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cluster.py +0 -0
  154. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/cluster.pyi +0 -0
  155. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/config.py +0 -0
  156. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/container_process.py +0 -0
  157. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/container_process.pyi +0 -0
  158. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/dict.py +0 -0
  159. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/dict.pyi +0 -0
  160. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/environments.py +0 -0
  161. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/environments.pyi +0 -0
  162. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/exception.py +0 -0
  163. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/__init__.py +0 -0
  164. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/_flash_container_entry.py +0 -0
  165. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/flash.py +0 -0
  166. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/flash.pyi +0 -0
  167. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/flash_server.py +0 -0
  168. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/flash_server.pyi +0 -0
  169. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/experimental/ipython.py +0 -0
  170. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/file_pattern_matcher.py +0 -0
  171. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/functions.py +0 -0
  172. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/functions.pyi +0 -0
  173. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/image.py +0 -0
  174. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/image.pyi +0 -0
  175. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/io_streams.py +0 -0
  176. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/io_streams.pyi +0 -0
  177. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/mount.py +0 -0
  178. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/mount.pyi +0 -0
  179. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/network_file_system.py +0 -0
  180. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/network_file_system.pyi +0 -0
  181. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/object.py +0 -0
  182. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/object.pyi +0 -0
  183. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/output.py +0 -0
  184. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/parallel_map.py +0 -0
  185. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/parallel_map.pyi +0 -0
  186. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/partial_function.py +0 -0
  187. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/partial_function.pyi +0 -0
  188. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/proxy.py +0 -0
  189. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/proxy.pyi +0 -0
  190. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/py.typed +0 -0
  191. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/queue.py +0 -0
  192. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/queue.pyi +0 -0
  193. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/retries.py +0 -0
  194. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/runner.py +0 -0
  195. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/runner.pyi +0 -0
  196. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/running_app.py +0 -0
  197. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/__init__.py +0 -0
  198. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_filesystem.py +0 -0
  199. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_sidecar.py +0 -0
  200. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_snapshot.py +0 -0
  201. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/_task_command_router_client.py +0 -0
  202. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/sandbox/sandbox.py +0 -0
  203. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/schedule.py +0 -0
  204. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/scheduler_placement.py +0 -0
  205. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/secret.py +0 -0
  206. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/secret.pyi +0 -0
  207. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/server.py +0 -0
  208. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/server.pyi +0 -0
  209. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/serving.py +0 -0
  210. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/serving.pyi +0 -0
  211. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/skills/modal/SKILL.md +0 -0
  212. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/stream_type.py +0 -0
  213. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/token_flow.py +0 -0
  214. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/token_flow.pyi +0 -0
  215. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/types.py +0 -0
  216. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/volume.py +0 -0
  217. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/volume.pyi +0 -0
  218. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/workspace.py +0 -0
  219. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal/workspace.pyi +0 -0
  220. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/dependency_links.txt +0 -0
  221. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/entry_points.txt +0 -0
  222. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/requires.txt +0 -0
  223. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal.egg-info/top_level.txt +0 -0
  224. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/__init__.py +0 -0
  225. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/gen_cli_docs.py +0 -0
  226. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/gen_cli_docs_main.py +0 -0
  227. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/gen_reference_docs.py +0 -0
  228. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/gen_reference_docs_main.py +0 -0
  229. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/mdmd/__init__.py +0 -0
  230. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/mdmd/mdmd.py +0 -0
  231. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/mdmd/signatures.py +0 -0
  232. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/mdmd/types.py +0 -0
  233. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_docs/reference_intro.md +0 -0
  234. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/__init__.py +0 -0
  235. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/api_grpc.py +0 -0
  236. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/api_pb2.py +0 -0
  237. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/api_pb2.pyi +0 -0
  238. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/api_pb2_grpc.py +0 -0
  239. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/api_pb2_grpc.pyi +0 -0
  240. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/modal_api_grpc.py +0 -0
  241. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/py.typed +0 -0
  242. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/task_command_router_grpc.py +0 -0
  243. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/task_command_router_pb2.py +0 -0
  244. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/task_command_router_pb2.pyi +0 -0
  245. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  246. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  247. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/modal_version/__main__.py +0 -0
  248. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/pyproject.toml +0 -0
  249. {modal-1.6.3.dev5 → modal-1.6.3.dev6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.6.3.dev5
3
+ Version: 1.6.3.dev6
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -116,7 +116,7 @@ class InterceptionContext:
116
116
  """Adds one response payload to an expected queue of responses for a method.
117
117
 
118
118
  These responses will be used once each instead of calling the MockServicer's
119
- implementation of the method.
119
+ implementation of the method. An exception payload is raised instead of being sent.
120
120
 
121
121
  The interception context will throw an exception on exit if not all of the added
122
122
  responses have been consumed.
@@ -184,6 +184,8 @@ class InterceptionContext:
184
184
  async def responder(servicer_self, stream):
185
185
  await stream.recv_message() # get the input message so we can track that
186
186
  for msg in next_response_messages:
187
+ if isinstance(msg, Exception):
188
+ raise msg
187
189
  await stream.send_message(msg)
188
190
 
189
191
  return responder
@@ -0,0 +1,18 @@
1
+ aiohappyeyeballs==2.7.1
2
+ aiohttp==3.14.5
3
+ aiosignal==1.4.0
4
+ async-timeout==5.0.1 ; python_version < "3.11"
5
+ attrs==26.1.0
6
+ cbor2==5.9.0
7
+ certifi==2026.10.19
8
+ frozenlist==1.8.0
9
+ grpclib==0.4.9
10
+ h2==4.4.1
11
+ hpack==4.2.0
12
+ hyperframe==6.1.0
13
+ idna==3.20
14
+ multidict==7.1.0
15
+ propcache==0.5.4
16
+ protobuf==7.36.2
17
+ typing_extensions==4.16.0
18
+ yarl==1.25.1
@@ -80,7 +80,7 @@ class _Client:
80
80
  server_url: str,
81
81
  client_type: int,
82
82
  credentials: typing.Optional[tuple[str, str]],
83
- version: str = "1.6.3.dev5",
83
+ version: str = "1.6.3.dev6",
84
84
  *,
85
85
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
86
86
  ):
@@ -290,7 +290,7 @@ class Client:
290
290
  server_url: str,
291
291
  client_type: int,
292
292
  credentials: typing.Optional[tuple[str, str]],
293
- version: str = "1.6.3.dev5",
293
+ version: str = "1.6.3.dev6",
294
294
  *,
295
295
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
296
296
  ):
@@ -386,7 +386,7 @@ class SandboxCreateArgs:
386
386
  secrets: Collection[_Secret] | None = None
387
387
  env: dict[str, str | None] | None = None
388
388
  name: str | None = None
389
- timeout: int = 300
389
+ timeout: int | None = 300
390
390
  idle_timeout: int | None = None
391
391
  workdir: str | None = None
392
392
  gpu: str | None = None
@@ -417,6 +417,7 @@ class SandboxCreateArgs:
417
417
  custom_domain: str | None = None
418
418
  include_oidc_identity_token: bool = False
419
419
  runtime: SandboxRuntime | None = None
420
+ persistent: bool = False
420
421
 
421
422
 
422
423
  class SandboxCreateCommon:
@@ -428,6 +429,8 @@ class SandboxCreateCommon:
428
429
 
429
430
  def __init__(self, create_args: SandboxCreateArgs, *, secrets: Sequence[_Secret]) -> None:
430
431
  a = create_args
432
+ if a.timeout is None:
433
+ raise InvalidError("`timeout=None` is only supported for experimental persistent Sandboxes.")
431
434
  _validate_exec_args(a.args)
432
435
  if a.name is not None:
433
436
  check_object_name(a.name, "Sandbox")
@@ -452,6 +455,7 @@ class SandboxCreateCommon:
452
455
  raise InvalidError("`inbound_cidr_allowlist` cannot be used when `block_network` is enabled")
453
456
 
454
457
  self.create_args = a
458
+ self.timeout = a.timeout
455
459
  self.secrets = secrets
456
460
  self.image = a.image or _default_image
457
461
  self.network_file_systems = validate_network_file_systems(a.network_file_systems)
@@ -503,7 +507,7 @@ class SandboxCreateCommon:
503
507
  secret_ids=[secret.object_id for secret in self.secrets],
504
508
  # Ephemeral disk requests are not supported on Sandboxes.
505
509
  resources=convert_fn_config_to_resources_config(cpu=a.cpu, memory=a.memory, gpu=a.gpu, ephemeral_disk=None),
506
- timeout_secs=a.timeout,
510
+ timeout_secs=self.timeout,
507
511
  idle_timeout_secs=a.idle_timeout,
508
512
  workdir=a.workdir,
509
513
  cloud_provider_str=a.cloud if a.cloud else None, # pyright: ignore[reportArgumentType]
@@ -637,6 +641,10 @@ class SandboxImpl(Protocol):
637
641
  """
638
642
  ...
639
643
 
644
+ def clone(self) -> "SandboxImpl":
645
+ """Create an implementation of the same type for another handle to the same Sandbox."""
646
+ ...
647
+
640
648
  @classmethod
641
649
  async def lookup_by_name(
642
650
  cls, client: _Client, req: api_pb2.SandboxGetFromNameRequest
@@ -674,6 +682,10 @@ class SandboxImpl(Protocol):
674
682
 
675
683
  async def terminate(self) -> None: ...
676
684
 
685
+ async def pause(self) -> None: ...
686
+
687
+ async def resume(self) -> None: ...
688
+
677
689
  async def close(self) -> None:
678
690
  """Release connections held for the Sandbox once in-flight operations complete."""
679
691
  ...
@@ -907,6 +919,9 @@ async def snapshot_filesystem_via_tcr(
907
919
  return _Image._new_hydrated(res.image_id, client, None)
908
920
 
909
921
 
922
+ _PERSISTENT_ONLY_MESSAGE = "`{}` requires a persistent Sandbox, created with `_experimental_persistent=True`."
923
+
924
+
910
925
  async def mount_image_via_tcr(
911
926
  impl: SandboxImpl,
912
927
  path: PurePosixPath | str,
@@ -0,0 +1,470 @@
1
+ # Copyright Modal Labs 2026
2
+ """Persistent Sandboxes, managed by the `PersistentSandbox*` control-plane RPCs.
3
+
4
+ A persistent Sandbox runs as a sequence of regular V2 Sandboxes, its sessions. The control plane starts each
5
+ session from the exit snapshot of the previous one.
6
+ """
7
+
8
+ import asyncio
9
+ import copy
10
+ import dataclasses
11
+ import time
12
+ import weakref
13
+ from collections.abc import Awaitable, Callable, Collection, Sequence
14
+ from pathlib import PurePosixPath
15
+ from typing import TYPE_CHECKING, Any, Literal, TypeVar
16
+
17
+ from grpclib import GRPCError, Status
18
+
19
+ from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
20
+
21
+ from .._image import _Image
22
+ from .._outbound_policy import _OutboundPolicy
23
+ from .._resolver import Resolver
24
+ from .._tunnel import Tunnel
25
+ from ..client import _Client
26
+ from ..cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
27
+ from ..config import logger
28
+ from ..container_process import _ContainerProcess
29
+ from ..exception import ClientClosed, InvalidError, SandboxTerminatedError
30
+ from ..io_streams import (
31
+ StreamReader,
32
+ StreamWriter,
33
+ _StreamReaderThroughSandboxCommandRouterParams,
34
+ _StreamWriterThroughCommandRouterSandboxParams,
35
+ )
36
+ from ..secret import _resolvable_secrets, _Secret, _secret_sources
37
+ from ..stream_type import StreamType
38
+ from ._common import SandboxCreateArgs, SandboxCreateCommon, SandboxImpl, command_router, sandbox_create_context
39
+ from ._impl_v2 import SandboxImplV2, auth_metadata
40
+ from ._snapshot import _SandboxSnapshot
41
+ from ._task_command_router_client import TaskCommandRouterClient
42
+
43
+ if TYPE_CHECKING:
44
+ import modal.app
45
+
46
+ _T = TypeVar("_T")
47
+
48
+ # Errors caused by the caller rather than by the session, which a new session can't fix.
49
+ _NON_RECOVERABLE_ERRORS = (InvalidError, ClientClosed)
50
+
51
+ # Lifetime limits of each session. A session that hits one is paused, and resumed on next use.
52
+ PERSISTENT_SANDBOX_SESSION_TIMEOUT = 60 * 60
53
+ PERSISTENT_SANDBOX_SESSION_IDLE_TIMEOUT = 5 * 60
54
+
55
+ # How long to retry resuming while the previous session is still saving its exit snapshot.
56
+ _RESUME_RETRY_TIMEOUT = 120.0
57
+ _RESUME_RETRY_INTERVAL = 1.0
58
+
59
+ # How often `wait()` checks whether the persistent Sandbox has been terminated.
60
+ _WAIT_POLL_INTERVAL = 1.0
61
+
62
+ _TERMINATED_RESULT = api_pb2.GenericResult(status=api_pb2.GenericResult.GENERIC_STATUS_TERMINATED)
63
+
64
+
65
+ class SandboxImplPersistent:
66
+ """Backs a persistent Sandbox, whose object ID is the ID of its first session.
67
+
68
+ Operations are forwarded to the current session. When one fails and the Sandbox hasn't been terminated
69
+ through this handle, the control plane is asked for the running session, resuming the Sandbox from its
70
+ latest exit snapshot if there is none. The operation is then retried once on the new session.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ client: _Client,
76
+ sandbox_id: str,
77
+ *,
78
+ result: api_pb2.GenericResult | None = None,
79
+ persistent_sandbox_id: str = "",
80
+ session: SandboxImplV2 | None = None,
81
+ ) -> None:
82
+ self.client = client
83
+ self.sandbox_id = sandbox_id
84
+ self.result = result
85
+ self._persistent_sandbox_id = persistent_sandbox_id
86
+ self._session = session if session is not None else SandboxImplV2(client, sandbox_id)
87
+ self._replace_lock: asyncio.Lock | None = None
88
+ self._terminated = result is not None
89
+
90
+ @property
91
+ def _delegate(self) -> SandboxImplV2:
92
+ return self._session
93
+
94
+ @property
95
+ def _client(self) -> _Client:
96
+ return self.client
97
+
98
+ @classmethod
99
+ async def create(
100
+ cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
101
+ ) -> "tuple[SandboxImpl, api_pb2.SandboxHandleMetadata]":
102
+ if create_args.timeout is not None or create_args.idle_timeout is not None:
103
+ raise InvalidError("Persistent Sandboxes require `timeout=None` and don't support `idle_timeout`.")
104
+ create_args = dataclasses.replace(
105
+ create_args,
106
+ timeout=PERSISTENT_SANDBOX_SESSION_TIMEOUT,
107
+ idle_timeout=PERSISTENT_SANDBOX_SESSION_IDLE_TIMEOUT,
108
+ )
109
+ if create_args.name is not None:
110
+ raise InvalidError("Named persistent Sandboxes are not supported yet.")
111
+ if create_args.gpu is not None or create_args.network_file_systems or create_args.pty_info is not None:
112
+ raise InvalidError("GPUs, network file systems and PTYs are not supported by persistent Sandboxes yet.")
113
+ if create_args.enable_snapshot:
114
+ raise InvalidError("Memory snapshots are not supported by persistent Sandboxes.")
115
+ if create_args.env or create_args.tags:
116
+ raise InvalidError("`env` and `tags` are not supported by persistent Sandboxes yet.")
117
+
118
+ secrets = create_args.secrets or []
119
+ common = SandboxCreateCommon(create_args, secrets=_resolvable_secrets(secrets))
120
+ async with sandbox_create_context(app, client) as (client, load_context):
121
+ await common.load_dependencies(Resolver(), load_context, load_ephemeral_bucket_secrets=False)
122
+ cloud_bucket_mount_protos, cloud_bucket_credentials = cloud_bucket_mounts_to_proto(
123
+ common.cloud_bucket_mounts, split_ephemeral_credentials=True
124
+ )
125
+ if cloud_bucket_credentials or any(source.HasField("env") for source in _secret_sources(secrets)):
126
+ raise InvalidError("Ephemeral credentials are not supported by persistent Sandboxes yet.")
127
+ req = api_pb2.PersistentSandboxCreateRequest(
128
+ app_id=load_context.app_id or "",
129
+ definition=common.definition(cloud_bucket_mounts=cloud_bucket_mount_protos),
130
+ )
131
+ resp = await client._stub.PersistentSandboxCreate(req, metadata=await auth_metadata(client))
132
+ app_id = load_context.app_id or ""
133
+ impl = cls(client, resp.sandbox_id, persistent_sandbox_id=resp.persistent_sandbox_id)
134
+ return impl, api_pb2.SandboxHandleMetadata(app_id=app_id)
135
+
136
+ # Persistent Sandboxes are only created through `create`, and can't be looked up yet.
137
+
138
+ @classmethod
139
+ async def lookup_by_name(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxGetFromNameResponse:
140
+ raise NotImplementedError
141
+
142
+ @classmethod
143
+ async def lookup_by_id(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxWaitResponse:
144
+ raise NotImplementedError
145
+
146
+ @classmethod
147
+ async def list_page(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxListResponse:
148
+ raise NotImplementedError
149
+
150
+ @classmethod
151
+ async def restore_from_memory_snapshot(
152
+ cls, *args: Any, **kwargs: Any
153
+ ) -> "tuple[SandboxImpl, api_pb2.SandboxHandleMetadata]":
154
+ raise NotImplementedError
155
+
156
+ def clone(self) -> "SandboxImplPersistent":
157
+ return copy.copy(self)
158
+
159
+ def make_stdio(self, check_open: Callable[[], None]) -> tuple[StreamReader[str], StreamReader[str], StreamWriter]:
160
+ """Create stdio streams of the entrypoint of whichever session is current when they connect."""
161
+ weak_self = weakref.ref(self)
162
+
163
+ async def resolve_router() -> tuple[str, TaskCommandRouterClient]:
164
+ this = weak_self()
165
+ if this is None:
166
+ raise RuntimeError("Sandbox was garbage collected before its stdio reader connected")
167
+ return await command_router(this)
168
+
169
+ def reader(file_descriptor: "api_pb2.FileDescriptor.ValueType") -> StreamReader[str]:
170
+ return StreamReader(
171
+ _StreamReaderThroughSandboxCommandRouterParams(
172
+ file_descriptor=file_descriptor, sandbox_id=self.sandbox_id, resolve_router=resolve_router
173
+ ),
174
+ by_line=True,
175
+ )
176
+
177
+ stdin = StreamWriter(
178
+ _StreamWriterThroughCommandRouterSandboxParams(resolve_router=resolve_router, check_open=check_open)
179
+ )
180
+ return reader(api_pb2.FILE_DESCRIPTOR_STDOUT), reader(api_pb2.FILE_DESCRIPTOR_STDERR), stdin
181
+
182
+ # Session replacement
183
+
184
+ async def _run(self, op: Callable[[SandboxImplV2], Awaitable[_T]]) -> _T:
185
+ """Run `op` on the session, switching to the running session and retrying once if it fails."""
186
+ session = self._delegate
187
+ try:
188
+ return await op(session)
189
+ except _NON_RECOVERABLE_ERRORS:
190
+ raise
191
+ except Exception as exc:
192
+ if self._terminated:
193
+ # A terminated persistent Sandbox is stopped for good, so it isn't resumed on use.
194
+ raise
195
+ replacement = await self._replace_session(session)
196
+ if replacement is None:
197
+ raise
198
+ logger.debug(f"Retrying operation on persistent Sandbox {self._persistent_sandbox_id} after: {exc!r}")
199
+ return await op(replacement)
200
+
201
+ async def _get_running_session_id(self) -> str:
202
+ req = api_pb2.PersistentSandboxGetRequest(persistent_sandbox_id=self._persistent_sandbox_id)
203
+ resp = await self._client._stub.PersistentSandboxGet(req, metadata=await auth_metadata(self._client))
204
+ return resp.sandbox_id
205
+
206
+ async def _resume_session(self) -> str:
207
+ """Resume the persistent Sandbox, returning the ID of its running session."""
208
+ req = api_pb2.PersistentSandboxResumeRequest(persistent_sandbox_id=self._persistent_sandbox_id)
209
+ deadline = time.monotonic() + _RESUME_RETRY_TIMEOUT
210
+ while True:
211
+ try:
212
+ resp = await self._client._stub.PersistentSandboxResume(req, metadata=await auth_metadata(self._client))
213
+ return resp.sandbox_id
214
+ except GRPCError as exc:
215
+ # UNAVAILABLE means the previous session is still pausing or another resume is in progress.
216
+ if exc.status != Status.UNAVAILABLE or time.monotonic() > deadline:
217
+ raise
218
+ await asyncio.sleep(_RESUME_RETRY_INTERVAL)
219
+
220
+ async def _replace_session(self, failed: SandboxImplV2 | None, *, resume: bool = True) -> SandboxImplV2 | None:
221
+ """Switch from the `failed` session, or return None if it is still the running session."""
222
+ # Lazily create the lock on the event loop that first uses the Sandbox.
223
+ if self._replace_lock is None:
224
+ self._replace_lock = asyncio.Lock()
225
+ async with self._replace_lock:
226
+ if self._session is not failed:
227
+ # A concurrent operation has already replaced the session.
228
+ return self._session
229
+
230
+ session_id = await self._get_running_session_id()
231
+ if failed is not None and session_id == failed.sandbox_id:
232
+ return None
233
+ if not session_id:
234
+ if not resume:
235
+ return None
236
+ session_id = await self._resume_session()
237
+ logger.debug(f"Switching persistent Sandbox {self._persistent_sandbox_id} to session {session_id}")
238
+ replacement = SandboxImplV2(self.client, session_id)
239
+ self._session = replacement
240
+ if failed is not None:
241
+ await failed.close()
242
+ return replacement
243
+
244
+ # Operations
245
+
246
+ async def close(self) -> None:
247
+ await self._session.close()
248
+
249
+ def _set_terminated_result(self) -> None:
250
+ self.result = _TERMINATED_RESULT
251
+
252
+ async def wait(self, raise_on_termination: bool) -> None:
253
+ # A persistent Sandbox keeps running across sessions, until it is terminated.
254
+ while not self._terminated:
255
+ await asyncio.sleep(_WAIT_POLL_INTERVAL)
256
+ self._set_terminated_result()
257
+ if raise_on_termination:
258
+ raise SandboxTerminatedError()
259
+
260
+ async def poll(self) -> None:
261
+ if self._terminated:
262
+ self._set_terminated_result()
263
+ else:
264
+ self.result = None
265
+
266
+ async def terminate(self) -> None:
267
+ req = api_pb2.PersistentSandboxTerminateRequest(persistent_sandbox_id=self._persistent_sandbox_id)
268
+ await self._client._stub.PersistentSandboxTerminate(req, metadata=await auth_metadata(self._client))
269
+ self._terminated = True
270
+ self._set_terminated_result()
271
+
272
+ async def pause(self) -> None:
273
+ """Stop the current session, which saves its exit snapshot for the next `resume`."""
274
+ self._check_not_terminated()
275
+ session = self._delegate
276
+ await session.terminate()
277
+ await session.wait(raise_on_termination=False)
278
+
279
+ async def resume(self) -> None:
280
+ """Start a session from the latest exit snapshot, unless one is already running."""
281
+ self._check_not_terminated()
282
+ session = self._delegate
283
+ session_id = await self._resume_session()
284
+ if session_id != session.sandbox_id:
285
+ self._session = SandboxImplV2(self.client, session_id)
286
+ await session.close()
287
+
288
+ def _check_not_terminated(self) -> None:
289
+ if self._terminated:
290
+ raise InvalidError(f"Persistent Sandbox {self._persistent_sandbox_id} has been terminated.")
291
+
292
+ async def tunnels(self, timeout: int) -> dict[int, Tunnel]:
293
+ return await self._run(lambda d: d.tunnels(timeout))
294
+
295
+ async def get_task_id(
296
+ self, raise_if_task_complete: bool = False, timeout: float | None = None, retry_transient: bool = False
297
+ ) -> str:
298
+ return await self._run(
299
+ lambda d: d.get_task_id(
300
+ raise_if_task_complete=raise_if_task_complete, timeout=timeout, retry_transient=retry_transient
301
+ )
302
+ )
303
+
304
+ async def resolve_task_id_for_logs(self) -> str:
305
+ return await self._delegate.resolve_task_id_for_logs()
306
+
307
+ async def get_exit_snapshot(self, timeout: float | None) -> _Image:
308
+ return await self._delegate.get_exit_snapshot(timeout)
309
+
310
+ async def snapshot_memory(self) -> _SandboxSnapshot:
311
+ raise NotImplementedError("Memory snapshots aren't supported for persistent Sandboxes.")
312
+
313
+ async def get_command_router_client(self, task_id: str) -> TaskCommandRouterClient:
314
+ # The task ID belongs to the current delegate, so this can't be retried on a replacement.
315
+ return await self._delegate.get_command_router_client(task_id)
316
+
317
+ async def get_tags(self) -> dict[str, str]:
318
+ raise NotImplementedError("Tags aren't supported for persistent Sandboxes yet.")
319
+
320
+ async def set_tags(self, tags: dict[str, str]) -> None:
321
+ raise NotImplementedError("Tags aren't supported for persistent Sandboxes yet.")
322
+
323
+ async def set_name(self, name: str) -> None:
324
+ raise InvalidError("The name of a persistent Sandbox can't be changed.")
325
+
326
+ async def create_connect_token(
327
+ self, req: api_pb2.SandboxCreateConnectTokenRequest
328
+ ) -> api_pb2.SandboxCreateConnectTokenResponse:
329
+ def create(d: SandboxImplV2) -> Awaitable[api_pb2.SandboxCreateConnectTokenResponse]:
330
+ delegate_req = api_pb2.SandboxCreateConnectTokenRequest()
331
+ delegate_req.CopyFrom(req)
332
+ delegate_req.sandbox_id = d.sandbox_id
333
+ return d.create_connect_token(delegate_req)
334
+
335
+ return await self._run(create)
336
+
337
+ async def wait_until_ready(self, timeout: int) -> None:
338
+ await self._run(lambda d: d.wait_until_ready(timeout))
339
+
340
+ async def set_network_access(self, network_access: api_pb2.NetworkAccess) -> None:
341
+ raise NotImplementedError("Updating network access isn't supported for persistent Sandboxes yet.")
342
+
343
+ async def update_outbound_policy(self, outbound_policy: _OutboundPolicy) -> None:
344
+ raise NotImplementedError("Updating the outbound policy isn't supported for persistent Sandboxes yet.")
345
+
346
+ async def snapshot_filesystem(self, timeout: int, *, ttl: int | None, container_id: str = "") -> _Image:
347
+ return await self._run(lambda d: d.snapshot_filesystem(timeout, ttl=ttl, container_id=container_id))
348
+
349
+ async def mount_image(
350
+ self,
351
+ path: PurePosixPath | str,
352
+ image_id: str,
353
+ *,
354
+ encryption_key: bytes | None,
355
+ container_id: str = "",
356
+ ) -> None:
357
+ raise NotImplementedError("Mounting images isn't supported for persistent Sandboxes yet.")
358
+
359
+ async def unmount_image(self, path: PurePosixPath | str, *, container_id: str = "") -> None:
360
+ raise NotImplementedError("Mounting images isn't supported for persistent Sandboxes yet.")
361
+
362
+ async def snapshot_directory(
363
+ self,
364
+ path: PurePosixPath | str,
365
+ *,
366
+ timeout: int,
367
+ ttl: int | None,
368
+ encryption_key: bytes | None,
369
+ container_id: str = "",
370
+ ) -> _Image:
371
+ return await self._run(
372
+ lambda d: d.snapshot_directory(
373
+ path, timeout=timeout, ttl=ttl, encryption_key=encryption_key, container_id=container_id
374
+ )
375
+ )
376
+
377
+ async def reload_volumes(self, *, timeout: int, container_id: str = "") -> None:
378
+ await self._run(lambda d: d.reload_volumes(timeout=timeout, container_id=container_id))
379
+
380
+ async def exec(
381
+ self,
382
+ *args: str,
383
+ pty_info: api_pb2.PTYInfo | None,
384
+ stdout: StreamType,
385
+ stderr: StreamType,
386
+ timeout: int | None,
387
+ workdir: str | None,
388
+ env: dict[str, str | None] | None,
389
+ secrets: Collection[_Secret] | None,
390
+ text: bool,
391
+ bufsize: Literal[-1, 1],
392
+ container_id: str | None,
393
+ ) -> _ContainerProcess[bytes] | _ContainerProcess[str]:
394
+ return await self._run(
395
+ lambda d: d.exec(
396
+ *args,
397
+ pty_info=pty_info,
398
+ stdout=stdout,
399
+ stderr=stderr,
400
+ timeout=timeout,
401
+ workdir=workdir,
402
+ env=env,
403
+ secrets=secrets,
404
+ text=text,
405
+ bufsize=bufsize,
406
+ container_id=container_id,
407
+ )
408
+ )
409
+
410
+ # Sidecar containers
411
+
412
+ def validate_sidecar_create(
413
+ self,
414
+ *,
415
+ cloud_bucket_mounts: Sequence[tuple[str, _CloudBucketMount]],
416
+ has_proxy: bool,
417
+ include_oidc_identity_token: bool,
418
+ ) -> None:
419
+ self._delegate.validate_sidecar_create(
420
+ cloud_bucket_mounts=cloud_bucket_mounts,
421
+ has_proxy=has_proxy,
422
+ include_oidc_identity_token=include_oidc_identity_token,
423
+ )
424
+
425
+ async def create_sidecar(
426
+ self,
427
+ *args: str,
428
+ name: str,
429
+ image_id: str,
430
+ secret_ids: Sequence[str],
431
+ secret_sources: Sequence[api_pb2.SecretSource],
432
+ workdir: str | None,
433
+ volume_mounts: Sequence[api_pb2.VolumeMount],
434
+ cloud_bucket_mounts: Sequence[tuple[str, _CloudBucketMount]],
435
+ network_access: api_pb2.NetworkAccess,
436
+ include_oidc_identity_token: bool,
437
+ proxy_id: str | None,
438
+ pty_info: api_pb2.PTYInfo | None,
439
+ memory_reserve_consume_mib: int | None,
440
+ ) -> tuple[str, str]:
441
+ return await self._run(
442
+ lambda d: d.create_sidecar(
443
+ *args,
444
+ name=name,
445
+ image_id=image_id,
446
+ secret_ids=secret_ids,
447
+ secret_sources=secret_sources,
448
+ workdir=workdir,
449
+ volume_mounts=volume_mounts,
450
+ cloud_bucket_mounts=cloud_bucket_mounts,
451
+ network_access=network_access,
452
+ include_oidc_identity_token=include_oidc_identity_token,
453
+ proxy_id=proxy_id,
454
+ pty_info=pty_info,
455
+ memory_reserve_consume_mib=memory_reserve_consume_mib,
456
+ )
457
+ )
458
+
459
+ async def get_sidecar(self, name: str, include_terminated: bool) -> sr_pb2.TaskContainerInfo:
460
+ return await self._run(lambda d: d.get_sidecar(name, include_terminated))
461
+
462
+ async def list_sidecars(self, include_terminated: bool) -> list[sr_pb2.TaskContainerInfo]:
463
+ return await self._run(lambda d: d.list_sidecars(include_terminated))
464
+
465
+ async def wait_sidecar(self, container_id: str, timeout: float) -> api_pb2.GenericResult | None:
466
+ # Sidecar containers don't carry over to a replacement delegate.
467
+ return await self._delegate.wait_sidecar(container_id, timeout)
468
+
469
+ async def terminate_sidecar(self, container_id: str) -> None:
470
+ await self._delegate.terminate_sidecar(container_id)
@@ -40,6 +40,7 @@ from ..io_streams import (
40
40
  from ..secret import _Secret
41
41
  from ..stream_type import StreamType
42
42
  from ._common import (
43
+ _PERSISTENT_ONLY_MESSAGE,
43
44
  _TASK_ID_POLL_INTERVAL,
44
45
  ExitSnapshotPoll,
45
46
  SandboxCreateArgs,
@@ -124,6 +125,9 @@ class SandboxImplV1:
124
125
  def _client(self) -> _Client:
125
126
  return self.client
126
127
 
128
+ def clone(self) -> "SandboxImplV1":
129
+ return type(self)(self.client, self.sandbox_id, result=self.result)
130
+
127
131
  @classmethod
128
132
  async def create(
129
133
  cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
@@ -289,6 +293,12 @@ class SandboxImplV1:
289
293
  req = api_pb2.SandboxTerminateRequest(sandbox_id=self.sandbox_id)
290
294
  await self.client._stub.SandboxTerminate(req)
291
295
 
296
+ async def pause(self) -> None:
297
+ raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_pause()"))
298
+
299
+ async def resume(self) -> None:
300
+ raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_resume()"))
301
+
292
302
  async def snapshot_filesystem(self, timeout: int, *, ttl: int | None, container_id: str = "") -> _Image:
293
303
  if not container_id and os.environ.get("MODAL_USE_LEGACY_FILESYSTEM_SNAPSHOT") == "1":
294
304
  if ttl != DEFAULT_SNAPSHOT_TTL:
@@ -32,6 +32,7 @@ from ..io_streams import (
32
32
  from ..secret import _resolvable_secrets, _Secret, _secret_sources
33
33
  from ..stream_type import StreamType
34
34
  from ._common import (
35
+ _PERSISTENT_ONLY_MESSAGE,
35
36
  _TASK_ID_POLL_INTERVAL,
36
37
  ExitSnapshotPoll,
37
38
  SandboxCreateArgs,
@@ -147,6 +148,9 @@ class SandboxImplV2:
147
148
  def _client(self) -> _Client:
148
149
  return self.client
149
150
 
151
+ def clone(self) -> "SandboxImplV2":
152
+ return type(self)(self.client, self.sandbox_id, result=self.result)
153
+
150
154
  @classmethod
151
155
  async def create(
152
156
  cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
@@ -351,6 +355,12 @@ class SandboxImplV2:
351
355
  req = api_pb2.SandboxTerminateRequest(sandbox_id=self.sandbox_id)
352
356
  await self.client._stub.SandboxTerminateV2(req, metadata=await self._metadata())
353
357
 
358
+ async def pause(self) -> None:
359
+ raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_pause()"))
360
+
361
+ async def resume(self) -> None:
362
+ raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_resume()"))
363
+
354
364
  def _create_sidecar_via_control_plane(self) -> bool:
355
365
  # Opt out via the `use_control_plane_sidecar_create` config setting to create
356
366
  # sidecars over the task command router instead.