modal 1.5.2.dev20__tar.gz → 1.5.2.dev22__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 (231) hide show
  1. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/PKG-INFO +1 -1
  2. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/task_command_router_client.py +127 -3
  3. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/_help.py +7 -1
  4. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/container.py +10 -2
  5. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/curl.py +6 -4
  6. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/shell.py +6 -2
  7. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/client.pyi +2 -2
  8. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/container_process.py +13 -1
  9. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/container_process.pyi +27 -0
  10. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/sandbox.py +73 -17
  11. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/sandbox.pyi +126 -18
  12. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/sandbox_fs.py +32 -80
  13. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/sandbox_fs.pyi +24 -0
  14. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/types.py +7 -1
  15. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/volume.py +26 -1
  16. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/volume.pyi +26 -0
  17. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/PKG-INFO +1 -1
  18. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/mdmd/mdmd.py +34 -11
  19. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/api_grpc.py +64 -0
  20. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/api_pb2.py +955 -855
  21. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/api_pb2.pyi +218 -4
  22. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/api_pb2_grpc.py +133 -1
  23. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/api_pb2_grpc.pyi +43 -3
  24. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/modal_api_grpc.py +4 -0
  25. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_version/__init__.py +1 -1
  26. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/LICENSE +0 -0
  27. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/README.md +0 -0
  28. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/__init__.py +0 -0
  29. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/__main__.py +0 -0
  30. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_billing.py +0 -0
  31. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_clustered_functions.py +0 -0
  32. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_clustered_functions.pyi +0 -0
  33. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_container_entrypoint.py +0 -0
  34. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_environments.py +0 -0
  35. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_function_variants.py +0 -0
  36. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_functions.py +0 -0
  37. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_grpc_client.py +0 -0
  38. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_image.py +0 -0
  39. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_ipython.py +0 -0
  40. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_load_context.py +0 -0
  41. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_location.py +0 -0
  42. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_logs.py +0 -0
  43. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_object.py +0 -0
  44. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_output/__init__.py +0 -0
  45. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_output/manager.py +0 -0
  46. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_output/pty.py +0 -0
  47. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_output/rich.py +0 -0
  48. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_output/status.py +0 -0
  49. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_partial_function.py +0 -0
  50. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_resolver.py +0 -0
  51. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_resources.py +0 -0
  52. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/__init__.py +0 -0
  53. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/asgi.py +0 -0
  54. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/container_io_manager.py +0 -0
  55. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/container_io_manager.pyi +0 -0
  56. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/execution_context.py +0 -0
  57. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/execution_context.pyi +0 -0
  58. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  59. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/task_lifecycle_manager.py +0 -0
  60. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  61. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/telemetry.py +0 -0
  62. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/user_code_event_loop.py +0 -0
  63. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_runtime/user_code_imports.py +0 -0
  64. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_serialization.py +0 -0
  65. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_server.py +0 -0
  66. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_traceback.py +0 -0
  67. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_tunnel.py +0 -0
  68. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_tunnel.pyi +0 -0
  69. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_type_manager.py +0 -0
  70. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/__init__.py +0 -0
  71. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/app_utils.py +0 -0
  72. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/async_utils.py +0 -0
  73. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/auth_token_manager.py +0 -0
  74. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/blob_utils.py +0 -0
  75. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/browser_utils.py +0 -0
  76. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/bytes_io_segment_payload.py +0 -0
  77. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/curl_utils.py +0 -0
  78. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/deprecation.py +0 -0
  79. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/docker_utils.py +0 -0
  80. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/function_utils.py +0 -0
  81. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/git_utils.py +0 -0
  82. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/grpc_testing.py +0 -0
  83. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/grpc_utils.py +0 -0
  84. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/hash_utils.py +0 -0
  85. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/http_utils.py +0 -0
  86. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/jwt_utils.py +0 -0
  87. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/logger.py +0 -0
  88. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/mount_utils.py +0 -0
  89. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/name_utils.py +0 -0
  90. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/package_utils.py +0 -0
  91. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/pattern_utils.py +0 -0
  92. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/proxy_support.py +0 -0
  93. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/rand_pb_testing.py +0 -0
  94. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/sandbox_fs_utils.py +0 -0
  95. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/shell_utils.py +0 -0
  96. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_utils/time_utils.py +0 -0
  97. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_vendor/__init__.py +0 -0
  98. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  99. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_vendor/cloudpickle.py +0 -0
  100. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_vendor/tblib.py +0 -0
  101. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_vendor/version.py +0 -0
  102. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_watcher.py +0 -0
  103. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/_workspace.py +0 -0
  104. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/app.py +0 -0
  105. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/app.pyi +0 -0
  106. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/billing.py +0 -0
  107. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/billing.pyi +0 -0
  108. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/2023.12.312.txt +0 -0
  109. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/2023.12.txt +0 -0
  110. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/2024.04.txt +0 -0
  111. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/2024.10.txt +0 -0
  112. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/2025.06.txt +0 -0
  113. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/PREVIEW.txt +0 -0
  114. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/README.md +0 -0
  115. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/builder/base-images.json +0 -0
  116. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/call_graph.py +0 -0
  117. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/__init__.py +0 -0
  118. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/_download.py +0 -0
  119. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/_traceback.py +0 -0
  120. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/app.py +0 -0
  121. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/billing.py +0 -0
  122. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/bootstrap.py +0 -0
  123. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/changelog.py +0 -0
  124. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/cluster.py +0 -0
  125. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/config.py +0 -0
  126. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/dashboard.py +0 -0
  127. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/dict.py +0 -0
  128. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/endpoint.py +0 -0
  129. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/entry_point.py +0 -0
  130. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/environment.py +0 -0
  131. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/image.py +0 -0
  132. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/import_refs.py +0 -0
  133. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/launch.py +0 -0
  134. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/logo.py +0 -0
  135. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/network_file_system.py +0 -0
  136. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/profile.py +0 -0
  137. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/programs/__init__.py +0 -0
  138. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/programs/run_jupyter.py +0 -0
  139. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/programs/vscode.py +0 -0
  140. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/queues.py +0 -0
  141. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/run.py +0 -0
  142. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/secret.py +0 -0
  143. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/selector.py +0 -0
  144. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/skills.py +0 -0
  145. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/token.py +0 -0
  146. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/utils.py +0 -0
  147. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/volume.py +0 -0
  148. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cli/workspace.py +0 -0
  149. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/client.py +0 -0
  150. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cloud_bucket_mount.py +0 -0
  151. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cloud_bucket_mount.pyi +0 -0
  152. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cls.py +0 -0
  153. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/cls.pyi +0 -0
  154. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/config.py +0 -0
  155. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/dict.py +0 -0
  156. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/dict.pyi +0 -0
  157. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/environments.py +0 -0
  158. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/environments.pyi +0 -0
  159. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/exception.py +0 -0
  160. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/experimental/__init__.py +0 -0
  161. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/experimental/flash.py +0 -0
  162. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/experimental/flash.pyi +0 -0
  163. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/experimental/ipython.py +0 -0
  164. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/file_io.py +0 -0
  165. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/file_io.pyi +0 -0
  166. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/file_pattern_matcher.py +0 -0
  167. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/functions.py +0 -0
  168. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/functions.pyi +0 -0
  169. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/image.py +0 -0
  170. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/image.pyi +0 -0
  171. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/io_streams.py +0 -0
  172. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/io_streams.pyi +0 -0
  173. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/mount.py +0 -0
  174. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/mount.pyi +0 -0
  175. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/network_file_system.py +0 -0
  176. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/network_file_system.pyi +0 -0
  177. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/object.py +0 -0
  178. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/object.pyi +0 -0
  179. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/output.py +0 -0
  180. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/parallel_map.py +0 -0
  181. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/parallel_map.pyi +0 -0
  182. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/partial_function.py +0 -0
  183. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/partial_function.pyi +0 -0
  184. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/proxy.py +0 -0
  185. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/proxy.pyi +0 -0
  186. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/py.typed +0 -0
  187. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/queue.py +0 -0
  188. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/queue.pyi +0 -0
  189. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/retries.py +0 -0
  190. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/runner.py +0 -0
  191. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/runner.pyi +0 -0
  192. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/running_app.py +0 -0
  193. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/schedule.py +0 -0
  194. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/scheduler_placement.py +0 -0
  195. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/secret.py +0 -0
  196. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/secret.pyi +0 -0
  197. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/server.py +0 -0
  198. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/server.pyi +0 -0
  199. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/serving.py +0 -0
  200. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/serving.pyi +0 -0
  201. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/skills/modal/SKILL.md +0 -0
  202. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/snapshot.py +0 -0
  203. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/snapshot.pyi +0 -0
  204. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/stream_type.py +0 -0
  205. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/token_flow.py +0 -0
  206. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/token_flow.pyi +0 -0
  207. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/workspace.py +0 -0
  208. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal/workspace.pyi +0 -0
  209. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/SOURCES.txt +0 -0
  210. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/dependency_links.txt +0 -0
  211. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/entry_points.txt +0 -0
  212. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/requires.txt +0 -0
  213. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal.egg-info/top_level.txt +0 -0
  214. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/__init__.py +0 -0
  215. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/gen_cli_docs.py +0 -0
  216. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/gen_cli_docs_main.py +0 -0
  217. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/gen_reference_docs.py +0 -0
  218. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/gen_reference_docs_main.py +0 -0
  219. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/mdmd/__init__.py +0 -0
  220. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/mdmd/signatures.py +0 -0
  221. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_docs/mdmd/types.py +0 -0
  222. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/__init__.py +0 -0
  223. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/py.typed +0 -0
  224. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/task_command_router_grpc.py +0 -0
  225. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2.py +0 -0
  226. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2.pyi +0 -0
  227. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  228. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  229. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/modal_version/__main__.py +0 -0
  230. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/pyproject.toml +0 -0
  231. {modal-1.5.2.dev20 → modal-1.5.2.dev22}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.2.dev20
