modal 1.5.2.dev19__tar.gz → 1.5.2.dev21__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.dev19 → modal-1.5.2.dev21}/PKG-INFO +1 -1
  2. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_environments.py +6 -9
  3. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_image.py +36 -7
  4. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_workspace.py +9 -3
  5. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/container.py +15 -7
  6. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/curl.py +6 -4
  7. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/environment.py +2 -2
  8. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/image.py +8 -1
  9. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/shell.py +6 -2
  10. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/client.pyi +2 -2
  11. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/environments.pyi +6 -14
  12. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/functions.pyi +6 -6
  13. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/sandbox.py +47 -5
  14. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/sandbox.pyi +114 -15
  15. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/types.py +8 -5
  16. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/volume.py +26 -1
  17. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/volume.pyi +26 -0
  18. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/workspace.pyi +2 -2
  19. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal.egg-info/PKG-INFO +1 -1
  20. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_docs/mdmd/mdmd.py +34 -11
  21. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/api_grpc.py +48 -0
  22. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/api_pb2.py +816 -763
  23. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/api_pb2.pyi +106 -5
  24. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/api_pb2_grpc.py +100 -1
  25. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/api_pb2_grpc.pyi +33 -3
  26. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/modal_api_grpc.py +3 -0
  27. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_version/__init__.py +1 -1
  28. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/LICENSE +0 -0
  29. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/README.md +0 -0
  30. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/__init__.py +0 -0
  31. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/__main__.py +0 -0
  32. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_billing.py +0 -0
  33. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_clustered_functions.py +0 -0
  34. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_clustered_functions.pyi +0 -0
  35. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_container_entrypoint.py +0 -0
  36. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_function_variants.py +0 -0
  37. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_functions.py +0 -0
  38. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_grpc_client.py +0 -0
  39. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_ipython.py +0 -0
  40. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_load_context.py +0 -0
  41. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_location.py +0 -0
  42. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_logs.py +0 -0
  43. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_object.py +0 -0
  44. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_output/__init__.py +0 -0
  45. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_output/manager.py +0 -0
  46. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_output/pty.py +0 -0
  47. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_output/rich.py +0 -0
  48. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_output/status.py +0 -0
  49. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_partial_function.py +0 -0
  50. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_resolver.py +0 -0
  51. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_resources.py +0 -0
  52. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/__init__.py +0 -0
  53. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/asgi.py +0 -0
  54. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/container_io_manager.py +0 -0
  55. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/container_io_manager.pyi +0 -0
  56. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/execution_context.py +0 -0
  57. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/execution_context.pyi +0 -0
  58. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  59. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/task_lifecycle_manager.py +0 -0
  60. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  61. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/telemetry.py +0 -0
  62. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/user_code_event_loop.py +0 -0
  63. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_runtime/user_code_imports.py +0 -0
  64. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_serialization.py +0 -0
  65. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_server.py +0 -0
  66. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_traceback.py +0 -0
  67. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_tunnel.py +0 -0
  68. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_tunnel.pyi +0 -0
  69. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_type_manager.py +0 -0
  70. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/__init__.py +0 -0
  71. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/app_utils.py +0 -0
  72. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/async_utils.py +0 -0
  73. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/auth_token_manager.py +0 -0
  74. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/blob_utils.py +0 -0
  75. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/browser_utils.py +0 -0
  76. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/bytes_io_segment_payload.py +0 -0
  77. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/curl_utils.py +0 -0
  78. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/deprecation.py +0 -0
  79. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/docker_utils.py +0 -0
  80. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/function_utils.py +0 -0
  81. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/git_utils.py +0 -0
  82. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/grpc_testing.py +0 -0
  83. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/grpc_utils.py +0 -0
  84. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/hash_utils.py +0 -0
  85. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/http_utils.py +0 -0
  86. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/jwt_utils.py +0 -0
  87. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/logger.py +0 -0
  88. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/mount_utils.py +0 -0
  89. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/name_utils.py +0 -0
  90. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/package_utils.py +0 -0
  91. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/pattern_utils.py +0 -0
  92. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/proxy_support.py +0 -0
  93. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/rand_pb_testing.py +0 -0
  94. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/sandbox_fs_utils.py +0 -0
  95. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/shell_utils.py +0 -0
  96. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/task_command_router_client.py +0 -0
  97. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_utils/time_utils.py +0 -0
  98. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_vendor/__init__.py +0 -0
  99. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  100. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_vendor/cloudpickle.py +0 -0
  101. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_vendor/tblib.py +0 -0
  102. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_vendor/version.py +0 -0
  103. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/_watcher.py +0 -0
  104. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/app.py +0 -0
  105. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/app.pyi +0 -0
  106. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/billing.py +0 -0
  107. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/billing.pyi +0 -0
  108. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/builder/2023.12.312.txt +0 -0
  109. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/builder/2023.12.txt +0 -0
  110. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/builder/2024.04.txt +0 -0
  111. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/builder/2024.10.txt +0 -0
  112. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/builder/2025.06.txt +0 -0
  113. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/builder/PREVIEW.txt +0 -0
  114. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/builder/README.md +0 -0
  115. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/builder/base-images.json +0 -0
  116. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/call_graph.py +0 -0
  117. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/__init__.py +0 -0
  118. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/_download.py +0 -0
  119. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/_help.py +0 -0
  120. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/_traceback.py +0 -0
  121. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/app.py +0 -0
  122. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/billing.py +0 -0
  123. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/bootstrap.py +0 -0
  124. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/changelog.py +0 -0
  125. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/cluster.py +0 -0
  126. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/config.py +0 -0
  127. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/dashboard.py +0 -0
  128. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/dict.py +0 -0
  129. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/endpoint.py +0 -0
  130. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/entry_point.py +0 -0
  131. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/import_refs.py +0 -0
  132. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/launch.py +0 -0
  133. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/logo.py +0 -0
  134. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/network_file_system.py +0 -0
  135. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/profile.py +0 -0
  136. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/programs/__init__.py +0 -0
  137. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/programs/run_jupyter.py +0 -0
  138. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/programs/vscode.py +0 -0
  139. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/queues.py +0 -0
  140. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/run.py +0 -0
  141. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/secret.py +0 -0
  142. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/selector.py +0 -0
  143. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/skills.py +0 -0
  144. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/token.py +0 -0
  145. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/utils.py +0 -0
  146. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/volume.py +0 -0
  147. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cli/workspace.py +0 -0
  148. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/client.py +0 -0
  149. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cloud_bucket_mount.py +0 -0
  150. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cloud_bucket_mount.pyi +0 -0
  151. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cls.py +0 -0
  152. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/cls.pyi +0 -0
  153. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/config.py +0 -0
  154. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/container_process.py +0 -0
  155. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/container_process.pyi +0 -0
  156. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/dict.py +0 -0
  157. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/dict.pyi +0 -0
  158. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/environments.py +0 -0
  159. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/exception.py +0 -0
  160. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/experimental/__init__.py +0 -0
  161. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/experimental/flash.py +0 -0
  162. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/experimental/flash.pyi +0 -0
  163. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/experimental/ipython.py +0 -0
  164. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/file_io.py +0 -0
  165. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/file_io.pyi +0 -0
  166. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/file_pattern_matcher.py +0 -0
  167. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/functions.py +0 -0
  168. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/image.py +0 -0
  169. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/image.pyi +0 -0
  170. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/io_streams.py +0 -0
  171. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/io_streams.pyi +0 -0
  172. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/mount.py +0 -0
  173. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/mount.pyi +0 -0
  174. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/network_file_system.py +0 -0
  175. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/network_file_system.pyi +0 -0
  176. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/object.py +0 -0
  177. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/object.pyi +0 -0
  178. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/output.py +0 -0
  179. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/parallel_map.py +0 -0
  180. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/parallel_map.pyi +0 -0
  181. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/partial_function.py +0 -0
  182. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/partial_function.pyi +0 -0
  183. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/proxy.py +0 -0
  184. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/proxy.pyi +0 -0
  185. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/py.typed +0 -0
  186. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/queue.py +0 -0
  187. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/queue.pyi +0 -0
  188. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/retries.py +0 -0
  189. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/runner.py +0 -0
  190. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/runner.pyi +0 -0
  191. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/running_app.py +0 -0
  192. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/sandbox_fs.py +0 -0
  193. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/sandbox_fs.pyi +0 -0
  194. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/schedule.py +0 -0
  195. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/scheduler_placement.py +0 -0
  196. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/secret.py +0 -0
  197. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/secret.pyi +0 -0
  198. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/server.py +0 -0
  199. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/server.pyi +0 -0
  200. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/serving.py +0 -0
  201. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/serving.pyi +0 -0
  202. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/skills/modal/SKILL.md +0 -0
  203. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/snapshot.py +0 -0
  204. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/snapshot.pyi +0 -0
  205. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/stream_type.py +0 -0
  206. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/token_flow.py +0 -0
  207. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/token_flow.pyi +0 -0
  208. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal/workspace.py +0 -0
  209. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal.egg-info/SOURCES.txt +0 -0
  210. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal.egg-info/dependency_links.txt +0 -0
  211. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal.egg-info/entry_points.txt +0 -0
  212. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal.egg-info/requires.txt +0 -0
  213. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal.egg-info/top_level.txt +0 -0
  214. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_docs/__init__.py +0 -0
  215. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_docs/gen_cli_docs.py +0 -0
  216. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_docs/gen_cli_docs_main.py +0 -0
  217. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_docs/gen_reference_docs.py +0 -0
  218. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_docs/gen_reference_docs_main.py +0 -0
  219. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_docs/mdmd/__init__.py +0 -0
  220. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_docs/mdmd/signatures.py +0 -0
  221. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_docs/mdmd/types.py +0 -0
  222. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/__init__.py +0 -0
  223. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/py.typed +0 -0
  224. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/task_command_router_grpc.py +0 -0
  225. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/task_command_router_pb2.py +0 -0
  226. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/task_command_router_pb2.pyi +0 -0
  227. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  228. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  229. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/modal_version/__main__.py +0 -0
  230. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/pyproject.toml +0 -0
  231. {modal-1.5.2.dev19 → modal-1.5.2.dev21}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.2.dev19
