modal 1.5.1.dev14__tar.gz → 1.5.1.dev15__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 (228) hide show
  1. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/PKG-INFO +1 -1
  2. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_image.py +3 -4
  3. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/gpu_memory_snapshot.py +5 -0
  4. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/task_lifecycle_manager.py +9 -4
  5. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_workspace.py +145 -1
  6. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/app.py +24 -3
  7. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/entry_point.py +4 -2
  8. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/environment.py +10 -11
  9. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/utils.py +8 -3
  10. modal-1.5.1.dev15/modal/cli/workspace.py +129 -0
  11. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/client.pyi +2 -2
  12. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/sandbox.py +11 -20
  13. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/sandbox.pyi +12 -9
  14. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/secret.py +39 -9
  15. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/secret.pyi +19 -0
  16. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/workspace.py +13 -1
  17. modal-1.5.1.dev15/modal/workspace.pyi +395 -0
  18. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal.egg-info/PKG-INFO +1 -1
  19. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal.egg-info/SOURCES.txt +1 -0
  20. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/api_grpc.py +16 -0
  21. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/api_pb2.py +969 -949
  22. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/api_pb2.pyi +36 -2
  23. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/api_pb2_grpc.py +34 -0
  24. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/api_pb2_grpc.pyi +12 -0
  25. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/modal_api_grpc.py +1 -0
  26. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_version/__init__.py +1 -1
  27. modal-1.5.1.dev14/modal/workspace.pyi +0 -139
  28. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/LICENSE +0 -0
  29. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/README.md +0 -0
  30. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/__init__.py +0 -0
  31. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/__main__.py +0 -0
  32. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_billing.py +0 -0
  33. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_clustered_functions.py +0 -0
  34. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_clustered_functions.pyi +0 -0
  35. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_container_entrypoint.py +0 -0
  36. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_environments.py +0 -0
  37. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_function_variants.py +0 -0
  38. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_functions.py +0 -0
  39. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_grpc_client.py +0 -0
  40. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_ipython.py +0 -0
  41. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_load_context.py +0 -0
  42. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_location.py +0 -0
  43. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_logs.py +0 -0
  44. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_object.py +0 -0
  45. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_output/__init__.py +0 -0
  46. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_output/manager.py +0 -0
  47. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_output/pty.py +0 -0
  48. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_output/rich.py +0 -0
  49. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_output/status.py +0 -0
  50. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_partial_function.py +0 -0
  51. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_resolver.py +0 -0
  52. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_resources.py +0 -0
  53. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/__init__.py +0 -0
  54. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/asgi.py +0 -0
  55. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/container_io_manager.py +0 -0
  56. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/container_io_manager.pyi +0 -0
  57. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/execution_context.py +0 -0
  58. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/execution_context.pyi +0 -0
  59. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  60. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/telemetry.py +0 -0
  61. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/user_code_event_loop.py +0 -0
  62. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_runtime/user_code_imports.py +0 -0
  63. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_serialization.py +0 -0
  64. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_server.py +0 -0
  65. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_traceback.py +0 -0
  66. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_tunnel.py +0 -0
  67. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_tunnel.pyi +0 -0
  68. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_type_manager.py +0 -0
  69. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/__init__.py +0 -0
  70. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/app_utils.py +0 -0
  71. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/async_utils.py +0 -0
  72. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/auth_token_manager.py +0 -0
  73. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/blob_utils.py +0 -0
  74. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/browser_utils.py +0 -0
  75. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/bytes_io_segment_payload.py +0 -0
  76. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/deprecation.py +0 -0
  77. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/docker_utils.py +0 -0
  78. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/function_utils.py +0 -0
  79. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/git_utils.py +0 -0
  80. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/grpc_testing.py +0 -0
  81. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/grpc_utils.py +0 -0
  82. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/hash_utils.py +0 -0
  83. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/http_utils.py +0 -0
  84. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/jwt_utils.py +0 -0
  85. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/logger.py +0 -0
  86. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/mount_utils.py +0 -0
  87. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/name_utils.py +0 -0
  88. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/package_utils.py +0 -0
  89. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/pattern_utils.py +0 -0
  90. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/proxy_support.py +0 -0
  91. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/rand_pb_testing.py +0 -0
  92. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/sandbox_fs_utils.py +0 -0
  93. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/shell_utils.py +0 -0
  94. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/task_command_router_client.py +0 -0
  95. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_utils/time_utils.py +0 -0
  96. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_vendor/__init__.py +0 -0
  97. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  98. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_vendor/cloudpickle.py +0 -0
  99. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_vendor/tblib.py +0 -0
  100. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_vendor/version.py +0 -0
  101. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/_watcher.py +0 -0
  102. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/app.py +0 -0
  103. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/app.pyi +0 -0
  104. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/billing.py +0 -0
  105. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/billing.pyi +0 -0
  106. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/builder/2023.12.312.txt +0 -0
  107. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/builder/2023.12.txt +0 -0
  108. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/builder/2024.04.txt +0 -0
  109. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/builder/2024.10.txt +0 -0
  110. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/builder/2025.06.txt +0 -0
  111. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/builder/PREVIEW.txt +0 -0
  112. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/builder/README.md +0 -0
  113. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/builder/base-images.json +0 -0
  114. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/call_graph.py +0 -0
  115. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/__init__.py +0 -0
  116. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/_download.py +0 -0
  117. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/_help.py +0 -0
  118. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/_traceback.py +0 -0
  119. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/billing.py +0 -0
  120. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/bootstrap.py +0 -0
  121. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/changelog.py +0 -0
  122. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/cluster.py +0 -0
  123. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/config.py +0 -0
  124. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/container.py +0 -0
  125. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/dashboard.py +0 -0
  126. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/dict.py +0 -0
  127. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/image.py +0 -0
  128. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/import_refs.py +0 -0
  129. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/launch.py +0 -0
  130. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/logo.py +0 -0
  131. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/network_file_system.py +0 -0
  132. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/profile.py +0 -0
  133. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/programs/__init__.py +0 -0
  134. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/programs/run_jupyter.py +0 -0
  135. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/programs/vscode.py +0 -0
  136. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/queues.py +0 -0
  137. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/run.py +0 -0
  138. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/secret.py +0 -0
  139. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/selector.py +0 -0
  140. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/shell.py +0 -0
  141. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/skills.py +0 -0
  142. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/token.py +0 -0
  143. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cli/volume.py +0 -0
  144. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/client.py +0 -0
  145. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cloud_bucket_mount.py +0 -0
  146. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cloud_bucket_mount.pyi +0 -0
  147. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cls.py +0 -0
  148. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/cls.pyi +0 -0
  149. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/config.py +0 -0
  150. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/container_process.py +0 -0
  151. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/container_process.pyi +0 -0
  152. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/dict.py +0 -0
  153. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/dict.pyi +0 -0
  154. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/environments.py +0 -0
  155. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/environments.pyi +0 -0
  156. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/exception.py +0 -0
  157. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/experimental/__init__.py +0 -0
  158. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/experimental/flash.py +0 -0
  159. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/experimental/flash.pyi +0 -0
  160. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/experimental/ipython.py +0 -0
  161. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/file_io.py +0 -0
  162. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/file_io.pyi +0 -0
  163. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/file_pattern_matcher.py +0 -0
  164. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/functions.py +0 -0
  165. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/functions.pyi +0 -0
  166. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/image.py +0 -0
  167. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/image.pyi +0 -0
  168. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/io_streams.py +0 -0
  169. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/io_streams.pyi +0 -0
  170. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/mount.py +0 -0
  171. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/mount.pyi +0 -0
  172. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/network_file_system.py +0 -0
  173. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/network_file_system.pyi +0 -0
  174. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/object.py +0 -0
  175. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/object.pyi +0 -0
  176. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/output.py +0 -0
  177. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/parallel_map.py +0 -0
  178. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/parallel_map.pyi +0 -0
  179. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/partial_function.py +0 -0
  180. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/partial_function.pyi +0 -0
  181. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/proxy.py +0 -0
  182. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/proxy.pyi +0 -0
  183. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/py.typed +0 -0
  184. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/queue.py +0 -0
  185. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/queue.pyi +0 -0
  186. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/retries.py +0 -0
  187. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/runner.py +0 -0
  188. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/runner.pyi +0 -0
  189. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/running_app.py +0 -0
  190. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/sandbox_fs.py +0 -0
  191. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/sandbox_fs.pyi +0 -0
  192. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/schedule.py +0 -0
  193. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/scheduler_placement.py +0 -0
  194. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/server.py +0 -0
  195. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/server.pyi +0 -0
  196. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/serving.py +0 -0
  197. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/serving.pyi +0 -0
  198. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/skills/modal/SKILL.md +0 -0
  199. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/snapshot.py +0 -0
  200. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/snapshot.pyi +0 -0
  201. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/stream_type.py +0 -0
  202. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/token_flow.py +0 -0
  203. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/token_flow.pyi +0 -0
  204. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/volume.py +0 -0
  205. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal/volume.pyi +0 -0
  206. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal.egg-info/dependency_links.txt +0 -0
  207. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal.egg-info/entry_points.txt +0 -0
  208. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal.egg-info/requires.txt +0 -0
  209. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal.egg-info/top_level.txt +0 -0
  210. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_docs/__init__.py +0 -0
  211. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_docs/gen_cli_docs.py +0 -0
  212. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_docs/gen_cli_docs_main.py +0 -0
  213. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_docs/gen_reference_docs.py +0 -0
  214. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_docs/gen_reference_docs_main.py +0 -0
  215. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_docs/mdmd/__init__.py +0 -0
  216. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_docs/mdmd/mdmd.py +0 -0
  217. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_docs/mdmd/signatures.py +0 -0
  218. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_docs/mdmd/types.py +0 -0
  219. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/__init__.py +0 -0
  220. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/py.typed +0 -0
  221. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/task_command_router_grpc.py +0 -0
  222. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/task_command_router_pb2.py +0 -0
  223. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/task_command_router_pb2.pyi +0 -0
  224. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  225. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  226. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/modal_version/__main__.py +0 -0
  227. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/pyproject.toml +0 -0
  228. {modal-1.5.1.dev14 → modal-1.5.1.dev15}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.1.dev14