3
+ Version: 1.5.2.dev22
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2025
2
2
  import asyncio
3
3
  import base64
4
+ import io
4
5
  import json
5
6
  import socket
6
7
  import ssl
@@ -8,9 +9,9 @@ import time
8
9
  import typing
9
10
  import urllib.parse
10
11
  import weakref
11
- from collections.abc import AsyncGenerator, Callable
12
+ from collections.abc import AsyncGenerator, AsyncIterable, Callable
12
13
  from contextlib import suppress
13
- from typing import TypeVar
14
+ from typing import BinaryIO, TypeVar
14
15
 
15
16
  import grpclib.client
16
17
  import grpclib.config
@@ -18,7 +19,7 @@ from grpclib import GRPCError, Status
18
19
  from grpclib.exceptions import StreamTerminatedError
19
20
 
20
21
  from modal.config import logger
21
- from modal.exception import ExecTimeoutError, TimeoutError as ModalTimeoutError
22
+ from modal.exception import AuthError, ExecTimeoutError, InternalError, ServiceError, TimeoutError as ModalTimeoutError
22
23
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
23
24
  from modal_proto.task_command_router_grpc import TaskCommandRouterStub
24
25
 
@@ -27,6 +28,17 @@ from .._utils.grpc_utils import ModalChannel
27
28
  from .async_utils import aclosing, retry