3
+ Version: 1.5.2.dev21
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -103,9 +103,6 @@ class _EnvironmentManager:
103
103
  await client.stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
104
104
 
105
105
 
106
- MemberRole = Literal["viewer", "contributor"]
107
-
108
-
109
106
  def _role_to_proto(role: str) -> "api_pb2.EnvironmentRole.ValueType":
110
107
  match role:
111
108
  case "viewer":
@@ -116,7 +113,7 @@ def _role_to_proto(role: str) -> "api_pb2.EnvironmentRole.ValueType":
116
113
  raise InvalidError(f"Invalid Environment role: {role!r} (expected 'viewer' or 'contributor')")
117
114
 
118
115
 
119
- def _role_from_proto(proto_value: int) -> MemberRole:
116
+ def _role_from_proto(proto_value: int) -> str:
120
117
  match proto_value:
121
118
  case api_pb2.ENVIRONMENT_ROLE_VIEWER:
122
119
  return "viewer"
@@ -137,7 +134,7 @@ class _EnvironmentMembersManager:
137
134
  """mdmd:hidden"""
138
135
  self._environment = environment
139
136
 
140
- async def list(self) -> dict[Literal["users", "service_users"], dict[str, MemberRole]]:
137
+ async def list(self) -> dict[Literal["users", "service_users"], dict[str, str]]:
141
138
  """Return the members of a restricted Environment with their roles.
142
139
 
143
140
  **Examples:**
@@ -155,8 +152,8 @@ class _EnvironmentMembersManager:
155
152
  req = api_pb2.EnvironmentGetManagedRequest(environment_id=self._environment.object_id)
156
153
  resp = await self._environment.client.stub.EnvironmentGetManaged(req)
157
154
 
158
- users: dict[str, MemberRole] = {}
159
- service_users: dict[str, MemberRole] = {}
155
+ users: dict[str, str] = {}
156
+ service_users: dict[str, str] = {}
160
157
  for principal in resp.principal_roles:
161
158
  role = _role_from_proto(principal.role)
162
159
  if principal.user_id:
@@ -169,8 +166,8 @@ class _EnvironmentMembersManager:
169
166
  async def update(
170
167
  self,
171
168
  *,
172
- users: Mapping[str, MemberRole] | None = None,
173
- service_users: Mapping[str, MemberRole] | None = None,
169
+ users: Mapping[str, str] | None = None,
170
+ service_users: Mapping[str, str] | None = None,
174
171
  ) -> None:
175
172
  """Add or modify roles for members of a restricted Environment.
