modal 1.5.2.dev13__tar.gz → 1.5.2.dev15__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/PKG-INFO +1 -1
  2. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/__init__.py +2 -1
  3. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_billing.py +2 -53
  4. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_environments.py +1 -1
  5. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_functions.py +1 -11
  6. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_image.py +1 -1
  7. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/grpc_utils.py +49 -9
  8. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/task_command_router_client.py +17 -5
  9. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_workspace.py +73 -33
  10. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/billing.py +2 -1
  11. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/billing.pyi +27 -28
  12. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/call_graph.py +1 -30
  13. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/_download.py +2 -1
  14. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/cluster.py +1 -1
  15. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/config.py +2 -2
  16. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/container.py +18 -7
  17. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/curl.py +6 -1
  18. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/dict.py +5 -5
  19. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/environment.py +3 -3
  20. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/image.py +7 -7
  21. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/network_file_system.py +12 -6
  22. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/profile.py +1 -1
  23. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/queues.py +5 -5
  24. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/secret.py +2 -2
  25. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/volume.py +17 -9
  26. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/workspace.py +46 -0
  27. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/client.pyi +2 -2
  28. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/config.py +1 -1
  29. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/dict.py +1 -13
  30. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/dict.pyi +4 -24
  31. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/environments.pyi +3 -3
  32. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/file_io.py +1 -1
  33. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/file_io.pyi +9 -9
  34. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/functions.pyi +11 -11
  35. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/network_file_system.pyi +7 -7
  36. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/queue.py +1 -13
  37. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/queue.pyi +4 -24
  38. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/sandbox.py +21 -33
  39. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/sandbox.pyi +34 -42
  40. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/sandbox_fs.py +1 -64
  41. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/sandbox_fs.pyi +14 -117
  42. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/secret.py +1 -13
  43. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/secret.pyi +4 -24
  44. modal-1.5.2.dev15/modal/types.py +286 -0
  45. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/volume.py +1 -43
  46. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/volume.pyi +10 -75
  47. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/workspace.py +5 -0
  48. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/workspace.pyi +138 -10
  49. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal.egg-info/PKG-INFO +1 -1
  50. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal.egg-info/SOURCES.txt +1 -0
  51. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_docs/gen_cli_docs.py +22 -7
  52. modal-1.5.2.dev15/modal_docs/gen_reference_docs.py +361 -0
  53. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_docs/mdmd/mdmd.py +91 -9
  54. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_docs/mdmd/signatures.py +28 -0
  55. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/api_pb2.py +857 -851
  56. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/api_pb2.pyi +37 -9
  57. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_version/__init__.py +1 -1
  58. modal-1.5.2.dev13/modal_docs/gen_reference_docs.py +0 -177
  59. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/LICENSE +0 -0
  60. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/README.md +0 -0
  61. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/__main__.py +0 -0
  62. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_clustered_functions.py +0 -0
  63. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_clustered_functions.pyi +0 -0
  64. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_container_entrypoint.py +0 -0
  65. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_function_variants.py +0 -0
  66. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_grpc_client.py +0 -0
  67. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_ipython.py +0 -0
  68. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_load_context.py +0 -0
  69. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_location.py +0 -0
  70. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_logs.py +0 -0
  71. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_object.py +0 -0
  72. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_output/__init__.py +0 -0
  73. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_output/manager.py +0 -0
  74. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_output/pty.py +0 -0
  75. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_output/rich.py +0 -0
  76. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_output/status.py +0 -0
  77. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_partial_function.py +0 -0
  78. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_resolver.py +0 -0
  79. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_resources.py +0 -0
  80. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/__init__.py +0 -0
  81. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/asgi.py +0 -0
  82. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/container_io_manager.py +0 -0
  83. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/container_io_manager.pyi +0 -0
  84. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/execution_context.py +0 -0
  85. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/execution_context.pyi +0 -0
  86. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  87. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/task_lifecycle_manager.py +0 -0
  88. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  89. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/telemetry.py +0 -0
  90. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/user_code_event_loop.py +0 -0
  91. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_runtime/user_code_imports.py +0 -0
  92. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_serialization.py +0 -0
  93. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_server.py +0 -0
  94. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_traceback.py +0 -0
  95. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_tunnel.py +0 -0
  96. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_tunnel.pyi +0 -0
  97. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_type_manager.py +0 -0
  98. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/__init__.py +0 -0
  99. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/app_utils.py +0 -0
  100. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/async_utils.py +0 -0
  101. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/auth_token_manager.py +0 -0
  102. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/blob_utils.py +0 -0
  103. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/browser_utils.py +0 -0
  104. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/bytes_io_segment_payload.py +0 -0
  105. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/curl_utils.py +0 -0
  106. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/deprecation.py +0 -0
  107. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/docker_utils.py +0 -0
  108. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/function_utils.py +0 -0
  109. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/git_utils.py +0 -0
  110. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/grpc_testing.py +0 -0
  111. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/hash_utils.py +0 -0
  112. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/http_utils.py +0 -0
  113. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/jwt_utils.py +0 -0
  114. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/logger.py +0 -0
  115. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/mount_utils.py +0 -0
  116. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/name_utils.py +0 -0
  117. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/package_utils.py +0 -0
  118. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/pattern_utils.py +0 -0
  119. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/proxy_support.py +0 -0
  120. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/rand_pb_testing.py +0 -0
  121. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/sandbox_fs_utils.py +0 -0
  122. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/shell_utils.py +0 -0
  123. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_utils/time_utils.py +0 -0
  124. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_vendor/__init__.py +0 -0
  125. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  126. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_vendor/cloudpickle.py +0 -0
  127. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_vendor/tblib.py +0 -0
  128. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_vendor/version.py +0 -0
  129. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/_watcher.py +0 -0
  130. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/app.py +0 -0
  131. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/app.pyi +0 -0
  132. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/builder/2023.12.312.txt +0 -0
  133. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/builder/2023.12.txt +0 -0
  134. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/builder/2024.04.txt +0 -0
  135. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/builder/2024.10.txt +0 -0
  136. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/builder/2025.06.txt +0 -0
  137. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/builder/PREVIEW.txt +0 -0
  138. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/builder/README.md +0 -0
  139. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/builder/base-images.json +0 -0
  140. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/__init__.py +0 -0
  141. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/_help.py +0 -0
  142. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/_traceback.py +0 -0
  143. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/app.py +0 -0
  144. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/billing.py +0 -0
  145. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/bootstrap.py +0 -0
  146. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/changelog.py +0 -0
  147. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/dashboard.py +0 -0
  148. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/endpoint.py +0 -0
  149. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/entry_point.py +0 -0
  150. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/import_refs.py +0 -0
  151. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/launch.py +0 -0
  152. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/logo.py +0 -0
  153. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/programs/__init__.py +0 -0
  154. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/programs/run_jupyter.py +0 -0
  155. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/programs/vscode.py +0 -0
  156. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/run.py +0 -0
  157. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/selector.py +0 -0
  158. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/shell.py +0 -0
  159. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/skills.py +0 -0
  160. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/token.py +0 -0
  161. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cli/utils.py +0 -0
  162. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/client.py +0 -0
  163. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cloud_bucket_mount.py +0 -0
  164. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cloud_bucket_mount.pyi +0 -0
  165. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cls.py +0 -0
  166. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/cls.pyi +0 -0
  167. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/container_process.py +0 -0
  168. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/container_process.pyi +0 -0
  169. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/environments.py +0 -0
  170. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/exception.py +0 -0
  171. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/experimental/__init__.py +0 -0
  172. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/experimental/flash.py +0 -0
  173. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/experimental/flash.pyi +0 -0
  174. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/experimental/ipython.py +0 -0
  175. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/file_pattern_matcher.py +0 -0
  176. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/functions.py +0 -0
  177. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/image.py +0 -0
  178. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/image.pyi +0 -0
  179. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/io_streams.py +0 -0
  180. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/io_streams.pyi +0 -0
  181. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/mount.py +0 -0
  182. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/mount.pyi +0 -0
  183. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/network_file_system.py +0 -0
  184. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/object.py +0 -0
  185. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/object.pyi +0 -0
  186. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/output.py +0 -0
  187. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/parallel_map.py +0 -0
  188. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/parallel_map.pyi +0 -0
  189. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/partial_function.py +0 -0
  190. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/partial_function.pyi +0 -0
  191. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/proxy.py +0 -0
  192. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/proxy.pyi +0 -0
  193. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/py.typed +0 -0
  194. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/retries.py +0 -0
  195. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/runner.py +0 -0
  196. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/runner.pyi +0 -0
  197. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/running_app.py +0 -0
  198. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/schedule.py +0 -0
  199. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/scheduler_placement.py +0 -0
  200. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/server.py +0 -0
  201. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/server.pyi +0 -0
  202. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/serving.py +0 -0
  203. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/serving.pyi +0 -0
  204. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/skills/modal/SKILL.md +0 -0
  205. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/snapshot.py +0 -0
  206. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/snapshot.pyi +0 -0
  207. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/stream_type.py +0 -0
  208. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/token_flow.py +0 -0
  209. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal/token_flow.pyi +0 -0
  210. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal.egg-info/dependency_links.txt +0 -0
  211. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal.egg-info/entry_points.txt +0 -0
  212. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal.egg-info/requires.txt +0 -0
  213. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal.egg-info/top_level.txt +0 -0
  214. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_docs/__init__.py +0 -0
  215. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_docs/gen_cli_docs_main.py +0 -0
  216. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_docs/gen_reference_docs_main.py +0 -0
  217. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_docs/mdmd/__init__.py +0 -0
  218. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_docs/mdmd/types.py +0 -0
  219. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/__init__.py +0 -0
  220. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/api_grpc.py +0 -0
  221. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/api_pb2_grpc.py +0 -0
  222. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/api_pb2_grpc.pyi +0 -0
  223. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/modal_api_grpc.py +0 -0
  224. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/py.typed +0 -0
  225. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/task_command_router_grpc.py +0 -0
  226. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/task_command_router_pb2.py +0 -0
  227. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/task_command_router_pb2.pyi +0 -0
  228. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  229. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  230. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/modal_version/__main__.py +0 -0
  231. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/pyproject.toml +0 -0
  232. {modal-1.5.2.dev13 → modal-1.5.2.dev15}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.2.dev13