28
29
  from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES
29
30
 
31
+ STREAMING_STDIN_CHUNK_SIZE = 256 * 1024
32
+
33
+ _STREAMING_STDIN_RESUMABLE_EXCEPTIONS = (
34
+ AuthError,
35
+ InternalError,
36
+ ServiceError,
37
+ StreamTerminatedError,
38
+ OSError,
39
+ asyncio.TimeoutError,
40
+ )
41
+
30
42
 
31
43
  @retry(n_attempts=34, base_delay=1, max_delay=10, attempt_timeout=10, total_timeout=310)
32
44
  async def _connect_channel(channel: grpclib.client.Channel):
@@ -472,6 +484,118 @@ class TaskCommandRouterClient:
472
484
  lambda: self._call_with_auth_retry(self._stub.TaskExecStdinWrite, request)
473
485
  )
474
486
 
487
+ async def exec_stdin_write_stream(
488
+ self,
489
+ task_id: str,
490
+ exec_id: str,
491
+ source: BinaryIO,
492
+ chunk_size: int = STREAMING_STDIN_CHUNK_SIZE,
493
+ max_resume_attempts: int = 9,
494
+ ) -> int:
495
+ """Stream `source` into the exec's stdin, with bounded resume on transient failures.
496
+
497
+ Streams the full contents of `source` in one client-streaming RPC and
498
+ closes stdin (EOF) on success. On a resumable error, queries
499
+ `exec_stdin_status` for the server's canonical offset, seeks `source`
500
+ to that point, and reopens the stream.
501
+
502
+ Args:
503
+ task_id: The task ID of the task running the exec'd command.
504
+ exec_id: The execution ID of the command to write to.
505
+ source: A seekable byte source (file, BytesIO, etc.).
506
+ chunk_size: Bytes per outbound message.
507
+ max_resume_attempts: Upper bound on resume retries before giving up.
508
+
509
+ Returns:
510
+ The total number of bytes streamed into the exec's stdin.
511
+ """
512
+ offset = 0
513
+ attempt = 0
514
+ while True:
515
+
516
+ async def _chunks():
517
+ while True:
518
+ chunk = source.read(chunk_size)
519
+ if not chunk:
520
+ return
521
+ yield chunk
522
+
523
+ try:
524
+ source.seek(offset)
525
+ await self._exec_stdin_write_stream(task_id, exec_id, offset, _chunks())
526
+ return source.tell()
527
+ except (*_STREAMING_STDIN_RESUMABLE_EXCEPTIONS, AttributeError) as e:
528
+ # StreamTerminatedError surfaces as AttributeError in grpclib<=0.4.7;
529
+ # see call_with_retries_on_transient_errors.
530
+ if isinstance(e, AttributeError) and "_write_appdata" not in str(e):
531
+ raise
532
+ attempt += 1
533
+ if attempt > max_resume_attempts:
534
+ raise
535
+ status = await self.exec_stdin_status(task_id=task_id, exec_id=exec_id)
536
+ if status.closed:
537
+ pos = source.tell()
538
+ if status.num_bytes_written == pos and source.seek(0, io.SEEK_END) == pos:
539
+ logger.debug(f"exec_stdin_write_stream completed but response was lost: {e}")
540
+ return pos
541
+ raise
542
+ offset = status.num_bytes_written
543
+ logger.debug(f"exec_stdin_write_stream resuming from offset {offset} after error: {e}")
544
+
545
+ async def _exec_stdin_write_stream(
546
+ self,
547
+ task_id: str,
548
+ exec_id: str,
549
+ start_offset: int,
550
+ chunks: AsyncIterable[bytes],
551
+ ) -> sr_pb2.TaskExecStdinWriteStreamResponse | None:
552
+ """Single client-streaming attempt: Start, Data chunks, then End (EOF).
553
+
554
+ Does not retry; `exec_stdin_write_stream` owns resume.
555
+ """
556
+ with grpc_error_converter():
557
+ stream = self._stub.TaskExecStdinWriteStream.open(metadata=self._get_metadata())
558
+ async with stream as s:
559
+ start = sr_pb2.TaskExecStdinWriteStreamRequest(
560
+ start=sr_pb2.TaskExecStdinWriteStreamStart(
561
+ task_id=task_id,
562
+ exec_id=exec_id,
563
+ offset=start_offset,
564
+ ),
565
+ )
566
+ await s.send_message(start)
567
+ async for data in chunks:
568
+ if not data:
569
+ continue
570
+ await s.send_message(sr_pb2.TaskExecStdinWriteStreamRequest(data=data))
571
+ # The server closes stdin only on this explicit End message. A
572
+ # stream that breaks before it leaves stdin open for resume.
573
+ await s.send_message(
574
+ sr_pb2.TaskExecStdinWriteStreamRequest(end=sr_pb2.TaskExecStdinWriteStreamEnd()),
575
+ end=True,
576
+ )
577
+ return await s.recv_message()
578
+
579
+ async def exec_stdin_status(self, task_id: str, exec_id: str) -> sr_pb2.TaskExecStdinStatusResponse:
580
+ """Read the current stdin write status for an exec'd command, to support retries from the right offset.
581
+
582
+ Used by streaming clients to find the resume offset after a stream
583
+ failure. Evicts any in-flight stdin stream for the exec, so it is not
584
+ safe to call for read-only observability.
585
+
586
+ Args:
587
+ task_id: The task ID of the task running the exec'd command.
588
+ exec_id: The execution ID of the command to query.
589
+ Raises:
590
+ Other errors: If retries are exhausted on transient errors or if there's an error
591
+ from the RPC itself.
592
+ """
593
+ request = sr_pb2.TaskExecStdinStatusRequest(task_id=task_id, exec_id=exec_id)
594
+ with grpc_error_converter():
595
+ return await call_with_retries_on_transient_errors(
596
+ lambda: self._call_with_auth_retry(self._stub.TaskExecStdinStatus, request)
597
+ )
598
+
475
599
  async def sandbox_stdin_write_v2(
476
600
  self, task_id: str, offset: int, data: bytes, eof: bool
477
601
  ) -> sr_pb2.SandboxStdinWriteV2Response:
