modal 1.5.3.dev13__tar.gz → 1.5.4.dev1__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 (236) hide show
  1. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/PKG-INFO +1 -1
  2. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_clustered_functions.py +11 -0
  3. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_clustered_functions.pyi +6 -2
  4. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_environments.py +72 -38
  5. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_image.py +50 -41
  6. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/environment.py +107 -44
  7. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/image.py +13 -25
  8. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/client.pyi +2 -2
  9. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/environments.py +3 -0
  10. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/environments.pyi +82 -55
  11. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/experimental/__init__.py +9 -1
  12. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/functions.pyi +6 -6
  13. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/image.pyi +6 -4
  14. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/PKG-INFO +1 -1
  15. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/api_pb2.py +702 -702
  16. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/api_pb2.pyi +22 -4
  17. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_version/__init__.py +1 -1
  18. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/LICENSE +0 -0
  19. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/README.md +0 -0
  20. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/__init__.py +0 -0
  21. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/__main__.py +0 -0
  22. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_billing.py +0 -0
  23. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_container_entrypoint.py +0 -0
  24. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_function_variants.py +0 -0
  25. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_functions.py +0 -0
  26. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_grpc_client.py +0 -0
  27. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_ipython.py +0 -0
  28. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_load_context.py +0 -0
  29. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_location.py +0 -0
  30. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_logs.py +0 -0
  31. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_logs_manager.py +0 -0
  32. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_logs_manager.pyi +0 -0
  33. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_object.py +0 -0
  34. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_output/__init__.py +0 -0
  35. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_output/manager.py +0 -0
  36. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_output/pty.py +0 -0
  37. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_output/rich.py +0 -0
  38. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_output/status.py +0 -0
  39. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_partial_function.py +0 -0
  40. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_resolver.py +0 -0
  41. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_resources.py +0 -0
  42. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/__init__.py +0 -0
  43. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/asgi.py +0 -0
  44. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/container_io_manager.py +0 -0
  45. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/container_io_manager.pyi +0 -0
  46. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/execution_context.py +0 -0
  47. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/execution_context.pyi +0 -0
  48. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  49. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/task_lifecycle_manager.py +0 -0
  50. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  51. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/telemetry.py +0 -0
  52. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/user_code_event_loop.py +0 -0
  53. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_runtime/user_code_imports.py +0 -0
  54. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_serialization.py +0 -0
  55. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_server.py +0 -0
  56. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_supports_logs.py +0 -0
  57. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_traceback.py +0 -0
  58. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_tunnel.py +0 -0
  59. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_tunnel.pyi +0 -0
  60. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_type_manager.py +0 -0
  61. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/__init__.py +0 -0
  62. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/app_utils.py +0 -0
  63. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/async_utils.py +0 -0
  64. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/auth_token_manager.py +0 -0
  65. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/blob_utils.py +0 -0
  66. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/browser_utils.py +0 -0
  67. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/bytes_io_segment_payload.py +0 -0
  68. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/curl_utils.py +0 -0
  69. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/deprecation.py +0 -0
  70. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/display_utils.py +0 -0
  71. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/docker_utils.py +0 -0
  72. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/function_utils.py +0 -0
  73. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/git_utils.py +0 -0
  74. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/grpc_testing.py +0 -0
  75. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/grpc_utils.py +0 -0
  76. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/hash_utils.py +0 -0
  77. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/http_utils.py +0 -0
  78. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/jwt_utils.py +0 -0
  79. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/logger.py +0 -0
  80. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/mount_utils.py +0 -0
  81. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/name_utils.py +0 -0
  82. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/package_utils.py +0 -0
  83. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/pattern_utils.py +0 -0
  84. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/proxy_support.py +0 -0
  85. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/rand_pb_testing.py +0 -0
  86. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/sandbox_fs_utils.py +0 -0
  87. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/shell_utils.py +0 -0
  88. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/task_command_router_client.py +0 -0
  89. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_utils/time_utils.py +0 -0
  90. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_vendor/__init__.py +0 -0
  91. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  92. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_vendor/cloudpickle.py +0 -0
  93. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_vendor/tblib.py +0 -0
  94. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_vendor/version.py +0 -0
  95. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_watcher.py +0 -0
  96. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/_workspace.py +0 -0
  97. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/app.py +0 -0
  98. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/app.pyi +0 -0
  99. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/billing.py +0 -0
  100. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/billing.pyi +0 -0
  101. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/2023.12.312.txt +0 -0
  102. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/2023.12.txt +0 -0
  103. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/2024.04.txt +0 -0
  104. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/2024.10.txt +0 -0
  105. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/2025.06.txt +0 -0
  106. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/PREVIEW.txt +0 -0
  107. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/README.md +0 -0
  108. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/builder/base-images.json +0 -0
  109. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/call_graph.py +0 -0
  110. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/__init__.py +0 -0
  111. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/_download.py +0 -0
  112. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/_help.py +0 -0
  113. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/_traceback.py +0 -0
  114. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/app.py +0 -0
  115. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/billing.py +0 -0
  116. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/bootstrap.py +0 -0
  117. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/changelog.py +0 -0
  118. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/cluster.py +0 -0
  119. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/config.py +0 -0
  120. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/container.py +0 -0
  121. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/curl.py +0 -0
  122. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/dashboard.py +0 -0
  123. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/dict.py +0 -0
  124. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/endpoint.py +0 -0
  125. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/entry_point.py +0 -0
  126. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/import_refs.py +0 -0
  127. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/launch.py +0 -0
  128. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/logo.py +0 -0
  129. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/network_file_system.py +0 -0
  130. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/profile.py +0 -0
  131. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/programs/__init__.py +0 -0
  132. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/programs/run_jupyter.py +0 -0
  133. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/programs/vscode.py +0 -0
  134. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/queues.py +0 -0
  135. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/run.py +0 -0
  136. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/secret.py +0 -0
  137. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/selector.py +0 -0
  138. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/shell.py +0 -0
  139. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/skills.py +0 -0
  140. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/token.py +0 -0
  141. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/utils.py +0 -0
  142. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/volume.py +0 -0
  143. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cli/workspace.py +0 -0
  144. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/client.py +0 -0
  145. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cloud_bucket_mount.py +0 -0
  146. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cloud_bucket_mount.pyi +0 -0
  147. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cls.py +0 -0
  148. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/cls.pyi +0 -0
  149. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/config.py +0 -0
  150. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/container_process.py +0 -0
  151. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/container_process.pyi +0 -0
  152. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/dict.py +0 -0
  153. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/dict.pyi +0 -0
  154. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/exception.py +0 -0
  155. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/experimental/flash.py +0 -0
  156. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/experimental/flash.pyi +0 -0
  157. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/experimental/ipython.py +0 -0
  158. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/file_io.py +0 -0
  159. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/file_io.pyi +0 -0
  160. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/file_pattern_matcher.py +0 -0
  161. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/functions.py +0 -0
  162. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/image.py +0 -0
  163. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/io_streams.py +0 -0
  164. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/io_streams.pyi +0 -0
  165. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/mount.py +0 -0
  166. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/mount.pyi +0 -0
  167. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/network_file_system.py +0 -0
  168. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/network_file_system.pyi +0 -0
  169. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/object.py +0 -0
  170. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/object.pyi +0 -0
  171. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/output.py +0 -0
  172. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/parallel_map.py +0 -0
  173. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/parallel_map.pyi +0 -0
  174. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/partial_function.py +0 -0
  175. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/partial_function.pyi +0 -0
  176. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/proxy.py +0 -0
  177. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/proxy.pyi +0 -0
  178. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/py.typed +0 -0
  179. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/queue.py +0 -0
  180. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/queue.pyi +0 -0
  181. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/retries.py +0 -0
  182. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/runner.py +0 -0
  183. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/runner.pyi +0 -0
  184. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/running_app.py +0 -0
  185. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/sandbox.py +0 -0
  186. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/sandbox.pyi +0 -0
  187. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/sandbox_fs.py +0 -0
  188. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/sandbox_fs.pyi +0 -0
  189. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/schedule.py +0 -0
  190. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/scheduler_placement.py +0 -0
  191. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/secret.py +0 -0
  192. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/secret.pyi +0 -0
  193. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/server.py +0 -0
  194. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/server.pyi +0 -0
  195. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/serving.py +0 -0
  196. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/serving.pyi +0 -0
  197. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/skills/modal/SKILL.md +0 -0
  198. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/snapshot.py +0 -0
  199. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/snapshot.pyi +0 -0
  200. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/stream_type.py +0 -0
  201. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/token_flow.py +0 -0
  202. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/token_flow.pyi +0 -0
  203. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/types.py +0 -0
  204. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/volume.py +0 -0
  205. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/volume.pyi +0 -0
  206. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/workspace.py +0 -0
  207. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal/workspace.pyi +0 -0
  208. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/SOURCES.txt +0 -0
  209. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/dependency_links.txt +0 -0
  210. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/entry_points.txt +0 -0
  211. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/requires.txt +0 -0
  212. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal.egg-info/top_level.txt +0 -0
  213. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/__init__.py +0 -0
  214. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/gen_cli_docs.py +0 -0
  215. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/gen_cli_docs_main.py +0 -0
  216. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/gen_reference_docs.py +0 -0
  217. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/gen_reference_docs_main.py +0 -0
  218. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/mdmd/__init__.py +0 -0
  219. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/mdmd/mdmd.py +0 -0
  220. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/mdmd/signatures.py +0 -0
  221. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/mdmd/types.py +0 -0
  222. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_docs/reference_intro.md +0 -0
  223. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/__init__.py +0 -0
  224. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/api_grpc.py +0 -0
  225. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/api_pb2_grpc.py +0 -0
  226. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/api_pb2_grpc.pyi +0 -0
  227. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/modal_api_grpc.py +0 -0
  228. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/py.typed +0 -0
  229. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/task_command_router_grpc.py +0 -0
  230. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/task_command_router_pb2.py +0 -0
  231. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/task_command_router_pb2.pyi +0 -0
  232. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  233. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  234. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/modal_version/__main__.py +0 -0
  235. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/pyproject.toml +0 -0
  236. {modal-1.5.3.dev13 → modal-1.5.4.dev1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.3.dev13
3
+ Version: 1.5.4.dev1
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -15,6 +15,7 @@ class ClusterInfo:
15
15
  cluster_id: str
16
16
  container_ips: list[str]
17
17
  container_ipv4_ips: list[str]
18
+ fabric_ids: list[str]
18
19
 
19
20
 
20
21
  cluster_info: ClusterInfo | None = None
@@ -29,6 +30,14 @@ def get_cluster_info() -> ClusterInfo:
29
30
  return cluster_info
30
31
 
31
32
 
33
+ def get_fabric_peers() -> list[int]:
34
+ info = get_cluster_info()
35
+ if info.rank >= len(info.fabric_ids) or not info.fabric_ids[info.rank]:
36
+ return [info.rank]
37
+ own_fabric_id = info.fabric_ids[info.rank]
38
+ return [rank for rank, fabric_id in enumerate(info.fabric_ids) if fabric_id == own_fabric_id]
39
+
40
+
32
41
  async def _initialize_clustered_function(client: _Client, task_id: str, world_size: int):
33
42
  global cluster_info
34
43
 
@@ -70,6 +79,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
70
79
  cluster_id=resp.cluster_id,
71
80
  container_ips=resp.container_ips,
72
81
  container_ipv4_ips=resp.container_ipv4_ips,
82
+ fabric_ids=resp.fabric_ids,
73
83
  )