176
173
 
@@ -117,13 +117,34 @@ def _validate_image_tag(tag: str) -> None:
117
117
  check_object_name(tag, "Image tag")
118
118
 
119
119
 
120
- def _parse_named_image_ref(name: str) -> str:
120
+ def _parse_named_image_ref(name: str) -> tuple[str, str]:
121
+ """Parse an image reference, returning (namespace_prefix, name_tag).
122
+
123
+ If the name contains a '/', the part before the last '/' is extracted as
124
+ a namespace prefix (intended for environment/name or workspace/env/name
125
+ syntax). The actual image name (after the last '/') is validated as a
126
+ standard object name.
127
+
128
+ Returns a tuple of (prefix, "full_name:tag") where prefix is empty string
129
+ if no '/' is present.
130
+ """
121
131
  image_name, sep, tag = name.partition(":")
122
132
  if not sep:
123
133
  tag = "latest"
134
+
135
+ prefix = ""
136
+ if "/" in image_name:
137
+ prefix, image_name = image_name.rsplit("/", 1)
138
+ if not prefix:
139
+ raise InvalidError("Invalid Image name: '/' prefix must be non-empty.")
140
+ if not image_name:
141
+ raise InvalidError("Invalid Image name: name after '/' must be non-empty.")
142
+
124
143
  _validate_image_name(image_name)