@@ -70,7 +70,13 @@ def _option_label(param: click.Parameter, ctx: click.Context) -> Text:
70
70
  text.append(opt, style=_OPTION_FLAG_STYLE)
71
71
  if not getattr(param, "is_flag", False) and not getattr(param, "count", False):
72
72
  text.append(" ")
73
- text.append(param.make_metavar(ctx), style=_OPTION_METAVAR_STYLE)
73
+ if "ctx" in inspect.signature(click.Parameter.make_metavar).parameters:
74
+ # click >= 8.2.0
75
+ metavar = param.make_metavar(ctx)
76
+ else:
77
+ # backwards compatibility
78
+ metavar = param.make_metavar() # type: ignore[call-arg]
79
+ text.append(metavar, style=_OPTION_METAVAR_STYLE)
74
80
  return text
75
81
 
76
82
 
@@ -269,15 +269,23 @@ async def _exec_impl(
269
269
  pty: bool | None = None,
270
270
  container_id: str = "",
271
271
  command: tuple[str, ...] = (),
272
+ object_id_for_v2: str | None = None,
272
273
  ):
273
- """Execute a command in a container (implementation)."""
274
+ """Execute a command in a container (implementation).
275
+
276
+ For tasks belonging to V2 sandboxes, `object_id_for_v2` must be set to the
277
+ sandbox ID.
278
+ """
274
279
 