74
84
  else:
75
85
  cluster_info = ClusterInfo(
@@ -77,6 +87,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
77
87
  cluster_id="", # No cluster ID for single-node # TODO(irfansharif): Is this right?
78
88
  container_ips=[container_ip],
79
89
  container_ipv4_ips=[], # No IPv4 IPs for single-node
90
+ fabric_ids=[],
80
91
  )
81
92
 
82
93
 
@@ -3,14 +3,17 @@ import types
3
3
  import typing_extensions
4
4
 
5
5
  class ClusterInfo:
6
- """ClusterInfo(rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str])"""
6
+ """ClusterInfo(rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str], fabric_ids: list[str])"""
7
7
 
8
8
  rank: int
9
9
  cluster_id: str
10
10
  container_ips: list[str]
11
11
  container_ipv4_ips: list[str]
12
+ fabric_ids: list[str]
12
13
 
13
- def __init__(self, rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str]) -> None:
14
+ def __init__(
15
+ self, rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str], fabric_ids: list[str]
16
+ ) -> None:
14
17
  """Initialize self. See help(type(self)) for accurate signature."""
15
18
  ...
16
19
 
@@ -23,6 +26,7 @@ class ClusterInfo:
23
26
  ...
24
27
 
25
28
  def get_cluster_info() -> ClusterInfo: ...