125
144
  _validate_image_tag(tag)
126
- return f"{image_name}:{tag}"
145
+
146
+ full_name = f"{prefix}/{image_name}" if prefix else image_name
147
+ return prefix, f"{full_name}:{tag}"
127
148
 
128
149
 
129
150
  def _validate_python_version(
@@ -2923,12 +2944,15 @@ class _Image(_Object, type_prefix="im"):
2923
2944
  ...
2924
2945
  ```
2925
2946
  """
2926
- tag = _parse_named_image_ref(name)
2947
+ namespace_prefix, tag = _parse_named_image_ref(name)
2948
+
2949
+ if namespace_prefix and environment_name:
2950
+ raise InvalidError("Cannot specify 'environment_name' when the image name contains a '/'.")
2927
2951
 
2928
2952
  async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
2929
2953
  req = api_pb2.ImageGetByTagRequest(
2930
2954
  tag=tag,
2931
- environment_name=load_context.environment_name,
2955
+ environment_name="" if namespace_prefix else load_context.environment_name,
2932
2956
  )
2933
2957
  response = await load_context.client.stub.ImageGetByTag(req)
2934
2958
  self._hydrate(response.image_id, load_context.client, None)
@@ -2963,15 +2987,20 @@ class _Image(_Object, type_prefix="im"):
2963
2987
  image.publish("my-image-with-numpy:v1")
2964
2988
  ```
2965
2989
  """
2966
- tag = _parse_named_image_ref(name)
2990
+ namespace_prefix, tag = _parse_named_image_ref(name)
2991
+
2992
+ if namespace_prefix:
2993
+ if environment_name:
2994
+ raise InvalidError("Cannot specify 'environment_name' when the image name contains a '/'.")
2995
+ resolved_env = ""
2996
+ else:
2997
+ resolved_env = environment_name or config.get("environment") or ""
2967
2998
 
2968
2999
  if self._object_id is None:
2969
3000
  raise InvalidError("Cannot publish an image that has not been created yet. Call `.build()` first.")
2970
3001
 
2971
3002
  _client = client or await _Client.from_env()
2972
3003
 
2973
- resolved_env = environment_name or config.get("environment") or ""
2974
-
2975
3004
  await _client.stub.ImagePublish(
2976
3005
  api_pb2.ImagePublishRequest(
2977
3006
  image_id=self._object_id,
@@ -13,13 +13,19 @@ from ._object import _Object
13
13
  from ._resolver import Resolver
14
14
  from ._utils.time_utils import timestamp_to_localized_dt
15
15
  from .client import _Client
16
- from .types import BillingReportItem, MemberRole, ProxyTokenInfo, TokenData, WorkspaceMemberInfo, WorkspaceSettings
16
+ from .types import (
17
+ BillingReportItem,
18
+ ProxyTokenInfo,
19
+ TokenData,
20
+ WorkspaceMemberInfo,
21
+ WorkspaceSettings,
22
+ )
17
23
 
18
24
 
19
- def _member_role_from_proto(proto_value: int) -> MemberRole:
25
+ def _member_role_from_proto(proto_value: int) -> str:
20
26
  match proto_value:
21
27
  case api_pb2.MEMBER_ROLE_USER:
22
- return "user"
28
+ return "member"
23
29
  case api_pb2.MEMBER_ROLE_MANAGER:
24
30
  return "manager"
25
31
  case api_pb2.MEMBER_ROLE_OWNER:
@@ -202,20 +202,20 @@ async def logs(
202
202
  task_info_resp = await client.stub.TaskGetInfo(api_pb2.TaskGetInfoRequest(task_id=task_id))
203
203
  app_id = task_info_resp.app_id
204
204
 
205
- if not task_info_resp.info.started_at:
206
- # Unlikely race or Modal backend issue, don't treat as a usage exception
205
+ if not task_info_resp.info.enqueued_at:
207
206
  return
208
- container_started_dt = datetime.fromtimestamp(task_info_resp.info.started_at, timezone.utc)
207
+
208
+ container_enqueued_dt = datetime.fromtimestamp(task_info_resp.info.enqueued_at, timezone.utc)
209
209
 
210
210
  now = datetime.now(timezone.utc)
211
211
  if all_logs:
212
- since_dt = container_started_dt
212
+ since_dt = container_enqueued_dt
213
213
  if task_info_resp.info.finished_at:
214
214
  until_dt = datetime.fromtimestamp(task_info_resp.info.finished_at, timezone.utc)
215
215
  else:
216
216
  until_dt = now
217
217
  else:
218
- since_dt = _parse_time_arg(since, default=container_started_dt)
218
+ since_dt = _parse_time_arg(since, default=container_enqueued_dt)
219
219
  if task_info_resp.info.finished_at:
220
220
  default_until_dt = datetime.fromtimestamp(task_info_resp.info.finished_at, timezone.utc)
221
221
  else:
@@ -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:
@@ -7,7 +7,7 @@ from click import UsageError
7
7
  from rich.text import Text
8
8
 
9
9
  from modal import environments
10
- from modal._environments import MemberRole, _Environment
10
+ from modal._environments import _Environment
11
11
  from modal._utils.async_utils import synchronizer
12
12
  from modal._utils.name_utils import check_environment_name
13
13
  from modal._utils.time_utils import format_interval
@@ -158,7 +158,7 @@ def members_list(environment: str, json: bool = False):
158
158
  "--role", type=click.Choice(["contributor", "viewer"]), required=True, help="Role to assign to the member"
159
159
  )
160
160
  @service_user_option
161
- def members_update(environment: str, member: str, role: MemberRole, service_user: bool = False):
161
+ def members_update(environment: str, member: str, role: str, service_user: bool = False):
162
162
  env = Environment.from_name(environment)
163
163
  if service_user:
164
164
  env.members.update(service_users={member: role})
@@ -168,11 +168,18 @@ async def history(
168
168
  env: Optional[str] = None,
169
169
  json: bool = False,
170
170
  ):
171
+ explicit_env = env # None when --env is not passed by the user
171
172
  env = ensure_env(env)
172
173
  try:
173
- name_tag = _parse_named_image_ref(name)
174
+ namespace_prefix, name_tag = _parse_named_image_ref(name)
174
175
  except InvalidError as exc:
175
176
  raise click.UsageError(str(exc)) from exc
177
+
178
+ if namespace_prefix:
179
+ if explicit_env is not None:
180
+ raise click.UsageError("Cannot specify '--env' when the image name contains a '/'.")
181
+ env = ""
182
+
176
183
  client = await _Client.from_env()
177
184
 
178
185
  first_page = await _fetch_history_page(client, env, name_tag, "")
@@ -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.dev19",
38
+ version: str = "1.5.2.dev21",
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.dev19",
208
+ version: str = "1.5.2.dev21",
209
209
  ):
210
210
  """mdmd:hidden
211
211
  The Modal client object is not intended to be instantiated directly by users.
@@ -112,9 +112,7 @@ class EnvironmentMembersManager:
112
112
  ...
113
113
 
114
114
  class __list_spec(typing_extensions.Protocol):
115
- def __call__(
116
- self, /
117
- ) -> dict[typing.Literal["users", "service_users"], dict[str, typing.Literal["viewer", "contributor"]]]:
115
+ def __call__(self, /) -> dict[typing.Literal["users", "service_users"], dict[str, str]]:
118
116
  """Return the members of a restricted Environment with their roles.
119
117
 
120
118
  **Examples:**
@@ -130,9 +128,7 @@ class EnvironmentMembersManager:
130
128
  """
131
129
  ...
132
130
 
133
- async def aio(
134
- self, /
135
- ) -> dict[typing.Literal["users", "service_users"], dict[str, typing.Literal["viewer", "contributor"]]]:
131
+ async def aio(self, /) -> dict[typing.Literal["users", "service_users"], dict[str, str]]:
136
132
  """Return the members of a restricted Environment with their roles.
137
133
 
138
134
  **Examples:**
@@ -155,10 +151,8 @@ class EnvironmentMembersManager:
155
151
  self,
156
152
  /,
157
153
  *,
158
- users: typing.Optional[collections.abc.Mapping[str, typing.Literal["viewer", "contributor"]]] = None,
159
- service_users: typing.Optional[
160
- collections.abc.Mapping[str, typing.Literal["viewer", "contributor"]]
161
- ] = None,
154
+ users: typing.Optional[collections.abc.Mapping[str, str]] = None,
155
+ service_users: typing.Optional[collections.abc.Mapping[str, str]] = None,
162
156
  ) -> None:
163
157
  """Add or modify roles for members of a restricted Environment.
164
158
 
@@ -181,10 +175,8 @@ class EnvironmentMembersManager:
181
175
  self,
182
176
  /,
183
177
  *,
184
- users: typing.Optional[collections.abc.Mapping[str, typing.Literal["viewer", "contributor"]]] = None,
185
- service_users: typing.Optional[
186
- collections.abc.Mapping[str, typing.Literal["viewer", "contributor"]]
187
- ] = None,
178
+ users: typing.Optional[collections.abc.Mapping[str, str]] = None,
179
+ service_users: typing.Optional[collections.abc.Mapping[str, str]] = None,
188
180
  ) -> None:
189
181
  """Add or modify roles for members of a restricted Environment.
190
182
 
@@ -491,7 +491,7 @@ class Function(
491
491
 
492
492
  _call_generator: ___call_generator_spec
493
493
 
494
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
494
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
495
495
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
496
496
  """Calls the function remotely, executing it with the given arguments and returning the execution's result.
497
497
 
@@ -516,7 +516,7 @@ class Function(
516
516
  """
517
517
  ...
518
518
 
519
- remote: __remote_spec[modal._functions.P, modal._functions.ReturnType]
519
+ remote: __remote_spec[modal._functions.ReturnType, modal._functions.P]
520
520
 
521
521
  class __remote_gen_spec(typing_extensions.Protocol):
522
522
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -566,7 +566,7 @@ class Function(
566
566
  """
567
567
  ...
568
568
 
569
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
569
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
570
570
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
571
571
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
572
572
 
@@ -599,7 +599,7 @@ class Function(
599
599
  """
600
600
  ...
601
601
 
602
- _experimental_spawn: ___experimental_spawn_spec[modal._functions.P, modal._functions.ReturnType]
602
+ _experimental_spawn: ___experimental_spawn_spec[modal._functions.ReturnType, modal._functions.P]
603
603
 
604
604
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER]):
605
605
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> None: ...
@@ -607,7 +607,7 @@ class Function(
607
607
 
608
608
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P]
609
609
 
610
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
610
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
611
611
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
612
612
  """Calls the function with the given arguments, without waiting for the results.
613
613
 
@@ -640,7 +640,7 @@ class Function(
640
640
  """
641
641
  ...
642
642
 
643
- spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType]
643
+ spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P]
644
644
 
645
645
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
646
646
  """Return the inner Python object wrapped by this Modal Function.
@@ -861,9 +861,10 @@ class _Sandbox(_Object, type_prefix="sb"):
861
861
  (e.g. `region="us-east-1"`).
862
862
 
863
863
  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.
864
+ `Sandbox.list()`. A named sandbox can be looked up with
865
+ `Sandbox._experimental_from_name(app_name, name)`; otherwise store
866
+ `sandbox.object_id` and use `Sandbox.from_id(sandbox.object_id)` to
867
+ reattach.
867
868
  """
868
869
  from .app import _App
869
870
 
@@ -1186,6 +1187,44 @@ class _Sandbox(_Object, type_prefix="sb"):
1186
1187
  resp = await client.stub.SandboxGetFromName(req)
1187
1188
  return _Sandbox._new_hydrated(resp.sandbox_id, client, None)
1188
1189
 
1190
+ @staticmethod
1191
+ async def _experimental_from_name(
1192
+ app_name: str,
1193
+ name: str,
1194
+ *,
1195
+ environment_name: str | None = None,
1196
+ client: _Client | None = None,
1197
+ ) -> "_Sandbox":
1198
+ """Get a running V2 Sandbox by name from a deployed App.
1199
+
1200
+ This looks up V2 sandboxes, ie sandboxes created via `modal.Sandbox._experimental_create`.
1201
+
1202
+ Args:
1203
+ app_name: Name of the deployed app to look up the sandbox under.
1204
+ name: Sandbox name to resolve.
1205
+ environment_name: Optional environment name for the lookup; defaults to the configured environment.
1206
+ client: Modal client to use for the RPC; defaults to `Client.from_env()` when omitted.
1207
+
1208
+ Returns:
1209
+ A `Sandbox` handle for the running sandbox.
1210
+
1211
+ Raises:
1212
+ NotFoundError: If no running sandbox exists with the given name.
1213
+ """
1214
+ if client is None:
1215
+ client = await _Client.from_env()
1216
+ env_name = _get_environment_name(environment_name)
1217
+
1218
+ req = api_pb2.SandboxGetFromNameRequest(sandbox_name=name, app_name=app_name, environment_name=env_name)
1219
+ assert client._auth_token_manager
1220
+ auth_token = await client._auth_token_manager.get_token()
1221
+ resp = await client.stub.SandboxGetFromNameV2(req, metadata=[("x-modal-auth-token", auth_token)])
1222
+
1223
+ obj = _Sandbox._new_hydrated(resp.sandbox_id, client, None)
1224
+ obj._is_v2 = True
1225
+ obj._hydrate_metadata_v2()
1226
+ return obj
1227
+
1189
1228
  @staticmethod
1190
1229
  async def from_id(sandbox_id: str, client: _Client | None = None) -> "_Sandbox":
1191
1230
  """Construct a Sandbox from an id and look up the Sandbox result.
@@ -2279,15 +2318,15 @@ class _Sandbox(_Object, type_prefix="sb"):
2279
2318
 
2280
2319
  @staticmethod
2281
2320
  async def _experimental_list(
2282
- *, app_id: str | None = None, client: _Client | None = None
2321
+ *, app_id: str | None = None, tags: dict[str, str] | None = None, client: _Client | None = None
2283
2322
  ) -> AsyncGenerator["_Sandbox", None]:
2284
2323
  """List v2 Sandboxes in an App.
2285
2324
 
2286
2325
  This function lists v2 sandboxes, ie sandboxes created via modal.Sandbox._experimental_create.
2287
- Filtering based on tags is not yet supported.
2288
2326
 
2289
2327
  Args:
2290
2328
  app_id: The App to list Sandboxes under.
2329
+ tags: If set, only sandboxes containing at least these tags are returned.
2291
2330
  client: Optional client to use for the session.
2292
2331
 
2293
2332
  Yields:
@@ -2304,12 +2343,15 @@ class _Sandbox(_Object, type_prefix="sb"):
2304
2343
  if client is None:
2305
2344
  client = await _Client.from_env()
2306
2345
 
2346
+ tags_list = [api_pb2.SandboxTag(tag_name=name, tag_value=value) for name, value in tags.items()] if tags else []
2347
+
2307
2348
  assert client._auth_token_manager
2308
2349
  while True:
2309
2350
  req = api_pb2.SandboxListRequest(
2310
2351
  app_id=app_id,
2311
2352
  before_timestamp=before_timestamp,
2312
2353
  include_finished=False,
2354
+ tags=tags_list,
2313
2355
  )
2314
2356
 
2315
2357
  # Fetches a batch of sandboxes. SandboxListV2 authenticates via the