275
280
  if pty is None:
276
281
  pty = is_tty()
277
282
 
278
283
  client = await _Client.from_env()
279
284
 
280
- command_router_client = await TaskCommandRouterClient.init(client, container_id)
285
+ if object_id_for_v2 is not None:
286
+ command_router_client = await TaskCommandRouterClient.init_v2(client, object_id_for_v2, container_id)
287
+ else:
288
+ command_router_client = await TaskCommandRouterClient.init(client, container_id)
281
289
 
282
290
  process_id = str(uuid.uuid4())
283
291
 
@@ -117,10 +117,12 @@ async def curl(curl_args: tuple[str, ...]):
117
117
  All arguments are passed directly to `curl`, which must be installed locally.
118
118
 
119
119
  Examples:
120
- ```bash
121
- modal curl https://user--my-app.us-west.modal.direct
122
- modal curl -X GET https://user--my-app.us-west.modal.direct
123
- ```
120
+
121
+ ```bash
122
+ modal curl https://user--my-app.us-west.modal.direct
123
+ modal curl -X GET https://user--my-app.us-west.modal.direct
124
+ ```
125
+
124
126
  """
125
127
  url = find_url(curl_args)
126
128
  if url is None:
@@ -17,7 +17,7 @@ from ..functions import Function
17
17
  from ..image import Image
18
18
  from ..mount import _Mount
19
19
  from ..runner import interactive_shell
20
- from ..sandbox import _MAIN_CONTAINER_NAME, Sandbox
20
+ from ..sandbox import _MAIN_CONTAINER_NAME, Sandbox, _is_v2_sandbox_id
21
21
  from ..secret import Secret
22
22
  from ..stream_type import StreamType
23
23
  from ..volume import Volume
@@ -135,6 +135,8 @@ def _start_shell_in_running_container(ref: str, cmd: str, pty: bool) -> None:
135
135
  if container_name is not None and container_name != _MAIN_CONTAINER_NAME:
136
136
  return _start_shell_in_sidecar_container(sandbox_id, container_name, cmd, pty)
137
137
 
138
+ object_id_for_v2: str | None = None
139
+
138
140
  if _is_valid_modal_id(sandbox_id, "sb-"):
139
141
  try:
140
142
  sandbox = Sandbox.from_id(sandbox_id)
@@ -143,10 +145,12 @@ def _start_shell_in_running_container(ref: str, cmd: str, pty: bool) -> None:
143
145
  raise ClickException(f"Sandbox '{sandbox_id}' not found (is it still running?)")
144
146
  except Exception as e:
145
147
  raise ClickException(f"Error connecting to Sandbox '{sandbox_id}': {str(e)}")
148
+ if _is_v2_sandbox_id(sandbox_id):
149
+ object_id_for_v2 = sandbox_id
146
150
 
147
151
  assert _is_valid_modal_id(ref, "ta-")
148
152
  try:
149
- _exec_impl(container_id=ref, command=shlex.split(cmd), pty=pty)
153
+ _exec_impl(container_id=ref, command=shlex.split(cmd), pty=pty, object_id_for_v2=object_id_for_v2)
150
154
  except NotFoundError:
151
155
  raise ClickException(f"Container '{ref}' not found (is it still running?)")
152
156
  except Exception as e:
@@ -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.2.dev20",
38
+ version: str = "1.5.2.dev22",
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.2.dev20",
208
+ version: str = "1.5.2.dev22",
209
209
  ):
210
210
  """mdmd:hidden