29
+ def get_fabric_peers() -> list[int]: ...
26
30
  async def _initialize_clustered_function(client: modal.client._Client, task_id: str, world_size: int): ...
27
31
 
28
32
  class __initialize_clustered_function_spec(typing_extensions.Protocol):
@@ -4,7 +4,7 @@ import builtins
4
4
  from collections.abc import Iterable, Mapping
5
5
  from dataclasses import dataclass
6
6
  from datetime import datetime, timezone
7
- from typing import Literal
7
+ from typing import Any, Literal
8
8
 
9
9
  from google.protobuf.empty_pb2 import Empty
10
10
  from google.protobuf.message import Message
@@ -16,6 +16,7 @@ from modal_proto import api_pb2
16
16
  from ._load_context import LoadContext
17
17
  from ._object import _Object
18
18
  from ._resolver import Resolver
19
+ from ._utils.deprecation import deprecation_warning
19
20
  from ._utils.name_utils import check_environment_name
20
21
  from ._utils.time_utils import is_utc_month_aligned, parse_billing_cycle
21
22
  from .client import _Client
@@ -38,6 +39,7 @@ class _EnvironmentManager:
38
39
  name: str, # Name to use for the new Environment
39
40
  *,
40
41
  restricted: bool = False, # If True, enable RBAC restrictions on the Environment
42
+ experimental_options: dict[str, Any] | None = None, # Experimental options for Environment creation
41
43
  client: _Client | None = None, # Optional client with Modal credentials
42
44
  ) -> None:
43
45
  """Create a new Environment.
@@ -50,7 +52,10 @@ class _EnvironmentManager:
50
52
  """
51
53
  check_environment_name(name)
52
54
  client = await _Client.from_env() if client is None else client