3
+ Version: 1.5.1.dev15
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -2965,17 +2965,16 @@ class _Image(_Object, type_prefix="im"):
2965
2965
  """
2966
2966
  tag = _parse_named_image_ref(name)
2967
2967
 
2968
- if not self.is_hydrated:
2968
+ if self._object_id is None:
2969
2969
  raise InvalidError("Cannot publish an image that has not been created yet. Call `.build()` first.")
2970
2970
 
2971
- _client = client or self._client
2972
- assert _client is not None
2971
+ _client = client or await _Client.from_env()
2973
2972
 
2974
2973
  resolved_env = environment_name or config.get("environment") or ""
2975
2974
 
2976
2975
  await _client.stub.ImagePublish(
2977
2976
  api_pb2.ImagePublishRequest(
2978
- image_id=self.object_id,
2977
+ image_id=self._object_id,
2979
2978
  environment_name=resolved_env,
2980
2979
  is_public=False,
2981
2980
  tag=tag,
@@ -17,6 +17,11 @@ from modal.config import config, logger
17
17
 
18
18
  CUDA_CHECKPOINT_PATH: str = config.get("cuda_checkpoint_path")
19
19
 
20
+ # Exit code used to signal to modal-runtime that a GPU memory snapshot restore failed
21
+ # and the task should be retried without a snapshot. This is a sentinel value that must
22
+ # stay in sync with `GPU_SNAPSHOT_RESTORE_FAILED_EXIT_CODE` in runner.rs.
23
+ SNAPSHOT_RESTORE_FAILED_EXIT_CODE: int = 222
24
+
20
25
  # Maximum total duration for each individual `cuda-checkpoint` invocation.
21
26
  CUDA_CHECKPOINT_TIMEOUT: float = 3 * 60.0
22
27
 
@@ -180,10 +180,15 @@ class _TaskLifecycleManager:
180
180
  if self.function_def._experimental_enable_gpu_snapshot and self.function_def.resources.gpu_config.gpu_type:
181
181
  logger.debug("GPU memory snapshot enabled. Attempting to restore GPU memory.")
182
182
 
183
- assert self._cuda_checkpoint_session, (
184
- "CudaCheckpointSession not found when attempting to restore GPU memory"
185
- )
186
- self._cuda_checkpoint_session.restore()
183
+ try:
184
+ if self._cuda_checkpoint_session is None:
185
+ raise gpu_memory_snapshot.CudaCheckpointException("CudaCheckpointSession not found")
186
+ self._cuda_checkpoint_session.restore()
187
+ except gpu_memory_snapshot.CudaCheckpointException as exc:
188
+ logger.warning(f"GPU memory snapshot restore failed; retrying task without snapshot. Error: {exc}")
189
+ sys.stderr.flush()
190
+ # Exit with a sentinel code that the runtime will use to retry the task without a snapshot.
191
+ os._exit(gpu_memory_snapshot.SNAPSHOT_RESTORE_FAILED_EXIT_CODE)
187
192
 
188
193
  self._client = await _Client.from_env()
189
194
 
@@ -121,10 +121,154 @@ class _Workspace(_Object, type_prefix="ac"):
121
121
  def billing(self) -> "_WorkspaceBillingManager":
122
122
  return _WorkspaceBillingManager(self)
123
123
 
124
+ @property
125
+ def proxy_tokens(self) -> "_WorkspaceProxyTokenManager":
126
+ return _WorkspaceProxyTokenManager(self)
127
+
128
+
129
+ @dataclass(frozen=True)
130
+ class TokenData:
131
+ """A token ID / secret pair."""
132
+
133
+ token_id: str
134
+ token_secret: str
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class ProxyTokenInfo:
139
+ """Metadata about a proxy token, not including the token secret."""
140
+
141
+ token_id: str
142
+ created_at: datetime
143
+ scoped: bool
144
+
145
+
146
+ class _WorkspaceProxyTokenManager:
147
+ """mdmd:namespace
148
+ Namespace with methods for managing the proxy tokens in a Workspace.
149
+
150
+ See [the guide](https://modal.com/docs/guide/webhook-proxy-auth) for more information on proxy tokens.
151
+ """
152
+
153
+ def __init__(self, workspace: "_Workspace"):
154
+ """mdmd:hidden"""
155
+ self._workspace = workspace
156
+
157
+ async def create(self) -> TokenData:
158
+ """Create a new proxy token for the Workspace.
159
+
160
+ Examples:
161
+ ```python notest
162
+ token = modal.Workspace.from_context().proxy_tokens.create()
163
+ print(token.token_id, token.token_secret)
164
+ ```
165
+ """
166
+ await self._workspace.hydrate()
167
+ resp = await self._workspace.client.stub.WebhookTokenCreate(api_pb2.WebhookTokenCreateRequest())
168
+ return TokenData(token_id=resp.token_id, token_secret=resp.token_secret)
169
+
170
+ async def list(self, environment_name: Optional[str] = None) -> builtins.list[ProxyTokenInfo]:
171
+ """List proxy tokens in the Workspace.
172
+
173
+ Args:
174
+ environment_name: When provided, list only the tokens associated with this environment.
175
+
176
+ Examples:
177
+ ```python notest
178
+ ws = modal.Workspace.from_context()
179
+
180
+ # List all proxy tokens in the Workspace
181
+ tokens = ws.proxy_tokens.list()
182
+ print([t.token_id for t in tokens])
183
+
184
+ # List only the proxy tokens associated with a specific Environment
185
+ env_tokens = ws.proxy_tokens.list(environment_name="prod")
186
+ ```
187
+ """
188
+ await self._workspace.hydrate()
189
+ if environment_name is None:
190
+ resp = await self._workspace.client.stub.WebhookTokenList(Empty())
191
+ else:
192
+ resp = await self._workspace.client.stub.WebhookTokenListForEnvironment(
193
+ api_pb2.WebhookTokenListForEnvironmentRequest(environment_name=environment_name)
194
+ )
195
+ return [
196
+ ProxyTokenInfo(
197
+ token_id=token.token_id,
198
+ created_at=timestamp_to_localized_dt(token.created_at),
199
+ scoped=token.scoped,
200
+ )
201
+ for token in resp.tokens
202
+ ]
203
+
204
+ async def allow(self, proxy_token_id: str, environment_name: str) -> None:
205
+ """Allow a proxy token to authenticate requests to a given Environment.
206
+
207
+ Args:
208
+ proxy_token_id: The token ID (`wk-...`) to operate on.
209
+ environment_name: The name of the environment to allow access to.
210
+
211
+ Examples:
212
+ ```python notest
213
+ ws = modal.Workspace.from_context()
214
+ token = ws.proxy_tokens.create()
215
+ ws.proxy_tokens.allow(token.token_id, "prod")
216
+ ```
217
+ """
218
+ await self._workspace.hydrate()
219
+ environment_id = await self._environment_id(environment_name)
220
+ req = api_pb2.WebhookTokenEnvironmentAddRequest(token_id=proxy_token_id, environment_id=environment_id)
221
+ await self._workspace.client.stub.WebhookTokenEnvironmentAdd(req)
222
+
223
+ async def revoke(self, proxy_token_id: str, environment_name: str) -> None:
224
+ """Revoke a proxy token's access to a given Environment.
225
+
226
+ The proxy token is not deleted, and it will continue to authenticate requests to any
227
+ other Environments it is associated with.
228
+
229
+ Args:
230
+ proxy_token_id: The token ID (`wk-...`) to operate on.
231
+ environment_name: The name of the environment to revoke access from.
232
+
233
+ Examples:
234
+ ```python notest
235
+ ws = modal.Workspace.from_context()
236
+ ws.proxy_tokens.revoke(token_id, "prod")
237
+ ```
238
+ """
239
+ await self._workspace.hydrate()
240
+ environment_id = await self._environment_id(environment_name)
241
+ req = api_pb2.WebhookTokenEnvironmentRemoveRequest(token_id=proxy_token_id, environment_id=environment_id)
242
+ await self._workspace.client.stub.WebhookTokenEnvironmentRemove(req)
243
+
244
+ async def delete(self, proxy_token_id: str) -> None:
245
+ """Delete a proxy token from the Workspace.
246
+
247
+ This cannot be reverted. Any clients currently using the token will immediately
248
+ lose access to associated resources.
249
+
250
+ Args:
251
+ proxy_token_id: The token ID (`wk-...`) to delete.
252
+
253
+ Examples:
254
+ ```python notest
255
+ modal.Workspace.from_context().proxy_tokens.delete(token_id)
256
+ ```
257
+ """
258
+ await self._workspace.hydrate()
259
+ await self._workspace.client.stub.WebhookTokenDelete(api_pb2.TokenDeleteRequest(token_id=proxy_token_id))
260
+
261
+ async def _environment_id(self, environment_name: str) -> str:
262
+ # The environment-association RPCs key on environment ID, so resolve the name first.
263
+ from ._environments import _Environment
264
+
265
+ environment = await _Environment.from_name(environment_name, client=self._workspace.client).hydrate()
266
+ return environment.object_id
267
+
124
268
 
125
269
  class _WorkspaceBillingManager:
126
270
  """mdmd:namespace
127
- Namespace for Workspace billing APIs
271
+ Namespace for Workspace billing APIs.
128
272
  """
129
273
 
130
274
  def __init__(self, workspace: _Workspace):
@@ -4,7 +4,7 @@ import sys
4
4
  import time
5
5
  import warnings
6
6
  from datetime import datetime, timedelta, timezone
7
- from typing import get_args
7
+ from typing import Literal, get_args
8
8
 
9
9
  import click
10
10
  import rich
@@ -268,6 +268,7 @@ async def logs(
268
268
  ```
269
269
 
270
270
  """
271
+ env = ensure_env(env)
271
272
 
272
273
  if follow and (since or until or tail):
273
274
  raise UsageError("--follow cannot be combined with --since, --until, or --tail.")
@@ -353,11 +354,18 @@ async def logs(
353
354
  @app_cli.command("rollback", no_args_is_help=True, context_settings={"ignore_unknown_options": True})
354
355
  @click.argument("app_identifier")
355
356
  @click.argument("version", default="")
357
+ @click.option(
358
+ "--strategy",
359
+ default="rolling",
360
+ type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE)),
361
+ help="Strategy for rollback",
362
+ )
356
363
  @env_option