211
211
  The Modal client object is not intended to be instantiated directly by users.
@@ -1,7 +1,7 @@
1
1
  # Copyright Modal Labs 2024
2
2
  import asyncio
3
3
  import platform
4
- from typing import Generic, TypeVar
4
+ from typing import BinaryIO, Generic, TypeVar
5
5
 
6
6
  from modal_proto import api_pb2
7
7
 
@@ -117,6 +117,18 @@ class _ContainerProcess(Generic[T]):
117
117
  )
118
118
  return self._returncode
119
119
 
120
+ async def _stdin_write_stream(self, source: BinaryIO) -> int:
121
+ """mdmd:hidden
122
+ Stream `source` into the process's stdin and close it (EOF) on success.
123
+
124
+ Returns the total bytes streamed.
125
+ """
126
+ return await self._command_router_client.exec_stdin_write_stream(
127
+ task_id=self._task_id,
128
+ exec_id=self._process_id,
129
+ source=source,
130
+ )
131
+
120
132
  async def poll(self) -> int | None:
121
133
  """Check if the container process has finished running.
122
134
 
@@ -53,6 +53,14 @@ class _ContainerProcess(typing.Generic[T]):
53
53
 
54
54
  @property
55
55
  def returncode(self) -> int: ...
56
+ async def _stdin_write_stream(self, source: typing.BinaryIO) -> int:
57
+ """mdmd:hidden
58
+ Stream `source` into the process's stdin and close it (EOF) on success.
59
+
60
+ Returns the total bytes streamed.
61
+ """
62
+ ...
63
+
56
64
  async def poll(self) -> typing.Optional[int]:
57
65
  """Check if the container process has finished running.
58
66
 
@@ -105,6 +113,25 @@ class ContainerProcess(typing.Generic[T]):
105
113
  @property
106
114
  def returncode(self) -> int: ...
107
115
 
116
+ class ___stdin_write_stream_spec(typing_extensions.Protocol):
117
+ def __call__(self, /, source: typing.BinaryIO) -> int:
118
+ """mdmd:hidden
119
+ Stream `source` into the process's stdin and close it (EOF) on success.
120
+
121
+ Returns the total bytes streamed.
122
+ """
123
+ ...
124
+
125
+ async def aio(self, /, source: typing.BinaryIO) -> int:
126
+ """mdmd:hidden
127
+ Stream `source` into the process's stdin and close it (EOF) on success.
128
+
129
+ Returns the total bytes streamed.
130
+ """
131
+ ...
132
+
133
+ _stdin_write_stream: ___stdin_write_stream_spec
134
+
108
135
  class __poll_spec(typing_extensions.Protocol):
109
136
  def __call__(self, /) -> typing.Optional[int]:
110
137
  """Check if the container process has finished running.
@@ -816,6 +816,7 @@ class _Sandbox(_Object, type_prefix="sb"):
816
816
  *args: str,
817
817
  app: "modal.app._App | None" = None,
818
818
  name: str | None = None,
819
+ tags: dict[str, str] | None = None,
819
820
  image: _Image | None = None,
820
821
  env: dict[str, str | None] | None = None,
821
822
  secrets: Collection[_Secret] | None = None,
@@ -851,7 +852,7 @@ class _Sandbox(_Object, type_prefix="sb"):
851
852
  `secret=...` or `oidc_auth_role_arn`), OIDC identity tokens, proxies, and
852
853
  filesystem snapshots.
853
854
 
854
- Features like tags, memory snapshots, network file systems, GPUs, and custom
855
+ Features like memory snapshots, network file systems, GPUs, and custom
855
856
  domains are not supported.
856
857
 
857
858
  Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
@@ -861,9 +862,10 @@ class _Sandbox(_Object, type_prefix="sb"):
861
862
  (e.g. `region="us-east-1"`).
862
863
 
863
864
  V2 sandboxes created with this method are not currently returned by
864
- `Sandbox.list()` and cannot be looked up with `Sandbox.from_name()`.
865
- Store `sandbox.object_id` if you need to retrieve the sandbox later, and
866
- use `Sandbox.from_id(sandbox.object_id)` to reattach.
865
+ `Sandbox.list()`. A named sandbox can be looked up with
866
+ `Sandbox._experimental_from_name(app_name, name)`; otherwise store
867
+ `sandbox.object_id` and use `Sandbox.from_id(sandbox.object_id)` to
868
+ reattach.
867
869
  """