53
- await client.stub.EnvironmentCreate(api_pb2.EnvironmentCreateRequest(name=name, is_managed=restricted))
55
+ request = api_pb2.EnvironmentCreateRequest(name=name, is_managed=restricted)
56
+ if (experimental_options or {}).get("is_public", False):
57
+ request.environment_type = api_pb2.ENVIRONMENT_TYPE_PUBLIC
58
+ await client.stub.EnvironmentCreate(request)
54
59
 
55
60
  async def list(
56
61
  self,
@@ -104,7 +109,7 @@ class _EnvironmentManager:
104
109
  await client.stub.EnvironmentDelete(api_pb2.EnvironmentDeleteRequest(name=name))
105
110
 
106
111
 
107
- class _EnvironmentMembersManager:
112
+ class _EnvironmentRolesManager:
108
113
  """mdmd:namespace"""
109
114
 
110
115
  def __init__(self, environment: "_Environment"):
@@ -112,13 +117,13 @@ class _EnvironmentMembersManager:
112
117
  self._environment = environment
113
118
 
114
119
  async def list(self) -> dict[Literal["users", "service_users"], dict[str, str]]:
115
- """Enumerate the environment roles for each user and service user in the workspace.
120
+ """Enumerate the Environment Role for each user and service user in the workspace.
116
121
 
117
122
  **Examples:**
118
123
 
119
124
  ```python notest
120
- members = modal.Environment.from_name("my-env").members.list()
121
- print(members)
125
+ roles = modal.Environment.from_name("my-env").roles.list()
126
+ print(roles)
122
127
  # {
123
128
  # "users": {"alice": "contributor", "bob": "viewer", "carol": "contributor"},
124
129
  # "service_users": {"alice-bot": "contributor", "ops-bot": "viewer", "ci-bot": "no-access"},
@@ -145,16 +150,17 @@ class _EnvironmentMembersManager:
145
150
  users: Mapping[str, str] | None = None,
146
151
  service_users: Mapping[str, str] | None = None,
147
152
  ) -> None:
148
- """Add or modify roles for members of a restricted Environment.
153
+ """Update the Environment Role of users and service users.
149
154
 
150
- Each user or service user will be added to the Environment if not currently a member;
151
- if already a member, the user or service user's role will be updated.
155
+ Each role is one of 'contributor', 'viewer', or 'no-access'. Service users can be
156
+ assigned a role on any Environment, while workspace members can only be assigned a
157
+ role on restricted Environments.
152
158
 
153
159
  **Examples:**
154
160
 
155
161
  ```python notest
156
162
  env = modal.Environment.from_name("my-restricted-env")
157
- env.members.update(
163
+ env.roles.update(
158
164
  users={"alice": "contributor", "bob": "viewer"},
159
165
  service_users={"alice-bot": "contributor"},
160
166
  )
@@ -196,24 +202,59 @@ class _EnvironmentMembersManager:
196
202
 
197
203
  await self._dispatch_role_updates(requests)
198
204
 
205
+ async def _dispatch_role_updates(self, requests: dict[str, api_pb2.EnvironmentRoleSetRequest]) -> None:
206
+ """Send batched EnvironmentRoleSet RPCs and report all errors encountered."""
207
+ results = await asyncio.gather(
208
+ *(self._environment.client.stub.EnvironmentRoleSet(req) for req in requests.values()),
209
+ return_exceptions=True,
210
+ )
211
+ errors = [(label, result) for label, result in zip(requests.keys(), results) if isinstance(result, Exception)]
212
+ if errors:
213
+ n = len(errors)
214
+ header = f"{n} error{'s' if n != 1 else ''} occurred while updating Environment roles:"
215
+ details = "\n".join(f" - {label}: {e}" for label, e in errors)
216
+ raise WorkspaceManagementError(f"{header}\n{details}")
217
+
218
+
219
+ # Deprecated alias for `_EnvironmentRolesManager`; each method warns and delegates.
220
+ class _EnvironmentMembersManager:
221
+ """mdmd:hidden"""
222
+
223
+ def __init__(self, environment: "_Environment"):
224
+ """mdmd:hidden"""
225
+ self._environment = environment
226
+
227
+ async def list(self) -> dict[Literal["users", "service_users"], dict[str, str]]:
228
+ deprecation_warning(
229
+ (2026, 7, 23),
230
+ "`Environment.members.list()` is deprecated; use `Environment.roles.list()` instead.",
231
+ )
232
+ return await _EnvironmentRolesManager(self._environment).list()
233
+
234
+ async def update(
235
+ self,
236
+ *,
237
+ users: Mapping[str, str] | None = None,
238
+ service_users: Mapping[str, str] | None = None,
239
+ ) -> None:
240
+ deprecation_warning(
241
+ (2026, 7, 23),
242
+ "`Environment.members.update()` is deprecated; use `Environment.roles.update()` instead.",
243
+ )
244
+ await _EnvironmentRolesManager(self._environment).update(users=users, service_users=service_users)
245
+
199
246
  async def remove(
200
247
  self,
201
248
  *,
202
249
  users: Iterable[str] | None = None,
203
250
  service_users: Iterable[str] | None = None,
204
251
  ) -> None:
205
- """Remove members from a restricted Environment.
206
-
207
- **Examples:**
208
-
209
- ```python notest
210
- env = modal.Environment.from_name("my-restricted-env")
211
- env.members.remove(
212
- users=["alice"],
213
- service_users=["alice-bot"],
252
+ """Remove the Environment Role of users and service users, reverting them to the default."""
253
+ deprecation_warning(
254
+ (2026, 7, 23),
255
+ "`Environment.members.remove()` is deprecated. Environment Roles are now explicit; "
256
+ "set a role (e.g. 'no-access') with `Environment.roles.update()` instead.",
214
257
  )
215
- ```
216
- """
217
258
  await self._environment.hydrate()
218
259
  users = users or []
219
260
  service_users = service_users or []
@@ -247,20 +288,7 @@ class _EnvironmentMembersManager:
247
288
  role=api_pb2.ENVIRONMENT_ROLE_UNSPECIFIED,
248
289
  )
249
290
 
250
- await self._dispatch_role_updates(requests)
251
-
252
- async def _dispatch_role_updates(self, requests: dict[str, api_pb2.EnvironmentRoleSetRequest]) -> None:
253
- """Send batched EnvironmentRoleSet RPCs and report all errors encountered."""
254
- results = await asyncio.gather(
255
- *(self._environment.client.stub.EnvironmentRoleSet(req) for req in requests.values()),
256
- return_exceptions=True,
257
- )
258
- errors = [(label, result) for label, result in zip(requests.keys(), results) if isinstance(result, Exception)]
259
- if errors:
260
- n = len(errors)
261
- header = f"{n} error{'s' if n != 1 else ''} occurred while updating Environment members:"
262
- details = "\n".join(f" - {label}: {e}" for label, e in errors)
263
- raise WorkspaceManagementError(f"{header}\n{details}")
291
+ await _EnvironmentRolesManager(self._environment)._dispatch_role_updates(requests)
264
292
 
265
293
 
266
294
  class _Environment(_Object, type_prefix="en"):
@@ -284,11 +312,17 @@ class _Environment(_Object, type_prefix="en"):
284
312
  return _EnvironmentManager()
285
313
 
286
314
  @property
287
- def members(self) -> "_EnvironmentMembersManager":
288
- """Namespace with methods for managing the membership of a restricted Environment.
315
+ def roles(self) -> "_EnvironmentRolesManager":
316
+ """Namespace with methods for managing the Environment Roles of users and service users.
289
317
 
290
- See https://modal.com/docs/guide/rbac for more information on restricted Environments.
318
+ See https://modal.com/docs/guide/rbac for more information on Environment Roles.
291
319
  """
320
+ return _EnvironmentRolesManager(self)
321
+
322
+ @property
323
+ def members(self) -> "_EnvironmentMembersManager":
324
+ """mdmd:hidden"""
325
+ # Deprecated alias for `Environment.roles`.
292
326
  return _EnvironmentMembersManager(self)
293
327
 
294
328
  # TODO(michael) Keeping this private for now until we decide what else should be in it
@@ -117,34 +117,51 @@ 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) -> 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
- """
131
- image_name, sep, tag = name.partition(":")
120
+ def _parse_image_name(partial_name: str) -> tuple[str | None, str]:
121
+ partial_name_no_env, sep, tag = partial_name.partition(":")
132
122
  if not sep:
133
123
  tag = "latest"
134
124
 
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.")
125
+ if "/" in partial_name_no_env:
126
+ environment, image_name = partial_name_no_env.rsplit("/", 1)
127
+ if not environment:
128
+ raise InvalidError("Invalid Image name: environment prefix before '/' must be non-empty.")
140
129
  if not image_name:
141
- raise InvalidError("Invalid Image name: name after '/' must be non-empty.")
130
+ raise InvalidError("Invalid Image name: name after last '/' must be non-empty.")
131
+ else:
132
+ environment = None
133
+ image_name = partial_name_no_env
142
134
 
143
135
  _validate_image_name(image_name)
144
136
  _validate_image_tag(tag)
145
137
 
146
- full_name = f"{prefix}/{image_name}" if prefix else image_name
147
- return prefix, f"{full_name}:{tag}"
138
+ return environment, f"{image_name}:{tag}"
139
+
140
+
141
+ def _upgrade_image_name(
142
+ partial_name: str,
143
+ *,
144
+ explicit_environment_name: str | None = None,
145
+ fallback_environment_name: str | None = None,
146
+ ) -> str:
147
+ """Ensures an Image name has a tag and an environment name when possible
148
+
149
+ * If no tag is present, add :latest
150
+ * If an explicit environment name is passed, add it as a prefix and error
151
+ if there is already a prefix
152
+ * If a fallback environment name is passed, add it only when there is no prefix
153
+
154
+ """
155
+ prefix_environment, name_tag = _parse_image_name(partial_name)
156
+ if prefix_environment:
157
+ if explicit_environment_name is not None:
158
+ raise InvalidError("environment_name cannot be used when an Image name includes an environment prefix.")
159
+ return f"{prefix_environment}/{name_tag}"
160
+
161
+ environment_name = explicit_environment_name if explicit_environment_name is not None else fallback_environment_name
162
+ if environment_name:
163
+ return f"{environment_name}/{name_tag}"
164
+ return name_tag # no explicit environment - use just the unprefixed name:tag
148
165
 
149
166
 
150
167
  def _validate_python_version(
@@ -2932,8 +2949,8 @@ class _Image(_Object, type_prefix="im"):
2932
2949
  ) -> "_Image":
2933
2950
  """Reference a named Image that was previously published with `.publish()`.
2934
2951
 
2935
- Names can contain an optional `:tag` part - if no tag part is included `":latest"` is used,
2936
- matching Docker conventions.
2952
+ Names can contain an optional `:tag` part. If no tag part is included, `":latest"` is used, matching
2953
+ Docker conventions.
2937
2954
 
2938
2955
  ```python notest
2939
2956
  image = modal.Image.from_name("my-image") # references my-image:latest
@@ -2944,20 +2961,16 @@ class _Image(_Object, type_prefix="im"):
2944
2961
  ...
2945
2962
  ```
2946
2963
  """
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 '/'.")
2964
+ _upgrade_image_name(name, explicit_environment_name=environment_name) # validate
2951
2965
 
2952
2966
  async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
2953
2967
  req = api_pb2.ImageGetByTagRequest(
2954
- tag=tag,
2955
- environment_name="" if namespace_prefix else load_context.environment_name,
2968
+ tag=_upgrade_image_name(name, fallback_environment_name=load_context.environment_name),
2956
2969
  )
2957
2970
  response = await load_context.client.stub.ImageGetByTag(req)
2958
2971
  self._hydrate(response.image_id, load_context.client, None)
2959
2972
 
2960
- rep = _Image._repr(tag, environment_name)
2973
+ rep = _Image._repr(name, environment_name)
2961
2974
  return _Image._from_loader(
2962
2975
  _load,
2963
2976
  rep,
@@ -2971,13 +2984,14 @@ class _Image(_Object, type_prefix="im"):
2971
2984
  name: str,
2972
2985
  *,
2973
2986
  environment_name: str | None = None,
2987
+ experimental_options: dict[str, Any] | None = None,
2974
2988
  client: _Client | None = None,
2975
2989
  ) -> None:
2976
2990
  """Publish this image under the given name
2977
2991
 
2978
2992
  The Image must already be created (typically by calling `image.build()` or `sandbox.snapshot_filesystem()`).
2979
2993
 
2980
- Image names can contain an explicit tag designation (using the `name:tag`). If no tag is included in the name,
2994
+ Image names can contain an explicit tag designation using `name:tag`. If no tag is included in the name,
2981
2995
  `":latest"` is used, matching Docker conventions. To publish multiple tags, call `.publish()` once per tag.
2982
2996
 
2983
2997
  ```python notest
@@ -2987,25 +3001,20 @@ class _Image(_Object, type_prefix="im"):
2987
3001
  image.publish("my-image-with-numpy:v1")
2988
3002
  ```
2989
3003
  """
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 ""
2998
-
2999
3004
  if self._object_id is None:
3000
3005
  raise InvalidError("Cannot publish an image that has not been created yet. Call `.build()` first.")
3001
3006
 
3002
3007
  _client = client or await _Client.from_env()
3008
+ tag = _upgrade_image_name(
3009
+ name,
3010
+ explicit_environment_name=environment_name,
3011
+ fallback_environment_name=config.get("environment"),
3012
+ )
3003
3013
 
3004
3014
  await _client.stub.ImagePublish(
3005
3015
  api_pb2.ImagePublishRequest(
3006
3016
  image_id=self._object_id,
3007
- environment_name=resolved_env,
3008
- allow_public=False,
3017
+ allow_public=bool((experimental_options or {}).get("is_public", False)),
3009
3018
  tag=tag,
3010
3019
  )
3011
3020
  )
@@ -1,5 +1,6 @@
1
1
  # Copyright Modal Labs 2023
2
2
  import json as json_mod
3
+ import warnings
3
4
 
4
5
  import click
5
6
  import rich
@@ -11,6 +12,7 @@ from rich.text import Text
11
12
  from modal import environments
12
13
  from modal._environments import _Environment
13
14
  from modal._utils.async_utils import synchronizer
15
+ from modal._utils.deprecation import deprecation_warning
14
16
  from modal._utils.display_utils import pretty_decimal
15
17
  from modal._utils.name_utils import check_environment_name
16
18
  from modal._utils.time_utils import format_interval
@@ -18,7 +20,7 @@ from modal.cli.billing import DATE_HELP, _validate_and_parse_interval
18
20
  from modal.cli.utils import _col_name_to_json_key, confirm_or_suggest_yes, display_table, yes_option
19
21
  from modal.config import config
20
22
  from modal.environments import Environment
21
- from modal.exception import InvalidError
23
+ from modal.exception import DeprecationError, InvalidError
22
24
  from modal.output import OutputManager
23
25
 
24
26
  from ._help import ModalGroup
@@ -55,7 +57,7 @@ def list_(json: bool = False):
55
57
  # over the server default
56
58
  active_env = config.get("environment") or ""
57
59
  for env in envs:
58
- if env.default is True and active_env is None:
60
+ if env.default is True and not active_env:
59
61
  active_env = env.name
60
62
 
61
63
  table_data = []
@@ -70,10 +72,11 @@ def list_(json: bool = False):
70
72
  @environment_cli.command("create", help="Create a new environment in the current workspace.", no_args_is_help=True)
71
73
  @click.argument("name")
72
74
  @click.option("--restricted", is_flag=True, default=False, help="Enable RBAC restrictions on the new environment")
73
- def create(name: str, restricted: bool = False):
75
+ @click.option("--public", is_flag=True, default=False, hidden=True)
76
+ def create(name: str, restricted: bool = False, public: bool = False):
74
77
  check_environment_name(name)
75
- Environment.objects.create(name, restricted=restricted)
76
- prefix = "Restricted " if restricted else ""
78
+ Environment.objects.create(name, restricted=restricted, experimental_options={"is_public": public})
79
+ prefix = "Public " if public else "Restricted " if restricted else ""
77
80
  rich.print(f"[green]✓[/green] {prefix}Environment created: {name}")
78
81
 
79
82
 
@@ -123,67 +126,127 @@ def update(
123
126
  rich.print("[green]✓[/green] Environment updated")
124
127
 
125
128
 
126
- MEMBERS_HELP_TEXT = """Manage members and their roles in a restricted Environment.
127
-
128
- Restricted Environments use RBAC to limit the actions that can be performed by
129
- users (and service users) based on roles: https://modal.com/docs/guide/rbac.
130
- """
131
-
132
- members_cli = ModalGroup(name="members", help=MEMBERS_HELP_TEXT)
133
- environment_cli.add_command(members_cli)
134
-
135
129
  service_user_option = click.option(
136
- "--service-user", is_flag=True, default=False, help="Treat MEMBER as the name of a service user"
130
+ "--service-user", is_flag=True, default=False, help="Treat PRINCIPAL as the name of a service user"
137
131
  )
138
132
 
139
133
 
140
- @members_cli.command("list", help="List the members of a restricted Environment", no_args_is_help=True)
141
- @click.argument("environment")
142
- @click.option("--json", is_flag=True, default=False)
143
- def members_list(environment: str, json: bool = False):
144
- members = Environment.from_name(environment).members.list()
134
+ def _render_roles_list(environment: str, json: bool) -> None:
135
+ roles = Environment.from_name(environment).roles.list()
145
136
  if json:
146
137
  # Mirror the API output shape: a nested dict keyed by users / service_users.
147
- OutputManager.get().print_json(json_mod.dumps(members))
138
+ OutputManager.get().print_json(json_mod.dumps(roles))
148
139
  return
149
140
  rows = []
150
- for name, role in members.get("users", {}).items():
141
+ for name, role in roles.get("users", {}).items():
151
142
  rows.append([name, role])
152
- for name, role in members.get("service_users", {}).items():
143
+ for name, role in roles.get("service_users", {}).items():
153
144
  rows.append([f"{name} (service user)", role])
154
145
  rows = sorted(rows, key=lambda x: x[0])
155
146
  display_table(["name", "role"], rows)
156
147
 
157
148
 
158
- @members_cli.command("update", help="Add or update a member's role in a restricted Environment", no_args_is_help=True)
159
- @click.argument("environment")
160
- @click.argument("member")
161
- @click.option(
162
- "--role", type=click.Choice(["contributor", "viewer"]), required=True, help="Role to assign to the member"
163
- )
164
- @service_user_option
165
- def members_update(environment: str, member: str, role: str, service_user: bool = False):
149
+ def _apply_role_update(environment: str, principal: str, role: str, service_user: bool) -> None:
166
150
  env = Environment.from_name(environment)
167
151
  if service_user:
168
- env.members.update(service_users={member: role})
152
+ env.roles.update(service_users={principal: role})
169
153
  else:
170
- env.members.update(users={member: role})
154
+ env.roles.update(users={principal: role})
171
155
  kind = "service user" if service_user else "user"
172
- rich.print(f"[green]✓[/green] Set {kind} {member!r} to role {role!r} in environment {environment!r}")
156
+ rich.print(f"[green]✓[/green] Set {kind} {principal!r} to role {role!r} in environment {environment!r}")
173
157
 
174
158
 
175
- @members_cli.command("remove", help="Remove a member from a restricted Environment", no_args_is_help=True)
176
- @click.argument("environment")
177
- @click.argument("member")
178
- @service_user_option
179
- def members_remove(environment: str, member: str, service_user: bool = False):
159
+ def _apply_role_remove(environment: str, principal: str, service_user: bool) -> None:
180
160
  env = Environment.from_name(environment)
181
- if service_user:
182
- env.members.remove(service_users=[member])
183
- else:
184
- env.members.remove(users=[member])
161
+ # `remove` only lives on the deprecated `members` manager, which warns. The caller (a
162
+ # deprecated `members` command) has already warned, so suppress the redundant SDK warning.
163
+ with warnings.catch_warnings():
164
+ warnings.simplefilter("ignore", DeprecationError)
165
+ if service_user:
166
+ env.members.remove(service_users=[principal])
167
+ else:
168
+ env.members.remove(users=[principal])
185
169
  kind = "service user" if service_user else "user"
186
- rich.print(f"[green]✓[/green] Removed {kind} {member!r} from environment {environment!r}")
170
+ rich.print(f"[green]✓[/green] Removed {kind} {principal!r} from environment {environment!r}")
171
+
172
+
173
+ ROLES_HELP_TEXT = """Manage the Environment Roles of users and service users.
174
+
175
+ An Environment Role is one of 'contributor' (read-write), 'viewer' (read-only), or
176
+ 'no-access', and dictates access to Environments. See
177
+ https://modal.com/docs/guide/rbac for details on which principals can be assigned
178
+ which roles.
179
+ """
180
+
181
+ roles_cli = ModalGroup(name="roles", help=ROLES_HELP_TEXT)
182
+ environment_cli.add_command(roles_cli)
183
+
184
+
185
+ @roles_cli.command("list", help="List the roles of each user and service user in an Environment", no_args_is_help=True)
186
+ @click.argument("environment")
187
+ @click.option("--json", is_flag=True, default=False)
188
+ def roles_list(environment: str, json: bool = False):
189
+ _render_roles_list(environment, json)
190
+
191
+
192
+ @roles_cli.command("update", help="Update a user's or service user's role in an Environment", no_args_is_help=True)
193
+ @click.argument("environment")
194
+ @click.argument("principal")
195
+ @click.option("--role", type=click.Choice(["contributor", "viewer", "no-access"]), required=True, help="Role to assign")
196
+ @service_user_option
197
+ def roles_update(environment: str, principal: str, role: str, service_user: bool = False):
198
+ _apply_role_update(environment, principal, role, service_user)
199
+
200
+
201
+ # The deprecated `members` command group keeps working for existing automation, but is hidden
202
+ # from `--help` so it is not discoverable. Each subcommand warns with its own guidance.
203
+ members_cli = ModalGroup(name="members", help="[Deprecated] Use `modal environment roles`.", hidden=True)
204
+ environment_cli.add_command(members_cli)
205
+
206
+
207
+ @members_cli.command("list", help="[Deprecated] Use `modal environment roles list`", no_args_is_help=True)
208
+ @click.argument("environment")
209
+ @click.option("--json", is_flag=True, default=False)
210
+ def members_list(environment: str, json: bool = False):
211
+ deprecation_warning(
212
+ (2026, 7, 23),
213
+ "`modal environment members list` is deprecated; use `modal environment roles list`.",
214
+ show_source=False,
215
+ )
216
+ _render_roles_list(environment, json)
217
+
218
+
219
+ @members_cli.command("update", help="[Deprecated] Use `modal environment roles update`", no_args_is_help=True)
220
+ @click.argument("environment")
221
+ @click.argument("principal")
222
+ @click.option(
223
+ "--role",
224
+ type=click.Choice(["contributor", "viewer", "no-access"]),
225
+ required=True,
226
+ help="Role to assign to the principal",
227
+ )
228
+ @service_user_option
229
+ def members_update(environment: str, principal: str, role: str, service_user: bool = False):
230
+ deprecation_warning(
231
+ (2026, 7, 23),
232
+ "`modal environment members update` is deprecated; use `modal environment roles update`.",
233
+ show_source=False,
234
+ )
235
+ _apply_role_update(environment, principal, role, service_user)
236
+
237
+
238
+ @members_cli.command("remove", help="[Deprecated] Use `modal environment roles update`", no_args_is_help=True)
239
+ @click.argument("environment")
240
+ @click.argument("principal")
241
+ @service_user_option
242
+ def members_remove(environment: str, principal: str, service_user: bool = False):
243
+ deprecation_warning(
244
+ (2026, 7, 23),
245
+ "`modal environment members remove` is deprecated; roles are now explicit — set a role "
246
+ "(e.g. `--role no-access`) with `modal environment roles update` instead.",
247
+ show_source=False,
248
+ )
249
+ _apply_role_remove(environment, principal, service_user)
187
250
 
188
251
 
189
252
  billing_cli = ModalGroup(name="billing", help="View billing and usage info for the given Environment.")