357
364
  @synchronizer.create_blocking
358
365
  async def rollback(
359
366
  app_identifier: str,
360
367
  version: str = "",
368
+ strategy: Literal["recreate", "rolling"] = "rolling",
361
369
  *,
362
370
  env: str | None = None,
363
371
  ):
@@ -403,8 +411,20 @@ async def rollback(
403
411
  else:
404
412
  raise UsageError(f"Invalid version specifier: {version}")
405
413
  req = api_pb2.AppRollbackRequest(app_id=app_id, version=version_number)
406
- await client.stub.AppRollback(req)
407
- rich.print("[green]✓[/green] Deployment rollback successful!")
414
+ resp = await client.stub.AppRollback(req)
415
+ print_server_warnings(resp.server_warnings)
416
+ output_mgr = OutputManager.get()
417
+ if strategy == "recreate":
418
+ try:
419
+ await _stop_and_wait_for_containers(client, app_id, resp.deployed_at, env)
420
+ except Exception as exc:
421
+ warnings.warn(
422
+ f"App rollback executed successfully, but containers did not all terminate. {exc}", UserWarning
423
+ )
424
+ output_mgr.print(f"\nView Deployment: [magenta]{resp.url}[/magenta]")
425
+ sys.exit(1)
426
+ output_mgr.print("[green]✓[/green] Deployment rollback successful!")
427
+ output_mgr.print(f"\nView Deployment: [magenta]{resp.url}[/magenta]")
408
428
 
409
429
 
410
430
  @app_cli.command("rollover", no_args_is_help=True)
@@ -626,6 +646,7 @@ async def dashboard(
626
646
  modal app dashboard my-app --env dev
627
647
  ```
628
648
  """
649
+ env = ensure_env(env)
629
650
  client = await _Client.from_env()
630
651
  app_id, _, _ = await resolve_app_identifier(app_identifier, env, client)
631
652
  url = f"https://modal.com/id/{app_id}"
@@ -29,6 +29,7 @@ from .secret import secret_cli
29
29
  from .skills import skills_cli
30
30
  from .token import _new_token, token_cli
31
31
  from .volume import volume_cli
32
+ from .workspace import workspace_cli
32
33
 
33
34
 
34
35
  @click.group(
@@ -117,11 +118,12 @@ entrypoint_cli.add_command(volume_cli, panel="Storage")
117
118
  entrypoint_cli.add_command(setup, panel="Onboarding")
118
119
  entrypoint_cli.add_command(bootstrap, panel="Onboarding")
119
120
 
120
- entrypoint_cli.add_command(config_cli, panel="Configuration")
121
+ entrypoint_cli.add_command(workspace_cli, panel="Configuration")
121
122
  entrypoint_cli.add_command(environment_cli, panel="Configuration")
122
123
  entrypoint_cli.add_command(profile_cli, panel="Configuration")
123
- entrypoint_cli.add_command(skills_cli, panel="Configuration")
124
+ entrypoint_cli.add_command(config_cli, panel="Configuration")
124
125
  entrypoint_cli.add_command(token_cli, panel="Configuration")
126
+ entrypoint_cli.add_command(skills_cli, panel="Configuration")
125
127
 
126
128
  entrypoint_cli.add_command(billing_cli, panel="Observability")
127
129
  entrypoint_cli.add_command(changelog, panel="Observability")
@@ -187,11 +187,10 @@ environment_cli.add_command(billing_cli)
187
187
 
188
188
 
189
189
  @billing_cli.command("report", no_args_is_help=True)
190
- @click.option(
191
- "-n",
192
- "--environment-name",
190
+ @click.argument(
191
+ "environment_name",
192
+ required=False,
193
193
  default=None,
194
- help="Name of the environment to build a report for. If no argument is provided, uses the current default.",
195
194
  )
196
195
  @click.option("--start", default=None, help=f"Start date. {DATE_HELP}")
197
196
  @click.option("--end", default=None, help=f"End date. {DATE_HELP} Defaults to now.")
@@ -244,19 +243,19 @@ async def environment_billing(
244
243
  Examples:
245
244
 
246
245
  ```bash
247
- modal environment billing report -n env1 --start 2025-12-01 --end 2026-01-01
246
+ modal environment billing report --start 2025-12-01 --end 2026-01-01
248
247
 
249
- modal environment billing report -n env2 --for "last month" --tag-names team,project
248
+ modal environment billing report --for "last month" --tag-names team,project
250
249
 
251
- modal environment billing report -n test_env --for today --resolution h
250
+ modal environment billing report test_env --for today --resolution h
252
251
 
253
- modal environment billing report -n test_env --for "this month" --show-resources
252
+ modal environment billing report test_env --for "this month" --show-resources
254
253
 
255
- modal environment billing report -n prod_env --for yesterday -r h --tz local
254
+ modal environment billing report prod_env --for yesterday -r h --tz local
256
255
 
257
- modal environment billing report -n main_env --for "last month" --csv > report.csv
256
+ modal environment billing report main_env --for "last month" --csv > report.csv
258
257
 
259
- modal environment billing report -n main_env --start 2025-12-01 --json > report.json
258
+ modal environment billing report main_env --start 2025-12-01 --json > report.json
260
259
  ```
261
260
 
262
261
  """
@@ -123,7 +123,7 @@ async def fetch_app_logs(
123
123
  await _drain_batches(output_mgr, batches, prefix_fields or [], filters.search_text)
124
124
 
125
125
 
126
- def _plain(text: Text | str) -> str:
126
+ def _plain(text: "Text | str | bool | None") -> "str | bool | None":
127
127
  return text.plain if isinstance(text, Text) else text
128
128
 
129
129
 
@@ -138,7 +138,7 @@ def is_tty() -> bool:
138
138
 
139
139
  def display_table(
140
140
  columns: Sequence[Column | str],
141
- rows: Sequence[Sequence[Text | str]],
141
+ rows: Sequence[Sequence["Text | str | bool | None"]],
142
142
  json: bool = False,
143
143
  csv: bool = False,
144
144
  title: str = "",
@@ -164,7 +164,12 @@ def display_table(
164
164
  else:
165
165
  table = Table(*columns, title=title)
166
166
  for row in rows:
167
- table.add_row(*row)
167
+ # rich can't render bare scalars like bools; stringify anything that isn't already
168
+ # a renderable (str/Text) or None (which rich treats as an empty cell).
169
+ cells: list[Text | str | None] = [
170
+ cell if cell is None or isinstance(cell, (str, Text)) else str(cell) for cell in row
171
+ ]
172
+ table.add_row(*cells)
168
173
  output.print(table)
169
174
 
170
175
 
@@ -0,0 +1,129 @@
1
+ # Copyright Modal Labs 2025
2
+ import json as json_mod
3
+
4
+ import click
5
+ import rich
6
+
7
+ from modal._utils.time_utils import timestamp_to_localized_str
8
+ from modal.cli.utils import confirm_or_suggest_yes, display_table, yes_option
9
+ from modal.output import OutputManager
10
+ from modal.workspace import Workspace
11
+
12
+ from ._help import ModalGroup
13
+
14
+ WORKSPACE_HELP_TEXT = """Interact with the current Modal Workspace.
15
+
16
+ A Workspace is the top-level account that owns your Modal resources. Use these commands
17
+ to manage workspace-level settings such as proxy tokens.
18
+ """
19
+
20
+ workspace_cli = ModalGroup(name="workspace", help=WORKSPACE_HELP_TEXT)
21
+
22
+
23
+ MEMBERS_HELP_TEXT = """View the members of the current Workspace."""
24
+
25
+ members_cli = ModalGroup(name="members", help=MEMBERS_HELP_TEXT)
26
+ workspace_cli.add_command(members_cli)
27
+
28
+
29
+ @members_cli.command("list", help="List the members of the current Workspace.")
30
+ @click.option("--json", is_flag=True, default=False)
31
+ def members_list(json: bool = False):
32
+ members = Workspace.from_context().members.list()
33
+ rows = [
34
+ [
35
+ member.name,
36
+ member.email,
37
+ member.role,
38
+ member.user_id,
39
+ timestamp_to_localized_str(member.joined_at.timestamp(), json),
40
+ timestamp_to_localized_str(member.last_active_at.timestamp(), json) if member.last_active_at else None,
41
+ ]
42
+ for member in members
43
+ ]
44
+ display_table(["Name", "Email", "Role", "User ID", "Joined", "Last active"], rows, json=json)
45
+
46
+
47
+ PROXY_TOKENS_HELP_TEXT = """Manage the proxy tokens of the current Workspace.
48
+
49
+ Proxy tokens provide authentication to HTTP interfaces on Modal Servers and Web Functions.
50
+ They are passed as request headers (`Modal-Key` and `Modal-Secret`). See
51
+ https://modal.com/docs/guide/webhook-proxy-auth for more information.
52
+
53
+ Proxy tokens and secrets have `wk-` and `ws-` prefixes, respectively. The cannot be
54
+ interchanged with API tokens (which use `ak-` and `as-` prefixes).
55
+
56
+ On workspaces with RBAC enabled, tokens are scoped to specific environments;
57
+ use the `allow` and `revoke` commands to manage environment associations.
58
+ """
59
+
60
+ proxy_tokens_cli = ModalGroup(name="proxy-tokens", help=PROXY_TOKENS_HELP_TEXT)
61
+ workspace_cli.add_command(proxy_tokens_cli)
62
+
63
+
64
+ @proxy_tokens_cli.command("create", help="Create a proxy token in the current Workspace.")
65
+ @click.option("--json", is_flag=True, default=False)
66
+ def proxy_tokens_create(*, json: bool = False):
67
+ """Create a proxy token in the current Workspace.
68
+
69
+ The new token's ID and secret will be printed to stdout. The secret is only
70
+ shown at creation time and cannot be retrieved later.
71
+ """
72
+ token = Workspace.from_context().proxy_tokens.create()
73
+ output_manager = OutputManager.get()
74
+ if json:
75
+ output_manager.print_json(json_mod.dumps({"Modal-Key": token.token_id, "Modal-Secret": token.token_secret}))
76
+ else:
77
+ output_manager.print(f"Modal-Key: {token.token_id}\nModal-Secret: {token.token_secret}")
78
+
79
+
80
+ @proxy_tokens_cli.command("list", help="List the proxy tokens of the current Workspace.")
81
+ @click.option(
82
+ "-e",
83
+ "--environment",
84
+ default=None,
85
+ help="Only list tokens associated with this environment. Lists all tokens when omitted.",
86
+ )
87
+ @click.option("--json", is_flag=True, default=False)
88
+ def proxy_tokens_list(environment: str | None = None, json: bool = False):
89
+ tokens = Workspace.from_context().proxy_tokens.list(environment_name=environment)
90
+ # Emit a real boolean for JSON output, but a string for the rich table (which can't render a bare bool).
91
+ rows = [
92
+ [
93
+ token.token_id,
94
+ timestamp_to_localized_str(token.created_at.timestamp(), json),
95
+ token.scoped if json else str(token.scoped),
96
+ ]
97
+ for token in tokens
98
+ ]
99
+ display_table(["Token ID", "Created at", "Scoped"], rows, json=json)
100
+
101
+
102
+ @proxy_tokens_cli.command("allow", help="Allow a proxy token to authenticate to an environment.", no_args_is_help=True)
103
+ @click.argument("token_id")
104
+ @click.argument("environment_name")
105
+ def proxy_tokens_allow(token_id: str, environment_name: str):
106
+ Workspace.from_context().proxy_tokens.allow(token_id, environment_name)
107
+ rich.print(f"[green]✓[/green] Allowed proxy token {token_id!r} to authenticate to environment {environment_name!r}")
108
+
109
+
110
+ @proxy_tokens_cli.command("revoke", help="Revoke a proxy token's access to an environment.", no_args_is_help=True)
111
+ @click.argument("token_id")
112
+ @click.argument("environment_name")
113
+ def proxy_tokens_revoke(token_id: str, environment_name: str):
114
+ Workspace.from_context().proxy_tokens.revoke(token_id, environment_name)
115
+ rich.print(f"[green]✓[/green] Revoked proxy token {token_id!r} access to environment {environment_name!r}")
116
+
117
+
118
+ @proxy_tokens_cli.command("delete", help="Delete a proxy token from the current Workspace.", no_args_is_help=True)
119
+ @click.argument("token_id")
120
+ @yes_option
121
+ def proxy_tokens_delete(token_id: str, yes: bool = False):
122
+ if not yes:
123
+ message = (
124
+ f"Are you sure you want to delete proxy token {token_id!r}? "
125
+ "Any web endpoints relying on it will no longer accept it."
126
+ )
127
+ confirm_or_suggest_yes(message)
128
+ Workspace.from_context().proxy_tokens.delete(token_id)
129
+ rich.print(f"[green]✓[/green] Deleted proxy token {token_id!r}")
@@ -35,7 +35,7 @@ class _Client:
35
35
  server_url: str,
36
36
  client_type: int,
37
37
  credentials: typing.Optional[tuple[str, str]],
38
- version: str = "1.5.1.dev14",
38
+ version: str = "1.5.1.dev15",
39
39
  ):
40
40
  """mdmd:hidden
41
41
  The Modal client object is not intended to be instantiated directly by users.
@@ -205,7 +205,7 @@ class Client:
205
205
  server_url: str,
206
206
  client_type: int,
207
207
  credentials: typing.Optional[tuple[str, str]],
208
- version: str = "1.5.1.dev14",
208
+ version: str = "1.5.1.dev15",
209
209
  ):
210
210
  """mdmd:hidden
211
211
  The Modal client object is not intended to be instantiated directly by users.
@@ -16,6 +16,8 @@ from dataclasses import dataclass
16
16
  from pathlib import PurePosixPath
17
17
  from typing import TYPE_CHECKING, Any, Literal, Union, overload
18
18
 
19
+ from modal.secret import _split_env_dict_and_resolvable_secrets
20
+
19
21
  from ._output.pty import get_pty_info
20
22
  from .config import config, logger
21
23
 
@@ -843,6 +845,7 @@ class _Sandbox(_Object, type_prefix="sb"):
843
845
  encrypted_ports: Sequence[int] = [],
844
846
  h2_ports: Sequence[int] = [],
845
847
  unencrypted_ports: Sequence[int] = [],
848
+ readiness_probe: Probe | None = None,
846
849
  include_oidc_identity_token: bool = False,
847
850
  verbose: bool = False,
848
851
  client: _Client | None = None,
@@ -851,11 +854,11 @@ class _Sandbox(_Object, type_prefix="sb"):
851
854
 
852
855
  Supported features include exec, encrypted tunnels, wait/poll/terminate,
853
856
  CPU and memory configuration, region placement, volumes, cloud bucket mounts
854
- (with static credentials via `secret=...`), and filesystem snapshots.
857
+ (with static credentials via `secret=...` or `oidc_auth_role_arn`), OIDC
858
+ identity tokens, and filesystem snapshots.
855
859
 
856
860
  Features like tags, memory snapshots, network file systems, GPUs, custom
857
- domains, OIDC identity tokens (including `oidc_auth_role_arn` on a
858
- CloudBucketMount), and proxies are not supported.
861
+ domains, and proxies are not supported.
859
862
 
860
863
  V2 sandboxes created with this method are not currently returned by
861
864
  `Sandbox.list()` and cannot be looked up with `Sandbox.from_name()`.
@@ -878,20 +881,6 @@ class _Sandbox(_Object, type_prefix="sb"):
878
881
  cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
879
882
  validated_volumes = [(k, v) for k, v in validated_volumes if isinstance(v, _Volume)]
880
883
 
881
- # The V2 backend does not yet propagate `oidc_identity_token` from server to worker - it is silently dropped
882
- # TODO(akshay) add support for OIDC tokens
883
- if include_oidc_identity_token:
884
- raise InvalidError(
885
- "Sandbox._experimental_create does not support include_oidc_identity_token=True. "
886
- "Use Sandbox.create instead."
887
- )
888
- for path, cbm in cloud_bucket_mounts:
889
- if cbm.oidc_auth_role_arn:
890
- raise InvalidError(
891
- "Sandbox._experimental_create does not support CloudBucketMount with oidc_auth_role_arn "
892
- f"(at mount path {path!r}). Use static credentials via `secret=...`, or use Sandbox.create."
893
- )
894
-
895
884
  secrets = secrets or []
896
885
  ephemeral_env: dict[str, str] = {}
897
886
  if env:
@@ -981,6 +970,7 @@ class _Sandbox(_Object, type_prefix="sb"):
981
970
  inbound_cidr_allowlist=list(inbound_cidr_allowlist) if inbound_cidr_allowlist is not None else [],
982
971
  volume_mounts=volume_mounts,
983
972
  cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
973
+ readiness_probe=(readiness_probe._to_proto() if readiness_probe else None),
984
974
  )
985
975
 
986
976
  create_req = api_pb2.SandboxCreateV2Request(
@@ -1857,10 +1847,11 @@ class _Sandbox(_Object, type_prefix="sb"):
1857
1847
  _validate_exec_args(args)
1858
1848
 
1859
1849
  secrets = list(secrets or [])
1860
- env_dict = {k: v for k, v in (env or {}).items() if v is not None}
1850
+ env_dict, resolvable_secrets = _split_env_dict_and_resolvable_secrets(secrets)
1851
+ env_dict |= {k: v for k, v in (env or {}).items() if v is not None}
1861
1852
 
1862
1853
  # Force explicit secret resolution so we can pass the secret IDs to the backend.
1863
- secret_coros = [secret.hydrate(client=self._client) for secret in secrets]
1854
+ secret_coros = [secret.hydrate(client=self._client) for secret in resolvable_secrets]
1864
1855
  await TaskContext.gather(*secret_coros)
1865
1856
 
1866
1857
  task_id = await self._get_task_id(raise_if_task_complete=True)
@@ -1878,7 +1869,7 @@ class _Sandbox(_Object, type_prefix="sb"):
1878
1869
  stderr=stderr,
1879
1870
  timeout=timeout,
1880
1871
  workdir=workdir,
1881
- secret_ids=[secret.object_id for secret in secrets],
1872
+ secret_ids=[secret.object_id for secret in resolvable_secrets],
1882
1873
  env=env_dict,
1883
1874
  text=text,
1884
1875
  bufsize=bufsize,