868
870
  from .app import _App
869
871
 
@@ -992,10 +994,12 @@ class _Sandbox(_Object, type_prefix="sb"):
992
994
  ),
993
995
  )
994
996
 
997
+ tag_protos = [api_pb2.SandboxTag(tag_name=k, tag_value=v) for k, v in tags.items()] if tags else []
995
998
  create_req = api_pb2.SandboxCreateV2Request(
996
999
  app_id=load_context.app_id,
997
1000
  definition=definition,
998
1001
  ephemeral_secrets=api_pb2.StringMap(contents=env_dict) if env_dict else None,
1002
+ tags=tag_protos,
999
1003
  )
1000
1004
  assert load_context.client._auth_token_manager
1001
1005
  auth_token = await load_context.client._auth_token_manager.get_token()
@@ -1186,6 +1190,44 @@ class _Sandbox(_Object, type_prefix="sb"):
1186
1190
  resp = await client.stub.SandboxGetFromName(req)
1187
1191
  return _Sandbox._new_hydrated(resp.sandbox_id, client, None)
1188
1192
 
1193
+ @staticmethod
1194
+ async def _experimental_from_name(
1195
+ app_name: str,
1196
+ name: str,
1197
+ *,
1198
+ environment_name: str | None = None,
1199
+ client: _Client | None = None,
1200
+ ) -> "_Sandbox":
1201
+ """Get a running V2 Sandbox by name from a deployed App.
1202
+
1203
+ This looks up V2 sandboxes, ie sandboxes created via `modal.Sandbox._experimental_create`.
1204
+
1205
+ Args:
1206
+ app_name: Name of the deployed app to look up the sandbox under.
1207
+ name: Sandbox name to resolve.
1208
+ environment_name: Optional environment name for the lookup; defaults to the configured environment.
1209
+ client: Modal client to use for the RPC; defaults to `Client.from_env()` when omitted.
1210
+
1211
+ Returns:
1212
+ A `Sandbox` handle for the running sandbox.
1213
+
1214
+ Raises:
1215
+ NotFoundError: If no running sandbox exists with the given name.
1216
+ """
1217
+ if client is None:
1218
+ client = await _Client.from_env()
1219
+ env_name = _get_environment_name(environment_name)
1220
+
1221
+ req = api_pb2.SandboxGetFromNameRequest(sandbox_name=name, app_name=app_name, environment_name=env_name)
1222
+ assert client._auth_token_manager
1223
+ auth_token = await client._auth_token_manager.get_token()
1224
+ resp = await client.stub.SandboxGetFromNameV2(req, metadata=[("x-modal-auth-token", auth_token)])
1225
+
1226
+ obj = _Sandbox._new_hydrated(resp.sandbox_id, client, None)
1227
+ obj._is_v2 = True
1228
+ obj._hydrate_metadata_v2()
1229
+ return obj
1230
+
1189
1231
  @staticmethod
1190
1232
  async def from_id(sandbox_id: str, client: _Client | None = None) -> "_Sandbox":
1191
1233
  """Construct a Sandbox from an id and look up the Sandbox result.
@@ -1228,22 +1270,27 @@ class _Sandbox(_Object, type_prefix="sb"):
1228
1270
  Returns:
1229
1271
  Tags as a map from tag name to tag value.
1230
1272
  """
1231
- self._ensure_v1("get_tags")
1232
1273
  req = api_pb2.SandboxTagsGetRequest(sandbox_id=self.object_id)
