modAL 1.6.3.dev8__tar.gz → 1.6.3.dev9__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 (248) hide show
  1. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/PKG-INFO +1 -1
  2. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_environments.py +5 -1
  3. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_image.py +65 -23
  4. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/docker_utils.py +27 -0
  5. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_workspace.py +65 -1
  6. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/app.py +39 -19
  7. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/app.pyi +59 -0
  8. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/builder/PREVIEW.txt +0 -1
  9. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/environment.py +21 -5
  10. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/workspace.py +100 -0
  11. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/client.pyi +2 -2
  12. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/environments.pyi +2 -0
  13. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/experimental/__init__.py +6 -5
  14. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/functions.pyi +6 -6
  15. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/mount.py +0 -1
  16. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/types.py +23 -0
  17. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/workspace.py +3 -0
  18. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/workspace.pyi +93 -0
  19. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal.egg-info/PKG-INFO +1 -1
  20. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/api_grpc.py +64 -0
  21. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/api_pb2.py +885 -843
  22. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/api_pb2.pyi +96 -0
  23. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/api_pb2_grpc.py +133 -1
  24. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/api_pb2_grpc.pyi +43 -3
  25. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/modal_api_grpc.py +4 -0
  26. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_version/__init__.py +1 -1
  27. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/LICENSE +0 -0
  28. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/README.md +0 -0
  29. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/__init__.py +0 -0
  30. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/__main__.py +0 -0
  31. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_billing.py +0 -0
  32. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_cluster.py +0 -0
  33. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_cluster.pyi +0 -0
  34. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_container_entrypoint.py +0 -0
  35. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_function_variants.py +0 -0
  36. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_functions.py +0 -0
  37. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_grpc_client.py +0 -0
  38. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_ipython.py +0 -0
  39. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_load_context.py +0 -0
  40. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_location.py +0 -0
  41. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_logs.py +0 -0
  42. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_logs_manager.py +0 -0
  43. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_logs_manager.pyi +0 -0
  44. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_metrics.py +0 -0
  45. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_metrics.pyi +0 -0
  46. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_object.py +0 -0
  47. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_outbound_policy.py +0 -0
  48. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_outbound_policy.pyi +0 -0
  49. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_output/__init__.py +0 -0
  50. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_output/manager.py +0 -0
  51. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_output/pty.py +0 -0
  52. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_output/rich.py +0 -0
  53. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_output/status.py +0 -0
  54. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_partial_function.py +0 -0
  55. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_resolver.py +0 -0
  56. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_resources.py +0 -0
  57. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/__init__.py +0 -0
  58. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/asgi.py +0 -0
  59. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/container_io_manager.py +0 -0
  60. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/container_io_manager.pyi +0 -0
  61. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/execution_context.py +0 -0
  62. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/execution_context.pyi +0 -0
  63. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  64. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/task_lifecycle_manager.py +0 -0
  65. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  66. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/telemetry.py +0 -0
  67. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/user_code_event_loop.py +0 -0
  68. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_runtime/user_code_imports.py +0 -0
  69. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_serialization.py +0 -0
  70. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_server.py +0 -0
  71. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_server.pyi +0 -0
  72. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_supports_logs.py +0 -0
  73. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_traceback.py +0 -0
  74. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_tunnel.py +0 -0
  75. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_tunnel.pyi +0 -0
  76. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_type_manager.py +0 -0
  77. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/__init__.py +0 -0
  78. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/app_utils.py +0 -0
  79. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/async_utils.py +0 -0
  80. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/auth_token_manager.py +0 -0
  81. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/blob_utils.py +0 -0
  82. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/browser_utils.py +0 -0
  83. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/bytes_io_segment_payload.py +0 -0
  84. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/curl_utils.py +0 -0
  85. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/deprecation.py +0 -0
  86. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/display_utils.py +0 -0
  87. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/function_utils.py +0 -0
  88. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/git_utils.py +0 -0
  89. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/grpc_testing.py +0 -0
  90. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/grpc_utils.py +0 -0
  91. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/hash_utils.py +0 -0
  92. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/http_utils.py +0 -0
  93. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/idle_countdown.py +0 -0
  94. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/jwt_utils.py +0 -0
  95. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/logger.py +0 -0
  96. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/mount_utils.py +0 -0
  97. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/name_utils.py +0 -0
  98. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/package_utils.py +0 -0
  99. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/pattern_utils.py +0 -0
  100. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/proxy_support.py +0 -0
  101. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/rand_pb_testing.py +0 -0
  102. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/sandbox_fs_utils.py +0 -0
  103. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/shell_utils.py +0 -0
  104. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_utils/time_utils.py +0 -0
  105. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_vendor/__init__.py +0 -0
  106. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  107. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_vendor/cloudpickle.py +0 -0
  108. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_vendor/tblib.py +0 -0
  109. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_vendor/version.py +0 -0
  110. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/_watcher.py +0 -0
  111. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/billing.py +0 -0
  112. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/billing.pyi +0 -0
  113. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/builder/2023.12.312.txt +0 -0
  114. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/builder/2023.12.txt +0 -0
  115. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/builder/2024.04.txt +0 -0
  116. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/builder/2024.10.txt +0 -0
  117. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/builder/2025.06.txt +0 -0
  118. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/builder/README.md +0 -0
  119. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/builder/base-images.json +0 -0
  120. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/call_graph.py +0 -0
  121. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/__init__.py +0 -0
  122. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/_download.py +0 -0
  123. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/_help.py +0 -0
  124. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/_logs.py +0 -0
  125. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/_stats.py +0 -0
  126. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/_traceback.py +0 -0
  127. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/app.py +0 -0
  128. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/billing.py +0 -0
  129. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/changelog.py +0 -0
  130. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/config.py +0 -0
  131. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/container.py +0 -0
  132. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/curl.py +0 -0
  133. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/dashboard.py +0 -0
  134. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/dict.py +0 -0
  135. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/endpoint.py +0 -0
  136. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/entry_point.py +0 -0
  137. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/function.py +0 -0
  138. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/image.py +0 -0
  139. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/import_refs.py +0 -0
  140. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/logo.py +0 -0
  141. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/network_file_system.py +0 -0
  142. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/profile.py +0 -0
  143. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/queues.py +0 -0
  144. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/run.py +0 -0
  145. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/secret.py +0 -0
  146. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/selector.py +0 -0
  147. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/server.py +0 -0
  148. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/shell.py +0 -0
  149. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/skills.py +0 -0
  150. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/token.py +0 -0
  151. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/utils.py +0 -0
  152. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cli/volume.py +0 -0
  153. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/client.py +0 -0
  154. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cloud_bucket_mount.py +0 -0
  155. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cloud_bucket_mount.pyi +0 -0
  156. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cls.py +0 -0
  157. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cls.pyi +0 -0
  158. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cluster.py +0 -0
  159. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/cluster.pyi +0 -0
  160. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/config.py +0 -0
  161. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/container_process.py +0 -0
  162. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/container_process.pyi +0 -0
  163. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/dict.py +0 -0
  164. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/dict.pyi +0 -0
  165. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/environments.py +0 -0
  166. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/exception.py +0 -0
  167. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/experimental/_flash_container_entry.py +0 -0
  168. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/experimental/flash.py +0 -0
  169. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/experimental/flash.pyi +0 -0
  170. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/experimental/flash_server.py +0 -0
  171. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/experimental/flash_server.pyi +0 -0
  172. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/experimental/ipython.py +0 -0
  173. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/file_pattern_matcher.py +0 -0
  174. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/functions.py +0 -0
  175. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/image.py +0 -0
  176. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/image.pyi +0 -0
  177. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/io_streams.py +0 -0
  178. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/io_streams.pyi +0 -0
  179. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/mount.pyi +0 -0
  180. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/network_file_system.py +0 -0
  181. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/network_file_system.pyi +0 -0
  182. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/object.py +0 -0
  183. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/object.pyi +0 -0
  184. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/output.py +0 -0
  185. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/parallel_map.py +0 -0
  186. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/parallel_map.pyi +0 -0
  187. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/partial_function.py +0 -0
  188. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/partial_function.pyi +0 -0
  189. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/proxy.py +0 -0
  190. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/proxy.pyi +0 -0
  191. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/py.typed +0 -0
  192. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/queue.py +0 -0
  193. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/queue.pyi +0 -0
  194. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/retries.py +0 -0
  195. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/runner.py +0 -0
  196. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/runner.pyi +0 -0
  197. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/running_app.py +0 -0
  198. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/__init__.py +0 -0
  199. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/_common.py +0 -0
  200. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/_filesystem.py +0 -0
  201. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/_impl_persistent.py +0 -0
  202. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/_impl_v1.py +0 -0
  203. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/_impl_v2.py +0 -0
  204. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/_sandbox.py +0 -0
  205. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/_sidecar.py +0 -0
  206. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/_snapshot.py +0 -0
  207. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/_task_command_router_client.py +0 -0
  208. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/sandbox.py +0 -0
  209. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/sandbox/sandbox.pyi +0 -0
  210. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/schedule.py +0 -0
  211. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/scheduler_placement.py +0 -0
  212. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/secret.py +0 -0
  213. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/secret.pyi +0 -0
  214. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/server.py +0 -0
  215. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/server.pyi +0 -0
  216. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/serving.py +0 -0
  217. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/serving.pyi +0 -0
  218. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/skills/modal/SKILL.md +0 -0
  219. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/stream_type.py +0 -0
  220. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/token_flow.py +0 -0
  221. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/token_flow.pyi +0 -0
  222. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/volume.py +0 -0
  223. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal/volume.pyi +0 -0
  224. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal.egg-info/SOURCES.txt +0 -0
  225. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal.egg-info/dependency_links.txt +0 -0
  226. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal.egg-info/entry_points.txt +0 -0
  227. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal.egg-info/requires.txt +0 -0
  228. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal.egg-info/top_level.txt +0 -0
  229. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_docs/__init__.py +0 -0
  230. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_docs/gen_cli_docs.py +0 -0
  231. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_docs/gen_cli_docs_main.py +0 -0
  232. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_docs/gen_reference_docs.py +0 -0
  233. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_docs/gen_reference_docs_main.py +0 -0
  234. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_docs/mdmd/__init__.py +0 -0
  235. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_docs/mdmd/mdmd.py +0 -0
  236. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_docs/mdmd/signatures.py +0 -0
  237. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_docs/mdmd/types.py +0 -0
  238. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_docs/reference_intro.md +0 -0
  239. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/__init__.py +0 -0
  240. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/py.typed +0 -0
  241. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/task_command_router_grpc.py +0 -0
  242. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/task_command_router_pb2.py +0 -0
  243. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/task_command_router_pb2.pyi +0 -0
  244. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  245. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  246. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/modal_version/__main__.py +0 -0
  247. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/pyproject.toml +0 -0
  248. {modal-1.6.3.dev8 → modal-1.6.3.dev9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.6.3.dev8
3
+ Version: 1.6.3.dev9
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -648,6 +648,7 @@ async def _update_environment(
648
648
  *,
649
649
  new_name: str | None = None,
650
650
  new_web_suffix: str | None = None,
651
+ new_image_builder_version: str | None = None,
651
652
  client: _Client | None = None,
652
653
  ):
653
654
  new_name_pb2 = None
@@ -662,7 +663,10 @@ async def _update_environment(
662
663
  new_web_suffix_pb2 = StringValue(value=new_web_suffix)
663
664
 
664
665
  update_payload = api_pb2.EnvironmentUpdateRequest(
665
- current_name=current_name, name=new_name_pb2, web_suffix=new_web_suffix_pb2
666
+ current_name=current_name,
667
+ name=new_name_pb2,
668
+ web_suffix=new_web_suffix_pb2,
669
+ image_builder_version=new_image_builder_version,
666
670
  )
667
671
  if client is None:
668
672
  client = await _Client.from_env()
@@ -40,6 +40,7 @@ from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
40
40
  from ._utils.docker_utils import (
41
41
  extract_copy_command_patterns,
42
42
  find_dockerignore_file,
43
+ splittable_from_line_index,
43
44
  )
44
45
  from ._utils.function_utils import FunctionSourceInfo, parse_gpu_config
45
46
  from ._utils.mount_utils import validate_only_modal_volumes, validate_volumes_by_object_id
@@ -77,7 +78,7 @@ LATEST_IMAGE_BUILDER_VERSION: ImageBuilderVersion = "2025.06"
77
78
  # Python versions in mount.py where we specify the "standalone Python versions" we create mounts for.
78
79
  # Consider consolidating these multiple sources of truth?
79
80
  SUPPORTED_PYTHON_SERIES: dict[ImageBuilderVersion, list[str]] = {
80
- "PREVIEW": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
81
+ "PREVIEW": ["3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
81
82
  "2025.06": ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"],
82
83
  "2024.10": ["3.10", "3.11", "3.12", "3.13"],
83
84
  "2024.04": ["3.10", "3.11", "3.12"],
@@ -174,6 +175,7 @@ def _validate_python_version(
174
175
  allow_free_threading: bool = False,
175
176
  caller_name: str = "",
176
177
  ) -> str:
178
+ python_version_unspecified = python_version is None
177
179
  if python_version is None:
178
180
  # If Python version is unspecified, match the local version, up to the minor component
179
181
  python_version = series_version = "{}.{}".format(*sys.version_info)
@@ -196,6 +198,12 @@ def _validate_python_version(
196
198
 
197
199
  supported_series = SUPPORTED_PYTHON_SERIES[builder_version]
198
200
  if series_version not in supported_series:
201
+ if python_version_unspecified:
202
+ raise InvalidError(
203
+ f"Python {series_version} is not supported by the {builder_version!r} Image builder."
204
+ f" Since `python_version` was not set, Modal tried to match your local Python ({series_version})."
205
+ f" Set `python_version` to one of the supported series: {supported_series!r}."
206
+ )
199
207
  raise InvalidError(
200
208
  f"Unsupported Python version: {python_version!r}."
201
209
  f" When using the {builder_version!r} Image builder, Modal supports the following series:"
@@ -2538,25 +2546,22 @@ class _Image(_Object, type_prefix="im"):
2538
2546
  if env:
2539
2547
  secrets = [*secrets, _Secret.from_dict(env)]
2540
2548
 
2541
- # --- Build the base dockerfile
2542
-
2543
- def build_dockerfile_base(version: ImageBuilderVersion) -> DockerfileSpec:
2544
- with open(os.path.expanduser(path)) as f:
2545
- commands = f.read().split("\n")
2546
- return DockerfileSpec(commands=commands, context_files={})
2547
-
2548
2549
  gpu_config = parse_gpu_config(gpu)
2549
- base_image = _Image._from_args(
2550
- dockerfile_function=build_dockerfile_base,
2551
- context_mount_function=_create_context_mount_function(
2552
- ignore=ignore, dockerfile_path=Path(path), context_dir=context_dir
2553
- ),
2554
- gpu_config=gpu_config,
2555
- secrets=secrets,
2556
- build_args=build_args,
2557
- force_build=force_build,
2550
+ context_mount_function = _create_context_mount_function(
2551
+ ignore=ignore, dockerfile_path=Path(path), context_dir=context_dir
2558
2552
  )
2559
2553
 
2554
+ def dockerfile_image(commands: list[str], base_images: dict[str, _Image] | None = None) -> _Image:
2555
+ return _Image._from_args(
2556
+ base_images=base_images,
2557
+ dockerfile_function=lambda version: DockerfileSpec(commands=commands, context_files={}),
2558
+ context_mount_function=context_mount_function,
2559
+ gpu_config=gpu_config,
2560
+ secrets=secrets,
2561
+ build_args=build_args,
2562
+ force_build=force_build,
2563
+ )
2564
+
2560
2565
  # --- Now add in the modal dependencies, and, optionally a Python distribution
2561
2566
  # This happening in two steps is probably a vestigial consequence of previous limitations,
2562
2567
  # but it will be difficult to merge them without forcing rebuilds of images.
@@ -2579,12 +2584,49 @@ class _Image(_Object, type_prefix="im"):
2579
2584
  context_files = {CONTAINER_REQUIREMENTS_PATH: requirements_path}
2580
2585
  return DockerfileSpec(commands=commands, context_files=context_files)
2581
2586
 
2582
- return _Image._from_args(
2583
- base_images={"base": base_image},
2584
- dockerfile_function=build_dockerfile_python,
2585
- context_mount_function=add_python_mount,
2586
- force_build=force_build,
2587
- )
2587
+ def python_image(base_image: _Image) -> _Image:
2588
+ return _Image._from_args(
2589
+ base_images={"base": base_image},
2590
+ dockerfile_function=build_dockerfile_python,
2591
+ context_mount_function=add_python_mount,
2592
+ force_build=force_build,
2593
+ )
2594
+
2595
+ async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
2596
+ builder_version = await _resolve_image_builder_version(load_context)
2597
+ if add_python:
2598
+ # Validate before any Dockerfile layer is built.
2599
+ _validate_python_version(
2600
+ add_python, builder_version, allow_micro_granularity=False, allow_free_threading=True
2601
+ )
2602
+ with open(os.path.expanduser(path)) as f:
2603
+ dockerfile_lines = f.read().split("\n")
2604
+
2605
+ # Builder versions after 2025.06 put the FROM line of a single-stage Dockerfile in its own image.
2606
+ # Dockerfiles the split could change (see splittable_from_line_index) stay in one layer.
2607
+ from_index = splittable_from_line_index(dockerfile_lines) if builder_version > "2025.06" else None
2608
+ if from_index is None:
2609
+ image = python_image(dockerfile_image(dockerfile_lines))
2610
+ else:
2611
+ from_lines = dockerfile_lines[: from_index + 1]
2612
+ from_image = _Image._from_args(
2613
+ dockerfile_function=lambda version: DockerfileSpec(commands=from_lines, context_files={}),
2614
+ force_build=force_build,
2615
+ )
2616
+ rest = dockerfile_lines[from_index + 1 :]
2617
+ rest_is_empty = all(not line.strip() or line.strip().startswith("#") for line in rest)
2618
+ if rest_is_empty:
2619
+ image = python_image(from_image)
2620
+ else:
2621
+ image = python_image(dockerfile_image(["FROM base", *rest], base_images={"base": from_image}))
2622
+ await resolver.load(image, load_context)
2623
+ self._hydrate_from_other(image)
2624
+ self._serve_mounts = image._serve_mounts
2625
+
2626
+ rep = f"Image.from_dockerfile({str(path)!r})"
2627
+ obj = _Image._from_loader(_load, rep, load_context_overrides=LoadContext.empty())
2628
+ obj.force_build = force_build
2629
+ return obj
2588
2630
 
2589
2631
  @staticmethod
2590
2632
  def from_scratch(force_build: bool = False) -> "_Image":
@@ -74,6 +74,33 @@ def extract_copy_command_patterns(dockerfile_lines: Sequence[str]) -> list[str]:
74
74
  return list(copy_source_patterns)
75
75
 
76
76
 
77
+ _FROM_PATTERN = re.compile(r"^\s*FROM\s", re.IGNORECASE)
78
+ _FROM_ALIAS_PATTERN = re.compile(r"\sAS\s+\S+\s*$", re.IGNORECASE)
79
+ _PARSER_DIRECTIVE_PATTERN = re.compile(r"^#\s*[a-zA-Z][a-zA-Z0-9]*\s*=")
80
+
81
+
82
+ def splittable_from_line_index(dockerfile_lines: Sequence[str]) -> int | None:
83
+ """Return the line index of the FROM instruction if it can be built as its own image.
84
+
85
+ Returns None unless the first instruction is a one-line FROM without a stage alias (no global ARGs), no
86
+ parser directives precede it, and no other line starts with FROM. Lines inside heredocs or continuations
87
+ count too, so a multi-stage Dockerfile is never split.
88
+ """
89
+ start = next(
90
+ (i for i, line in enumerate(dockerfile_lines) if line.strip() and not line.strip().startswith("#")),
91
+ None,
92
+ )
93
+ if start is None or not _FROM_PATTERN.match(dockerfile_lines[start]):
94
+ return None
95
+ if dockerfile_lines[start].rstrip().endswith("\\") or _FROM_ALIAS_PATTERN.search(dockerfile_lines[start]):
96
+ return None
97
+ if any(_PARSER_DIRECTIVE_PATTERN.match(line.strip()) for line in dockerfile_lines[:start]):
98
+ return None
99
+ if any(_FROM_PATTERN.match(line) for line in dockerfile_lines[start + 1 :]):
100
+ return None
101
+ return start
102
+
103
+
77
104
  def find_dockerignore_file(context_directory: Path, dockerfile_path: Path | None = None) -> Path | None:
78
105
  """
79
106
  Find dockerignore file relative to current context directory
@@ -9,7 +9,7 @@ from typing import Optional
9
9
  from google.protobuf.empty_pb2 import Empty
10
10
 
11
11
  from modal.exception import DeprecationError, InvalidError, NotFoundError
12
- from modal.types import ServiceUserInfo
12
+ from modal.types import MemberTokenInfo, ServiceUserInfo
13
13
  from modal_proto import api_pb2
14
14
 
15
15
  from ._load_context import LoadContext
@@ -139,6 +139,11 @@ class _Workspace(_Object, type_prefix="ac"):
139
139
  """
140
140
  return _WorkspaceServiceUserManager(self)
141
141
 
142
+ @property
143
+ def member_tokens(self) -> "_WorkspaceMemberTokenManager":
144
+ """Namespace for managing Member API Tokens in a Workspace."""
145
+ return _WorkspaceMemberTokenManager(self)
146
+
142
147
 
143
148
  class _WorkspaceProxyTokenManager:
144
149
  """mdmd:namespace"""
@@ -735,3 +740,62 @@ class _WorkspaceServiceUserManager:
735
740
  except NotFoundError:
736
741
  if not allow_missing:
737
742
  raise
743
+
744
+
745
+ class _WorkspaceMemberTokenManager:
746
+ """mdmd:namespace"""
747
+
748
+ def __init__(self, workspace: _Workspace):
749
+ self._workspace = workspace
750
+
751
+ async def list(self) -> list[MemberTokenInfo]:
752
+ """List Member Tokens in the specified Workspace.
753
+
754
+ If you are a Manager or an Owner of this Workspace, this method will return a list of all the
755
+ Member Tokens in the Workspace. Otherwise, this method will only return the Tokens you have
756
+ created.
757
+
758
+ Returns:
759
+ A list of `MemberTokenInfo` dataclasses.
760
+
761
+ Examples:
762
+
763
+ ```python notest
764
+ ws = modal.Workspace.from_context()
765
+ for member_token in ws.member_tokens.list():
766
+ print(member_token)
767
+ ```
768
+ """
769
+ if not self._workspace._is_hydrated:
770
+ await self._workspace.hydrate()
771
+
772
+ resp = await self._workspace.client._stub.TokensList(Empty())
773
+
774
+ return [MemberTokenInfo._from_proto(token) for token in resp.tokens]
775
+
776
+ async def delete(self, token_id: str, *, allow_missing: bool = False) -> None:
777
+ """Delete a Member Token in the specified Workspace.
778
+
779
+ If you are a Workspace Manager or Owner, you can use this method to delete any Token in the
780
+ Workspace. Otherwise, you can only delete Member Tokens that you have created.
781
+
782
+ Args:
783
+ token_id: The ID of the Member Token you would like to delete.
784
+ allow_missing: If `True`, do not throw an exception if the Member Token does not exist.
785
+
786
+ Examples:
787
+
788
+ ```python notest
789
+ ws = modal.Workspace.from_context()
790
+ ws.member_tokens.delete(token_id="ak-0123456789abcdefghijkl")
791
+ ```
792
+ """
793
+
794
+ if not self._workspace._is_hydrated:
795
+ await self._workspace.hydrate()
796
+
797
+ try:
798
+ await self._workspace.client._stub.TokenDelete(req=api_pb2.TokenDeleteRequest(token_id=token_id))
799
+ except NotFoundError:
800
+ if not allow_missing:
801
+ raise
@@ -178,7 +178,7 @@ class _App:
178
178
  # Running apps only (container apps or running local)
179
179
  _app_id: str | None # Kept after app finishes
180
180
  _running_app: RunningApp | None # Various app info
181
- _client: _Client | None
181
+ _client: _Client | None # Kept after app finishes
182
182
 
183
183
  # Metadata for loading objects within this app
184
184
  # passed by reference to functions and classes so it can be updated by run()/deploy()
@@ -400,9 +400,13 @@ class _App:
400
400
  print(app.get_dashboard_url())
401
401
  ```
402
402
  """
403
+ app_id = self._ensure_app_id("get_dashboard_url")
404
+ return f"https://modal.com/id/{app_id}"
405
+
406
+ def _ensure_app_id(self, method: str) -> str:
403
407
  if self._app_id is None:
404
- raise InvalidError("App is not running")
405
- return f"https://modal.com/id/{self._app_id}"
408
+ raise InvalidError(f"`App.{method}` requires the App to have an ID.")
409
+ return self._app_id
406
410
 
407
411
  def _validate_blueprint_value(self, key: str, value: Any):
408
412
  if not isinstance(value, _Object):
@@ -425,7 +429,6 @@ class _App:
425
429
  yield
426
430
  finally:
427
431
  self._running_app = None
428
- self._client = None
429
432
  self._uncreate_all_objects()
430
433
 
431
434
  @asynccontextmanager
@@ -582,6 +585,7 @@ class _App:
582
585
  self, name=name, environment_name=environment_name, tag=tag, client=client, deployment_strategy=strategy
583
586
  )
584
587
  self._app_id = result.app_id
588
+ self._client = self._root_load_context.client
585
589
  return self
586
590
 
587
591
  def _get_default_image(self):
@@ -1596,10 +1600,9 @@ class _App:
1596
1600
  # when the App is running, but lazy (and potentially ignored) otherwise. There would be
1597
1601
  # other complications, like what do you do with any tags set in the constructor, and
1598
1602
  # what should `.get_tags()` do when it's called before the App is running?
1599
- if self._app_id is None:
1600
- raise InvalidError("`App.set_tags` cannot be called before the App is running.")
1603
+ app_id = self._ensure_app_id("set_tags")
1601
1604
  check_tag_dict(tags)
1602
- req = api_pb2.AppSetTagsRequest(app_id=self._app_id, tags=tags)
1605
+ req = api_pb2.AppSetTagsRequest(app_id=app_id, tags=tags)
1603
1606
 
1604
1607
  client = client or self._client or await _Client.from_env()
1605
1608
  await client._stub.AppSetTags(req)
@@ -1613,13 +1616,33 @@ class _App:
1613
1616
  Returns:
1614
1617
  Tags as a map from key to value.
1615
1618
  """
1616
- if self._app_id is None:
1617
- raise InvalidError("`App.get_tags` cannot be called before the App is running.")
1618
- req = api_pb2.AppGetTagsRequest(app_id=self._app_id)
1619
+ app_id = self._ensure_app_id("get_tags")
1620
+ req = api_pb2.AppGetTagsRequest(app_id=app_id)
1619
1621
  client = client or self._client or await _Client.from_env()
1620
1622
  resp = await client._stub.AppGetTags(req)
1621
1623
  return dict(resp.tags)
1622
1624
 
1625
+ async def stop(self, *, client: _Client | None = None) -> None:
1626
+ """Stop the App.
1627
+
1628
+ Stopping an App terminates all of its running containers and sandboxes and prevents
1629
+ new inputs from being processed.
1630
+
1631
+ **Examples:**
1632
+
1633
+ ```python notest
1634
+ app = modal.App.lookup("my-app")
1635
+ app.stop()
1636
+ ```
1637
+
1638
+ Args:
1639
+ client: Modal client to use for the RPC.
1640
+ """
1641
+ app_id = self._ensure_app_id("stop")
1642
+ req = api_pb2.AppStopRequest(app_id=app_id, source=api_pb2.APP_STOP_SOURCE_PYTHON_CLIENT)
1643
+ client = client or self._client or await _Client.from_env()
1644
+ await client._stub.AppStop(req)
1645
+
1623
1646
  @classmethod
1624
1647
  def _get_container_app(cls) -> "_App | None":
1625
1648
  """Returns the `App` running inside a container.
@@ -1634,11 +1657,10 @@ class _App:
1634
1657
 
1635
1658
  async def _get_log_query_data(self) -> _LogQueryData:
1636
1659
  """Get the data needed to query logs for this app."""
1637
- if not self._app_id:
1638
- raise InvalidError("`app.logs` requires a running/stopped app.")
1660
+ app_id = self._ensure_app_id("logs")
1639
1661
  client = self._client or await _Client.from_env()
1640
1662
  filters = LogsFilters()
1641
- return _LogQueryData(client=client, app_id=self._app_id, filters=filters, source_object_id=self._app_id)
1663
+ return _LogQueryData(client=client, app_id=app_id, filters=filters, source_object_id=app_id)
1642
1664
 
1643
1665
  @property
1644
1666
  def logs(self) -> _AppLogsManager:
@@ -1653,8 +1675,7 @@ class _App:
1653
1675
  - [`modal app logs`](https://modal.com/docs/cli/latest/app#modal-app-logs):
1654
1676
  CLI access to logs for an App.
1655
1677
  """
1656
- if not self._app_id:
1657
- raise InvalidError("`app.logs` requires a running/stopped app.")
1678
+ self._ensure_app_id("logs")
1658
1679
 
1659
1680
  return _AppLogsManager(self)
1660
1681
 
@@ -1682,12 +1703,11 @@ class _App:
1682
1703
  servers=self._info.servers,
1683
1704
  lifecycle=self._info.lifecycle,
1684
1705
  )
1685
- if not self._app_id:
1686
- raise InvalidError("`app.info` requires a running or stopped app.")
1687
- request = api_pb2.AppGetInfoRequest(app_id=self._app_id)
1706
+ app_id = self._ensure_app_id("info")
1707
+ request = api_pb2.AppGetInfoRequest(app_id=app_id)
1688
1708
  client = self._client or await _Client.from_env()
1689
1709
  resp = await client._stub.AppGetInfo(request)
1690
- self._info = AppInfo._from_proto(resp.info, self._app_id)
1710
+ self._info = AppInfo._from_proto(resp.info, app_id)
1691
1711
  return self._info
1692
1712
 
1693
1713
 
@@ -298,6 +298,7 @@ class _App:
298
298
  """
299
299
  ...
300
300
 
301
+ def _ensure_app_id(self, method: str) -> str: ...
301
302
  def _validate_blueprint_value(self, key: str, value: typing.Any): ...
302
303
  def _uncreate_all_objects(self): ...
303
304
  def _set_local_app(
@@ -902,6 +903,24 @@ class _App:
902
903
  """
903
904
  ...
904
905
 
906
+ async def stop(self, *, client: typing.Optional[modal.client._Client] = None) -> None:
907
+ """Stop the App.
908
+
909
+ Stopping an App terminates all of its running containers and sandboxes and prevents
910
+ new inputs from being processed.
911
+
912
+ **Examples:**
913
+
914
+ ```python notest
915
+ app = modal.App.lookup("my-app")
916
+ app.stop()
917
+ ```
918
+
919
+ Args:
920
+ client: Modal client to use for the RPC.
921
+ """
922
+ ...
923
+
905
924
  @classmethod
906
925
  def _get_container_app(cls) -> typing.Optional[_App]:
907
926
  """Returns the `App` running inside a container.
@@ -1187,6 +1206,7 @@ class App:
1187
1206
 
1188
1207
  get_dashboard_url: __get_dashboard_url_spec
1189
1208
 
1209
+ def _ensure_app_id(self, method: str) -> str: ...
1190
1210
  def _validate_blueprint_value(self, key: str, value: typing.Any): ...
1191
1211
  def _uncreate_all_objects(self): ...
1192
1212
 
@@ -1975,6 +1995,45 @@ class App:
1975
1995
 
1976
1996
  get_tags: __get_tags_spec
1977
1997
 
1998
+ class __stop_spec(typing_extensions.Protocol):
1999
+ def __call__(self, /, *, client: typing.Optional[modal.client.Client] = None) -> None:
2000
+ """Stop the App.
2001
+
2002
+ Stopping an App terminates all of its running containers and sandboxes and prevents
2003
+ new inputs from being processed.
2004
+
2005
+ **Examples:**
2006
+
2007
+ ```python notest
2008
+ app = modal.App.lookup("my-app")
2009
+ app.stop()
2010
+ ```
2011
+
2012
+ Args:
2013
+ client: Modal client to use for the RPC.
2014
+ """
2015
+ ...
2016
+
2017
+ async def aio(self, /, *, client: typing.Optional[modal.client.Client] = None) -> None:
2018
+ """Stop the App.
2019
+
2020
+ Stopping an App terminates all of its running containers and sandboxes and prevents
2021
+ new inputs from being processed.
2022
+
2023
+ **Examples:**
2024
+
2025
+ ```python notest
2026
+ app = modal.App.lookup("my-app")
2027
+ app.stop()
2028
+ ```
2029
+
2030
+ Args:
2031
+ client: Modal client to use for the RPC.
2032
+ """
2033
+ ...
2034
+
2035
+ stop: __stop_spec
2036
+
1978
2037
  @classmethod
1979
2038
  def _get_container_app(cls) -> typing.Optional[App]:
1980
2039
  """Returns the `App` running inside a container.
@@ -1,7 +1,6 @@
1
1
  aiohappyeyeballs==2.7.1
2
2
  aiohttp==3.14.5
3
3
  aiosignal==1.4.0
4
- async-timeout==5.0.1 ; python_version < "3.11"
5
4
  attrs==26.1.0
6
5
  cbor2==5.9.0
7
6
  certifi==2026.10.19
@@ -64,9 +64,11 @@ def list_(json: bool = False):
64
64
  for item in envs:
65
65
  is_active = item.name == active_env
66
66
  is_active_display: Text | str = str(is_active) if json else RenderableBool(is_active)
67
- row = [item.name, item.webhook_suffix, is_active_display]
67
+ # Unset when the Environment inherits the Workspace's image builder version
68
+ image_builder_version = item.image_builder_version if item.HasField("image_builder_version") else None
69
+ row = [item.name, item.webhook_suffix, image_builder_version, is_active_display]
68
70
  table_data.append(row)
69
- display_table(["name", "web suffix", "active"], table_data, json=json)
71
+ display_table(["name", "web suffix", "image builder version", "active"], table_data, json=json)
70
72
 
71
73
 
72
74
  @environment_cli.command(
@@ -108,18 +110,32 @@ Deletes all Apps in the selected Environment and deletes the Environment irrevoc
108
110
  @click.argument("current_name")
109
111
  @click.option("--set-name", default=None, help="New name of the Environment.")
110
112
  @click.option("--set-web-suffix", default=None, help="New web suffix of the Environment (empty string is no suffix).")
113
+ @click.option(
114
+ "--set-image-builder-version",
115
+ default=None,
116
+ help="New image builder version of the Environment (empty string uses the Workspace default).",
117
+ )
111
118
  def update(
112
119
  current_name: str,
113
120
  set_name: str | None = None,
114
121
  set_web_suffix: str | None = None,
122
+ set_image_builder_version: str | None = None,
115
123
  ):
116
- if set_name is None and set_web_suffix is None:
117
- raise UsageError("You need to at least one new property (using --set-name or --set-web-suffix)")
124
+ if set_name is None and set_web_suffix is None and set_image_builder_version is None:
125
+ raise UsageError(
126
+ "You need to set at least one new property"
127
+ " (using --set-name, --set-web-suffix, or --set-image-builder-version)"
128
+ )
118
129
 
119
130
  if set_name and set_name != current_name:
120
131
  check_environment_name(set_name, allow_periods=False)
121
132
 
122
- environments.update_environment(current_name, new_name=set_name, new_web_suffix=set_web_suffix)
133
+ environments.update_environment(
134
+ current_name,
135
+ new_name=set_name,
136
+ new_web_suffix=set_web_suffix,
137
+ new_image_builder_version=set_image_builder_version,
138
+ )
123
139
  rich.print("[green]✓[/green] Environment updated")
124
140
 
125
141
 
@@ -639,3 +639,103 @@ def service_users_set_role(
639
639
  f"[green]✓[/green] Updated Service User [bold]{service_user_name}[/bold]'s role in "
640
640
  f"Environment [bold]{environment_name}[/bold] to {role.capitalize()}."
641
641
  )
642
+
643
+
644
+ MEMBER_TOKENS_HELP_TEXT = """Manage Member API Tokens.
645
+
646
+ All Member Tokens are associated with a unique Member of the Workspace. Any requests to Modal using a
647
+ Token are authenticated as the associated Member (and as such, are governed by the permissions that
648
+ have been assigned to the specific Member). Workspace Owners and Managers can use [`modal environment roles update`](https://modal.com/docs/cli/latest/environment#modal-environment-roles-update)
649
+ to update these permissions programmatically.
650
+
651
+ This CLI allows users to list and delete the tokens associated to themselves. Workspace Owners
652
+ and Managers can list all tokens in their Workspace, and can delete any of them.
653
+ """
654
+
655
+ member_tokens_cli = ModalGroup(name="member-tokens", help=MEMBER_TOKENS_HELP_TEXT)
656
+ workspace_cli.add_command(member_tokens_cli)
657
+
658
+
659
+ @member_tokens_cli.command("list")
660
+ @click.option("--json", is_flag=True, default=False, help="Output as JSON.")
661
+ def member_tokens_list(json: bool):
662
+ """List Member Tokens in the Workspace. Workspace Managers and Owners can see all tokens in the
663
+ Workspace.
664
+
665
+ ```bash
666
+ modal workspace member-tokens list
667
+
668
+ # Show list in JSON format:
669
+ modal workspace member-tokens list --json
670
+ ```
671
+ """
672
+ ws = Workspace.from_context()
673
+ tokens = ws.member_tokens.list()
674
+
675
+ output_manager = OutputManager.get()
676
+
677
+ if json:
678
+ output_manager.print_json(
679
+ json_mod.dumps(
680
+ [
681
+ {
682
+ "name": tok.name,
683
+ "token_id": tok.token_id,
684
+ "user_id": tok.user_id,
685
+ "username": tok.username,
686
+ "created_at": timestamp_to_localized_str(tok.created_at.timestamp()),
687
+ "last_used_at": timestamp_to_localized_str(tok.last_used_at.timestamp())
688
+ if tok.last_used_at is not None
689
+ else None,
690
+ "expires_at": timestamp_to_localized_str(tok.expires_at.timestamp())
691
+ if tok.expires_at is not None
692
+ else None,
693
+ }
694
+ for tok in tokens
695
+ ]
696
+ )
697
+ )
698
+ return
699
+
700
+ table = rich.table.Table(box=SIMPLE_HEAD)
701
+ table.add_column("Token ID")
702
+ table.add_column("Name")
703
+ table.add_column("Created By")
704
+ table.add_column("Created At")
705
+ table.add_column("Expires")
706
+
707
+ for tok in tokens:
708
+ table.add_row(
709
+ tok.token_id,
710
+ tok.name if tok.name else "-",
711
+ tok.username,
712
+ timestamp_to_localized_str(tok.created_at.timestamp(), isotz=False),
713
+ timestamp_to_localized_str(tok.expires_at.timestamp(), isotz=False) if tok.expires_at else "-",
714
+ )
715
+
716
+ output_manager.print(table)
717
+
718
+
719
+ @member_tokens_cli.command("delete", no_args_is_help=True)
720
+ @click.argument("token_id")
721
+ @click.option("--allow-missing", is_flag=True, default=False, help="Don't error if the Token doesn't exist.")
722
+ @yes_option
723
+ def member_tokens_delete(token_id: str, *, allow_missing: bool, yes: bool):
724
+ """Delete a Member Token in this Workspace.
725
+
726
+ Note that this action is irreversible. This will immediately invalidate the target token and will
727
+ render it unable to authenticate with Modal.
728
+
729
+ ```bash
730
+ # Delete the Token with ID `ak-0123456789abcdefghijkl`
731
+ modal workspace member-tokens delete ak-0123456789abcdefghijkl
732
+ ```
733
+ """
734
+ if not yes:
735
+ confirm_or_suggest_yes(f"Are you sure you want to irrevocably delete the Token '{token_id}'?")
736
+
737
+ ws = Workspace.from_context()
738
+ ws.member_tokens.delete(token_id, allow_missing=allow_missing)
739
+
740
+ output_manager = OutputManager.get()
741
+ output_manager.print(f"[green]✓[/green] Deleted Token `{token_id}`.")
@@ -80,7 +80,7 @@ class _Client:
80
80
  server_url: str,
81
81
  client_type: int,
82
82
  credentials: typing.Optional[tuple[str, str]],
83
- version: str = "1.6.3.dev8",
83
+ version: str = "1.6.3.dev9",
84
84
  *,
85
85
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
86
86
  ):
@@ -290,7 +290,7 @@ class Client:
290
290
  server_url: str,
291
291
  client_type: int,
292
292
  credentials: typing.Optional[tuple[str, str]],
293
- version: str = "1.6.3.dev8",
293
+ version: str = "1.6.3.dev9",
294
294
  *,
295
295
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
296
296
  ):