3
+ Version: 1.5.2.dev15
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -9,7 +9,7 @@ if sys.version_info[:2] >= (3, 15):
9
9
  from modal_version import __version__
10
10
 
11
11
  try:
12
- from . import billing
12
+ from . import billing, types
13
13
  from ._runtime.execution_context import current_function_call_id, current_input_id, interact, is_local
14
14
  from ._tunnel import Tunnel, forward
15
15
  from .app import App
@@ -100,6 +100,7 @@ __all__ = [
100
100
  "interact",
101
101
  "method",
102
102
  "parameter",
103
+ "types",
103
104
  "web_endpoint",
104
105
  "web_server",
105
106
  "wsgi_app",
@@ -1,10 +1,7 @@
1
1
  # Copyright Modal Labs 2025
2
- from dataclasses import FrozenInstanceError, dataclass
3
- from datetime import datetime, timezone
2
+ from datetime import datetime
4
3
  from decimal import Decimal
5
- from typing import Any, Iterable, TypedDict
6
-
7
- from modal_proto import api_pb2
4
+ from typing import TypedDict
8
5
 
9
6
  from ._utils.deprecation import deprecation_warning
10
7
  from .client import _Client
@@ -19,54 +16,6 @@ class WorkspaceBillingReportItem(TypedDict):
19
16
  tags: dict[str, str]
20
17
 
21
18
 
22
- @dataclass(slots=True, frozen=True)
23
- class BillingReportItem:
24
- object_id: str
25
- description: str
26
- environment_name: str
27
- interval_start: datetime
28
- cost: Decimal
29
- cost_by_resource: dict[str, Decimal]
30
- tags: dict[str, str]
31
-
32
- def __getitem__(self, key: str) -> Any:
33
- """mdmd:ignore"""
34
- if key not in self.__slots__:
35
- raise KeyError(key)
36
-
37
- return getattr(self, key)
38
-
39
- def __setitem__(self, key: str, _: Any):
40
- """mdmd:ignore"""
41
- raise FrozenInstanceError(f"cannot assign to field {key!r}")
42
-
43
- def keys(self) -> Iterable[str]:
44
- """mdmd:ignore"""
45
- yield from self.__slots__
46
-
47
- def values(self) -> Iterable[Any]:
48
- """mdmd:ignore"""
49
- for k in self.__slots__:
50
- yield getattr(self, k)
51
-
52
- def items(self) -> Iterable[tuple[str, Any]]:
53
- """mdmd:ignore"""
54
- for k in self.__slots__:
55
- yield k, getattr(self, k)
56
-
57
- @classmethod
58
- def _from_proto(cls, pb_item: api_pb2.WorkspaceBillingReportItem) -> "BillingReportItem":
59
- return cls(
60
- object_id=pb_item.object_id,
61
- description=pb_item.description,
62
- environment_name=pb_item.environment_name,
63
- interval_start=pb_item.interval.ToDatetime().replace(tzinfo=timezone.utc),
64
- cost=Decimal(pb_item.cost),
65
- cost_by_resource={k: Decimal(v) for k, v in pb_item.cost_by_resource.items()},
66
- tags=dict(pb_item.tags),
67
- )
68
-
69
-
70
19
  async def _workspace_billing_report(
71
20
  *,
72
21
  start: datetime, # Start of the report, inclusive
@@ -13,7 +13,6 @@ from synchronicity import classproperty
13
13
 
14
14
  from modal_proto import api_pb2
15
15
 
16
- from ._billing import BillingReportItem
17
16
  from ._load_context import LoadContext
18
17
  from ._object import _Object
19
18
  from ._resolver import Resolver
@@ -21,6 +20,7 @@ from ._utils.name_utils import check_environment_name
21
20
  from .client import _Client
22
21
  from .config import config, logger
23
22
  from .exception import InvalidError, WorkspaceManagementError
23
+ from .types import BillingReportItem
24
24
 
25
25
 
26
26
  @dataclass(frozen=True)
@@ -90,6 +90,7 @@ from .proxy import _Proxy
90
90
  from .retries import Retries, RetryManager
91
91
  from .schedule import Schedule
92
92
  from .secret import _Secret
93
+ from .types import FunctionStats
93
94
  from .volume import _Volume, _volume_to_mount_proto
94
95
 
95
96
  if TYPE_CHECKING:
@@ -541,17 +542,6 @@ class _InputPlaneInvocation:
541
542
  return [("x-modal-input-plane-region", input_plane_region), ("x-modal-auth-token", token)]
542
543
 
543
544
 
544
- # Wrapper type for api_pb2.FunctionStats
545
- @dataclass(frozen=True)
546
- class FunctionStats:
547
- """Simple data structure storing stats for a running function."""
548
-
549
- backlog: int
550
- num_total_runners: int
551
- num_running_inputs: int
552
- input_headroom: int
553
-
554
-
555
545
  @dataclass
556
546
  class _FunctionSpec:
557
547
  """
@@ -2976,7 +2976,7 @@ class _Image(_Object, type_prefix="im"):
2976
2976
  api_pb2.ImagePublishRequest(
2977
2977
  image_id=self._object_id,
2978
2978
  environment_name=resolved_env,
2979
- is_public=False,
2979
+ allow_public=False,
2980
2980
  tag=tag,
2981
2981
  )
2982
2982
  )
@@ -31,7 +31,7 @@ from modal_version import __version__
31
31
 
32
32
  from .._traceback import suppress_tb_frame
33
33
  from ..config import config
34
- from .async_utils import retry
34
+ from .async_utils import TaskContext, retry
35
35
  from .logger import logger
36
36
 
37
37
  RequestType = TypeVar("RequestType", bound=Message)
@@ -190,14 +190,7 @@ class ConnectionManager:
190
190
 
191
191
  async def get_or_create_channel(self, server_url: str) -> grpclib.client.Channel:
192
192
  if server_url not in self._channels:
193
- self._channels[server_url] = create_channel(
194
- server_url,
195
- self._metadata,
196
- )
197
- try:
198
- await connect_channel(self._channels[server_url])
199
- except (OSError, asyncio.TimeoutError) as exc:
200
- raise ConnectionError("Could not connect to the Modal server.") from exc
193
+ self._channels[server_url] = await create_channel_with_fallbacks(server_url, self._metadata)
201
194
  return self._channels[server_url]
202
195
 
203
196
  def close(self):
@@ -296,6 +289,12 @@ def create_channel(
296
289
  for k, v in metadata.items():
297
290
  event.metadata[k] = v
298
291
 
292
+ if o.hostname is not None:
293
+ # GRPC actually injects a :authority pseudo-header containing the hostname.
294
+ # But this gets stripped out by the C/C++ extension server-side and is hard to fix.
295
+ # So let's just add a separate header we can parse easily.
296
+ event.metadata["x-modal-host"] = o.hostname
297
+
299
298
  idempotency_key = typing.cast(Optional[str], event.metadata.get("x-idempotency-key"))
300
299
  if idempotency_key is None:
301
300
  logger.debug(f"Sending request to {event.method_name}")
@@ -314,6 +313,47 @@ async def connect_channel(channel: grpclib.client.Channel):
314
313
  await channel.__connect__()
315
314
 
316
315
 
316
+ async def create_channel_with_fallbacks(
317
+ server_url: str,
318
+ metadata: dict[str, str] = {},
319
+ stagger_delay: float = 1.0,
320
+ ) -> grpclib.client.Channel:
321
+ """Create a connected channel, supporting comma-separated fallback URLs.
322
+
323
+ `server_url` may be a single URL or a comma-separated list of URLs. The connection attempts
324
+ are raced against each other and the first one to connect wins, which lets the client fail
325
+ over to a secondary endpoint when the primary is unreachable (e.g. a DNS outage on the
326
+ primary's domain). Attempts are staggered: the candidate at index `i` waits `i * stagger_delay`
327
+ seconds before starting, so earlier URLs (e.g. the primary) get a head start and are preferred
328
+ when reachable. Losing attempts are cancelled and their channels closed.
329
+ """
330
+ urls = [url.strip() for url in server_url.split(",")]
331
+
332
+ async def connect(index: int, url: str) -> grpclib.client.Channel:
333
+ # Stagger the start so earlier URLs are preferred; once started, all attempts race.
334
+ await asyncio.sleep(index * stagger_delay)
335
+ channel = create_channel(url, metadata)
336
+ try:
337
+ await connect_channel(channel)
338
+ except BaseException:
339
+ # Close the channel of a losing/cancelled attempt so its socket doesn't leak.
340
+ channel.close()
341
+ raise
342
+ return channel
343
+
344
+ first_exc: BaseException | None = None
345
+ async with TaskContext() as tc:
346
+ tasks = [tc.create_task(connect(i, url)) for i, url in enumerate(urls)]
347
+ for attempt in asyncio.as_completed(tasks):
348
+ try:
349
+ return await attempt
350
+ except Exception as exc:
351
+ if first_exc is None:
352
+ first_exc = exc
353
+
354
+ raise ConnectionError("Could not connect to the Modal server.") from first_exc
355
+
356
+
317
357
  if typing.TYPE_CHECKING:
318
358
  import modal.client
319
359
 
@@ -811,13 +811,25 @@ class TaskCommandRouterClient:
811
811
  lambda: self._call_with_auth_retry(self._stub.TaskSetNetworkAccess, request)
812
812
  )
813
813
 
814
- async def reload_volumes(self, task_id: str) -> sr_pb2.TaskReloadVolumesResponse:
815
- """Reload all Volumes mounted in the task to reflect their latest committed state."""
814
+ async def reload_volumes(self, task_id: str, timeout: float) -> sr_pb2.TaskReloadVolumesResponse:
815
+ """Reload all Volumes mounted in the task to reflect their latest committed state.
816
+
817
+ Args:
818
+ task_id: The task whose mounted Volumes should be reloaded.
819
+ timeout: Client-side deadline in seconds. If the reload does not complete within this
820
+ window, the call is cancelled and a `modal.exception.TimeoutError` is raised.
821
+ """
816
822
  request = sr_pb2.TaskReloadVolumesRequest(task_id=task_id)
817
823
  with grpc_error_converter():
818
- return await call_with_retries_on_transient_errors(
819
- lambda: self._call_with_auth_retry(self._stub.TaskReloadVolumes, request)
820
- )
824
+ try:
825
+ return await asyncio.wait_for(
826
+ call_with_retries_on_transient_errors(
827
+ lambda: self._call_with_auth_retry(self._stub.TaskReloadVolumes, request, timeout=timeout),
828
+ ),
829
+ timeout=timeout,
830
+ )
831
+ except asyncio.TimeoutError:
832
+ raise ModalTimeoutError("Timeout expired")
821
833
 
822
834
  async def _snapshot_with_deadline(self, rpc, request, *, timeout: float, **kwargs):
823
835
  # helper method for snapshot_directory and snapshot_filesystem to handle grpc
@@ -1,22 +1,19 @@
1
1
  # Copyright Modal Labs 2025
2
2
  import builtins
3
- from dataclasses import dataclass
4
3
  from datetime import datetime, timezone
5
- from typing import Literal, Optional
4
+ from typing import Optional
6
5
 
7
6
  from google.protobuf.empty_pb2 import Empty
8
7
 
9
8
  from modal.exception import InvalidError
10
9
  from modal_proto import api_pb2
11
10
 
12
- from ._billing import BillingReportItem
13
11
  from ._load_context import LoadContext
14
12
  from ._object import _Object
15
13
  from ._resolver import Resolver
16
14
  from ._utils.time_utils import timestamp_to_localized_dt
17
15
  from .client import _Client
18
-
19
- MemberRole = Literal["user", "manager", "owner"]
16
+ from .types import BillingReportItem, MemberRole, ProxyTokenInfo, TokenData, WorkspaceMemberInfo, WorkspaceSettings
20
17
 
21
18
 
22
19
  def _member_role_from_proto(proto_value: int) -> MemberRole:
@@ -31,18 +28,6 @@ def _member_role_from_proto(proto_value: int) -> MemberRole:
31
28
  raise ValueError(f"Unknown workspace member role: {proto_value}")
32
29
 
33
30
 
34
- @dataclass(frozen=True)
35
- class WorkspaceMemberInfo:
36
- """Metadata about a Workspace member."""
37
-
38
- name: str
39
- email: str
40
- user_id: str
41
- role: MemberRole
42
- joined_at: datetime
43
- last_active_at: Optional[datetime] # None if the member has never been active
44
-
45
-
46
31
  class _WorkspaceMembersManager:
47
32
  """mdmd:namespace
48
33
  Namespace with methods for managing the membership of a Workspace.
@@ -125,22 +110,9 @@ class _Workspace(_Object, type_prefix="ac"):
125
110
  def proxy_tokens(self) -> "_WorkspaceProxyTokenManager":
126
111
  return _WorkspaceProxyTokenManager(self)
127
112
 
128
-
129
- @dataclass(frozen=True)
130
- class TokenData:
131
- """A token ID / secret pair."""
132
-
133
- token_id: str
134
- token_secret: str
135
-
136
-
137
- @dataclass(frozen=True)
138
- class ProxyTokenInfo:
139
- """Metadata about a proxy token, not including the token secret."""
140
-
141
- token_id: str
142
- created_at: datetime
143
- scoped: bool
113
+ @property
114
+ def settings(self) -> "_WorkspaceSettingsManager":
115
+ return _WorkspaceSettingsManager(self)
144
116
 
145
117
 
146
118
  class _WorkspaceProxyTokenManager:
@@ -340,3 +312,71 @@ class _WorkspaceBillingManager:
340
312
  BillingReportItem._from_proto(pb_item)
341
313
  async for pb_item in self._workspace.client.stub.WorkspaceBillingReport.unary_stream(request)
342
314
  ]
315
+
316
+
317
+ class _WorkspaceSettingsManager:
318
+ """mdmd:namespace
319
+ Namespace for Workspace settings APIs.
320
+ """
321
+
322
+ @classmethod
323
+ def valid_settings(cls):
324
+ return ("default-environment", "image-builder-version")
325
+
326
+ def __init__(self, workspace: _Workspace):
327
+ """mdmd:hidden"""
328
+ self._workspace = workspace
329
+
330
+ async def list(self):
331
+ """Return a the current workspace settings.
332
+
333
+ Returns:
334
+ A `WorkspaceSettings` dataclass.
335
+ """
336
+ if not self._workspace.is_hydrated:
337
+ await self._workspace.hydrate()
338
+ resp = await self._workspace.client.stub.WorkspaceSettings(Empty())
339
+ return WorkspaceSettings(
340
+ default_environment=resp.default_environment_name, image_builder_version=resp.image_builder_version
341
+ )
342
+
343
+ async def _set_image_builder_version(self, version: str) -> None:
344
+ """mdmd:hidden
345
+ Set the image builder version for the Workspace.
346
+ """
347
+ if not self._workspace.is_hydrated:
348
+ await self._workspace.hydrate()
349
+ req = api_pb2.WorkspaceSetImageBuilderVersionRequest(new_image_builder_version=version)
350
+ await self._workspace.client.stub.WorkspaceSetImageBuilderVersion(req)
351
+
352
+ async def _set_default_environment(self, name: str) -> None:
353
+ """Set the default environment for the Workspace."""
354
+ if not self._workspace.is_hydrated:
355
+ await self._workspace.hydrate()
356
+ req = api_pb2.WorkspaceSetDefaultEnvironmentRequest(environment_name=name)
357
+ await self._workspace.client.stub.WorkspaceSetDefaultEnvironment(req)
358
+
359
+ async def set(self, name: str, value: str) -> None:
360
+ """Set a workspace setting to a new value. Must be workspace manager or owner.
361
+
362
+ The following settings can be updated:
363
+
364
+ - image-builder-version: The image builder version determines the software included in our base images.
365
+ - default-environment: The default environment when the environment is omitted from SDK or CLI methods.
366
+
367
+ Args:
368
+ name: The name of the setting.
369
+ value: The new value of the setting.
370
+
371
+ Examples:
372
+ ```python notest
373
+ modal.Workspace.from_context().settings.set("default-environment", "dev")
374
+ ```
375
+ """
376
+ match name:
377
+ case "image-builder-version":
378
+ await self._set_image_builder_version(value)
379
+ case "default-environment":
380
+ await self._set_default_environment(value)
381
+ case _:
382
+ raise ValueError(f"Unknown setting {name!r}. Valid settings: {', '.join(self.valid_settings())}")
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2025
2
- from ._billing import BillingReportItem, WorkspaceBillingReportItem, _workspace_billing_report
2
+ from ._billing import WorkspaceBillingReportItem, _workspace_billing_report
3
3
  from ._utils.async_utils import synchronize_api
4
+ from .types import BillingReportItem
4
5
 
5
6
  workspace_billing_report = synchronize_api(_workspace_billing_report, target_module=__name__)
6
7
 
@@ -2,12 +2,32 @@ import datetime
2
2
  import decimal
3
3
  import modal._billing
4
4
  import modal.client
5
+ import modal.types
5
6
  import modal_proto.api_pb2
6
7
  import typing
7
8
  import typing_extensions
8
9
 
10
+ class WorkspaceBillingReportItem(typing.TypedDict):
11
+ """dict() -> new empty dictionary
12
+ dict(mapping) -> new dictionary initialized from a mapping object's
13
+ (key, value) pairs
14
+ dict(iterable) -> new dictionary initialized as if via:
15
+ d = {}
16
+ for k, v in iterable:
17
+ d[k] = v
18
+ dict(**kwargs) -> new dictionary initialized with the name=value pairs
19
+ in the keyword argument list. For example: dict(one=1, two=2)
20
+ """
21
+
22
+ object_id: str
23
+ description: str
24
+ environment_name: str
25
+ interval_start: datetime.datetime
26
+ cost: decimal.Decimal
27
+ tags: dict[str, str]
28
+
9
29
  class BillingReportItem:
10
- """BillingReportItem(object_id: str, description: str, environment_name: str, interval_start: datetime.datetime, cost: decimal.Decimal, cost_by_resource: dict[str, decimal.Decimal], tags: dict[str, str])"""
30
+ """Costs generated by a specific object during a specific time interval."""
11
31
 
12
32
  object_id: str
13
33
  description: str
@@ -18,29 +38,27 @@ class BillingReportItem:
18
38
  tags: dict[str, str]
19
39
 
20
40
  def __getitem__(self, key: str) -> typing.Any:
21
- """mdmd:ignore"""
41
+ """mdmd:hidden"""
22
42
  ...
23
43
 
24
44
  def __setitem__(self, key: str, _: typing.Any):
25
- """mdmd:ignore"""
45
+ """mdmd:hidden"""
26
46
  ...
27
47
 
28
48
  def keys(self) -> typing.Iterable[str]:
29
- """mdmd:ignore"""
49
+ """mdmd:hidden"""
30
50
  ...
31
51
 
32
52
  def values(self) -> typing.Iterable[typing.Any]:
33
- """mdmd:ignore"""
53
+ """mdmd:hidden"""
34
54
  ...
35
55
 
36
56
  def items(self) -> typing.Iterable[tuple[str, typing.Any]]:
37
- """mdmd:ignore"""
57
+ """mdmd:hidden"""
38
58
  ...
39
59
 
40
60
  @classmethod
41
- def _from_proto(
42
- cls, pb_item: modal_proto.api_pb2.WorkspaceBillingReportItem
43
- ) -> modal._billing.BillingReportItem: ...
61
+ def _from_proto(cls, pb_item: modal_proto.api_pb2.WorkspaceBillingReportItem) -> modal.types.BillingReportItem: ...
44
62
  def __init__(
45
63
  self,
46
64
  object_id: str,
@@ -77,25 +95,6 @@ class BillingReportItem:
77
95
  def __getstate__(self): ...
78
96
  def __setstate__(self, state): ...
79
97
 
80
- class WorkspaceBillingReportItem(typing.TypedDict):
81
- """dict() -> new empty dictionary
82
- dict(mapping) -> new dictionary initialized from a mapping object's
83
- (key, value) pairs
84
- dict(iterable) -> new dictionary initialized as if via:
85
- d = {}
86
- for k, v in iterable:
87
- d[k] = v
88
- dict(**kwargs) -> new dictionary initialized with the name=value pairs
89
- in the keyword argument list. For example: dict(one=1, two=2)
90
- """
91
-
92
- object_id: str
93
- description: str
94
- environment_name: str
95
- interval_start: datetime.datetime
96
- cost: decimal.Decimal
97
- tags: dict[str, str]
98
-
99
98
  class __workspace_billing_report_spec(typing_extensions.Protocol):
100
99
  def __call__(
101
100
  self,
@@ -1,36 +1,7 @@
1
1
  # Copyright Modal Labs 2022
2
- from dataclasses import dataclass
3
- from enum import IntEnum
4
-
5
2
  from modal_proto import api_pb2
6
3
 
7
-
8
- class InputStatus(IntEnum):
9
- """Enum representing status of a function input."""
10
-
11
- PENDING = 0
12
- SUCCESS = api_pb2.GenericResult.GENERIC_STATUS_SUCCESS
13
- FAILURE = api_pb2.GenericResult.GENERIC_STATUS_FAILURE
14
- INIT_FAILURE = api_pb2.GenericResult.GENERIC_STATUS_INIT_FAILURE
15
- TERMINATED = api_pb2.GenericResult.GENERIC_STATUS_TERMINATED
16
- TIMEOUT = api_pb2.GenericResult.GENERIC_STATUS_TIMEOUT
17
-
18
- @classmethod
19
- def _missing_(cls, value):
20
- return cls.PENDING
21
-
22
-
23
- @dataclass
24
- class InputInfo:
25
- """Simple data structure storing information about a function input."""
26
-
27
- input_id: str
28
- function_call_id: str
29
- task_id: str
30
- status: InputStatus
31
- function_name: str
32
- module_name: str
33
- children: list["InputInfo"]
4
+ from .types import InputInfo, InputStatus
34
5
 
35
6
 
36
7
  def _reconstruct_call_graph(ser_graph: api_pb2.FunctionGetCallGraphResponse) -> list[InputInfo]:
@@ -13,7 +13,8 @@ from click import UsageError
13
13
  from modal._utils.async_utils import TaskContext
14
14
  from modal.config import logger
15
15
  from modal.network_file_system import _NetworkFileSystem
16
- from modal.volume import FileEntry, FileEntryType, _Volume
16
+ from modal.types import FileEntry, FileEntryType
17
+ from modal.volume import _Volume
17
18
 
18
19
  PIPE_PATH = Path("-")
19
20
 
@@ -59,7 +59,7 @@ async def list_(env: str | None = None, json: bool = False):
59
59
  display_table(column_names, rows, json=json, title=f"Active Multi-node Clusters in environment: {environment_name}")
60
60
 
61
61
 
62
- @cluster_cli.command("shell")
62
+ @cluster_cli.command("shell", no_args_is_help=True)
63
63
  @click.argument("cluster_id")
64
64
  @click.option("--rank", default=0, help="Rank of the node to shell into")
65
65
  @synchronizer.create_blocking
@@ -40,7 +40,7 @@ when running a command that requires an environment.
40
40
  """
41
41
 
42
42
 
43
- @config_cli.command("set-environment", help=SET_DEFAULT_ENV_HELP)
43
+ @config_cli.command("set-environment", help=SET_DEFAULT_ENV_HELP, no_args_is_help=True)
44
44
  @click.argument("environment_name")
45
45
  def set_environment(environment_name: str):
46
46
  # Confirm that the environment exists by looking it up
@@ -49,7 +49,7 @@ def set_environment(environment_name: str):
49
49
  click.echo(f"New default environment for profile {_profile}: {environment_name}")
50
50
 
51
51
 
52
- @config_cli.command("set", hidden=True)
52
+ @config_cli.command("set", hidden=True, no_args_is_help=True)
53
53
  @click.argument("key")
54
54
  @click.argument("value")
55
55
  def set(key: str, value: str):
@@ -317,7 +317,7 @@ async def _exec_impl(
317
317
  ).wait()
318
318
 
319
319
 
320
- @container_cli.command("exec")
320
+ @container_cli.command("exec", no_args_is_help=True)
321
321
  @click.option("--pty/--no-pty", default=None, help="Run the command using a PTY.")
322
322
  @click.argument("container_id")
323
323
  @click.argument("command", nargs=-1, required=True)
@@ -330,16 +330,27 @@ def exec(
330
330
  _exec_impl(pty=pty, container_id=container_id, command=command)
331
331
 
332
332
 
333
- @container_cli.command("stop")
333
+ @container_cli.command("stop", no_args_is_help=True)
334
334
  @click.argument("container_id")
335
+ @click.option(
336
+ "--graceful",
337
+ is_flag=True,
338
+ default=False,
339
+ help="Let the container finish its current inputs before exiting, instead of cancelling them.",
340
+ )
335
341
  @yes_option
336
342
  @synchronizer.create_blocking
337
- async def stop(container_id: str = "", *, yes: bool = False):
343
+ async def stop(container_id: str = "", *, graceful: bool = False, yes: bool = False):
338
344
  """Terminate a running container.
339
345
 
340
- This will send the container a SIGINT signal that Modal will handle.
341
- Any inputs that are currently running on the container will be cancelled and rescheduled
342
- on other containers.
346
+ By default, this will send the container a SIGINT signal that Modal will handle.
347
+ For Functions, any inputs that are currently running on the container will be cancelled
348
+ and rescheduled on other containers.
349
+
350
+ With `--graceful`, the container will be allowed to finish the inputs it is currently
351
+ running, exiting once they complete. Graceful stops are only supported for containers
352
+ running a Modal Function.
353
+
343
354
  """
344
355
  client = await _Client.from_env()
345
356
  resp = await client.stub.TaskGetInfo(api_pb2.TaskGetInfoRequest(task_id=container_id))
@@ -347,5 +358,5 @@ async def stop(container_id: str = "", *, yes: bool = False):
347
358
  raise SystemExit(f"Container '{container_id}' is already stopped.")
348
359
  if not yes:
349
360
  confirm_or_suggest_yes(f"Are you sure you want to stop container '{container_id}'?")
350
- request = api_pb2.ContainerStopRequest(task_id=container_id)
361
+ request = api_pb2.ContainerStopRequest(task_id=container_id, graceful=graceful)
351
362
  await client.stub.ContainerStop(request)
@@ -96,7 +96,12 @@ def _run_curl(curl_args: tuple[str, ...], token: str | None = None) -> None:
96
96
  sys.exit(subprocess.call(cmd))
97
97
 
98
98
 
99
- @click.command("curl", cls=ModalCommand, context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
99
+ @click.command(
100
+ "curl",
101
+ cls=ModalCommand,
102
+ no_args_is_help=True,
103
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
104
+ )
100
105
  @click.argument("curl_args", nargs=-1, required=True, type=click.UNPROCESSED)
101
106
  @synchronizer.create_blocking
102
107
  async def curl(curl_args: tuple[str, ...]):