1233
- resp = await self._client.stub.SandboxTagsGet(req)
1274
+ stub = self._client.stub
1275
+ if self._is_v2:
1276
+ assert self._client._auth_token_manager
1277
+ auth_token = await self._client._auth_token_manager.get_token()
1278
+ resp = await stub.SandboxTagsGetV2(req, metadata=[("x-modal-auth-token", auth_token)])
1279
+ else:
1280
+ resp = await stub.SandboxTagsGet(req)
1234
1281
 
1235
1282
  return {tag.tag_name: tag.tag_value for tag in resp.tags}
1236
1283
 
1237
1284
  async def set_tags(self, tags: dict[str, str], *, client: _Client | None = None) -> None:
1238
1285
  """Set tags (key-value pairs) on the Sandbox. Tags can be used to filter results in `Sandbox.list`.
1239
1286
 
1287
+ Setting tags replaces the Sandbox's entire tag set; passing an empty dict clears all tags.
1288
+
1240
1289
  Args:
1241
1290
  tags: Tag names and values to set on this sandbox.
1242
1291
  client: Deprecated. Prefer setting the client when creating or re-attaching to the sandbox.
1243
1292
 
1244
1293
  """
1245
- self._ensure_v1("set_tags")
1246
- environment_name = _get_environment_name()
1247
1294
  if client is not None:
1248
1295
  deprecation_warning(
1249
1296
  (2025, 9, 18),
@@ -1252,13 +1299,19 @@ class _Sandbox(_Object, type_prefix="sb"):
1252
1299
  )
1253
1300
 
1254
1301
  tags_list = [api_pb2.SandboxTag(tag_name=name, tag_value=value) for name, value in tags.items()]
1255
-
1256
- req = api_pb2.SandboxTagsSetRequest(
1257
- environment_name=environment_name,
1258
- sandbox_id=self.object_id,
1259
- tags=tags_list,
1260
- )
1261
- await self._client.stub.SandboxTagsSet(req)
1302
+ stub = self._client.stub
1303
+ if self._is_v2:
1304
+ assert self._client._auth_token_manager
1305
+ auth_token = await self._client._auth_token_manager.get_token()
1306
+ req = api_pb2.SandboxTagsSetRequest(sandbox_id=self.object_id, tags=tags_list)
1307
+ await stub.SandboxTagsSetV2(req, metadata=[("x-modal-auth-token", auth_token)])
1308
+ else:
1309
+ req = api_pb2.SandboxTagsSetRequest(
1310
+ environment_name=_get_environment_name(),
1311
+ sandbox_id=self.object_id,
1312
+ tags=tags_list,
1313
+ )
1314
+ await stub.SandboxTagsSet(req)
1262
1315
 
1263
1316
  async def _experimental_set_outbound_network_policy(
1264
1317
  self,
@@ -2279,15 +2332,15 @@ class _Sandbox(_Object, type_prefix="sb"):
2279
2332
 
2280
2333
  @staticmethod
2281
2334
  async def _experimental_list(
2282
- *, app_id: str | None = None, client: _Client | None = None
2335
+ *, app_id: str | None = None, tags: dict[str, str] | None = None, client: _Client | None = None
2283
2336
  ) -> AsyncGenerator["_Sandbox", None]:
2284
2337
  """List v2 Sandboxes in an App.
2285
2338
 
2286
2339
  This function lists v2 sandboxes, ie sandboxes created via modal.Sandbox._experimental_create.
2287
- Filtering based on tags is not yet supported.
2288
2340
 
2289
2341
  Args:
2290
2342
  app_id: The App to list Sandboxes under.
2343
+ tags: If set, only sandboxes containing at least these tags are returned.
2291
2344
  client: Optional client to use for the session.
2292
2345
 
2293
2346
  Yields:
@@ -2304,12 +2357,15 @@ class _Sandbox(_Object, type_prefix="sb"):
2304
2357
  if client is None:
2305
2358
  client = await _Client.from_env()
2306
2359
 
2360
+ tags_list = [api_pb2.SandboxTag(tag_name=name, tag_value=value) for name, value in tags.items()] if tags else []
2361
+
2307
2362
  assert client._auth_token_manager
2308
2363
  while True:
2309
2364
  req = api_pb2.SandboxListRequest(
2310
2365
  app_id=app_id,
2311
2366
  before_timestamp=before_timestamp,
2312
2367
  include_finished=False,
2368
+ tags=tags_list,
2313
2369
  )
2314
2370
 
2315
2371
  # Fetches a batch of sandboxes. SandboxListV2 authenticates via the