modal 1.4.2.dev5__tar.gz → 1.4.2.dev7__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 (210) hide show
  1. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/PKG-INFO +1 -1
  2. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/grpc_utils.py +29 -5
  3. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/task_command_router_client.py +2 -1
  4. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/app.py +163 -79
  5. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/container.py +20 -2
  6. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/utils.py +10 -17
  7. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/client.pyi +2 -2
  8. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/dict.py +5 -5
  9. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/dict.pyi +8 -8
  10. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/image.py +2 -0
  11. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/image.pyi +2 -0
  12. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/queue.py +5 -5
  13. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/queue.pyi +8 -8
  14. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/sandbox_fs.py +6 -6
  15. modal-1.4.2.dev7/modal/sandbox_fs.pyi +591 -0
  16. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/secret.py +5 -5
  17. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/secret.pyi +8 -8
  18. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/volume.py +5 -5
  19. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/volume.pyi +8 -8
  20. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/PKG-INFO +1 -1
  21. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/mdmd/mdmd.py +30 -13
  22. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/api_pb2.py +656 -656
  23. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/api_pb2.pyi +18 -4
  24. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_version/__init__.py +1 -1
  25. modal-1.4.2.dev5/modal/sandbox_fs.pyi +0 -627
  26. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/LICENSE +0 -0
  27. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/README.md +0 -0
  28. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/__init__.py +0 -0
  29. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/__main__.py +0 -0
  30. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_billing.py +0 -0
  31. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_clustered_functions.py +0 -0
  32. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_clustered_functions.pyi +0 -0
  33. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_container_entrypoint.py +0 -0
  34. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_functions.py +0 -0
  35. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_grpc_client.py +0 -0
  36. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_ipython.py +0 -0
  37. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_load_context.py +0 -0
  38. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_location.py +0 -0
  39. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_logs.py +0 -0
  40. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_object.py +0 -0
  41. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_output/__init__.py +0 -0
  42. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_output/manager.py +0 -0
  43. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_output/pty.py +0 -0
  44. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_output/rich.py +0 -0
  45. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_output/status.py +0 -0
  46. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_partial_function.py +0 -0
  47. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_resolver.py +0 -0
  48. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_resources.py +0 -0
  49. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/__init__.py +0 -0
  50. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/asgi.py +0 -0
  51. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/container_io_manager.py +0 -0
  52. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/container_io_manager.pyi +0 -0
  53. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/execution_context.py +0 -0
  54. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/execution_context.pyi +0 -0
  55. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  56. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/telemetry.py +0 -0
  57. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/user_code_event_loop.py +0 -0
  58. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_runtime/user_code_imports.py +0 -0
  59. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_serialization.py +0 -0
  60. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_server.py +0 -0
  61. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_traceback.py +0 -0
  62. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_tunnel.py +0 -0
  63. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_tunnel.pyi +0 -0
  64. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_type_manager.py +0 -0
  65. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/__init__.py +0 -0
  66. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/app_utils.py +0 -0
  67. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/async_utils.py +0 -0
  68. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/auth_token_manager.py +0 -0
  69. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/blob_utils.py +0 -0
  70. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/browser_utils.py +0 -0
  71. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/bytes_io_segment_payload.py +0 -0
  72. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/deprecation.py +0 -0
  73. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/docker_utils.py +0 -0
  74. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/function_utils.py +0 -0
  75. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/git_utils.py +0 -0
  76. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/grpc_testing.py +0 -0
  77. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/hash_utils.py +0 -0
  78. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/http_utils.py +0 -0
  79. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/jwt_utils.py +0 -0
  80. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/logger.py +0 -0
  81. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/mount_utils.py +0 -0
  82. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/name_utils.py +0 -0
  83. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/package_utils.py +0 -0
  84. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/pattern_utils.py +0 -0
  85. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/rand_pb_testing.py +0 -0
  86. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/sandbox_fs_utils.py +0 -0
  87. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/shell_utils.py +0 -0
  88. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_utils/time_utils.py +0 -0
  89. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_vendor/__init__.py +0 -0
  90. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  91. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_vendor/cloudpickle.py +0 -0
  92. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_vendor/tblib.py +0 -0
  93. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_vendor/version.py +0 -0
  94. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/_watcher.py +0 -0
  95. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/app.py +0 -0
  96. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/app.pyi +0 -0
  97. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/billing.py +0 -0
  98. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/2023.12.312.txt +0 -0
  99. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/2023.12.txt +0 -0
  100. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/2024.04.txt +0 -0
  101. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/2024.10.txt +0 -0
  102. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/2025.06.txt +0 -0
  103. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/PREVIEW.txt +0 -0
  104. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/README.md +0 -0
  105. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/builder/base-images.json +0 -0
  106. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/call_graph.py +0 -0
  107. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/__init__.py +0 -0
  108. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/_download.py +0 -0
  109. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/_traceback.py +0 -0
  110. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/billing.py +0 -0
  111. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/changelog.py +0 -0
  112. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/cluster.py +0 -0
  113. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/config.py +0 -0
  114. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/dashboard.py +0 -0
  115. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/dict.py +0 -0
  116. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/entry_point.py +0 -0
  117. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/environment.py +0 -0
  118. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/import_refs.py +0 -0
  119. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/launch.py +0 -0
  120. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/network_file_system.py +0 -0
  121. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/profile.py +0 -0
  122. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/programs/__init__.py +0 -0
  123. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/programs/run_jupyter.py +0 -0
  124. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/programs/vscode.py +0 -0
  125. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/queues.py +0 -0
  126. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/run.py +0 -0
  127. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/secret.py +0 -0
  128. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/selector.py +0 -0
  129. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/shell.py +0 -0
  130. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/token.py +0 -0
  131. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cli/volume.py +0 -0
  132. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/client.py +0 -0
  133. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cloud_bucket_mount.py +0 -0
  134. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cloud_bucket_mount.pyi +0 -0
  135. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cls.py +0 -0
  136. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/cls.pyi +0 -0
  137. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/config.py +0 -0
  138. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/container_process.py +0 -0
  139. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/container_process.pyi +0 -0
  140. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/environments.py +0 -0
  141. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/environments.pyi +0 -0
  142. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/exception.py +0 -0
  143. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/experimental/__init__.py +0 -0
  144. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/experimental/flash.py +0 -0
  145. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/experimental/flash.pyi +0 -0
  146. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/experimental/ipython.py +0 -0
  147. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/file_io.py +0 -0
  148. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/file_io.pyi +0 -0
  149. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/file_pattern_matcher.py +0 -0
  150. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/functions.py +0 -0
  151. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/functions.pyi +0 -0
  152. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/io_streams.py +0 -0
  153. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/io_streams.pyi +0 -0
  154. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/mount.py +0 -0
  155. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/mount.pyi +0 -0
  156. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/network_file_system.py +0 -0
  157. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/network_file_system.pyi +0 -0
  158. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/object.py +0 -0
  159. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/object.pyi +0 -0
  160. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/output.py +0 -0
  161. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/parallel_map.py +0 -0
  162. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/parallel_map.pyi +0 -0
  163. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/partial_function.py +0 -0
  164. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/partial_function.pyi +0 -0
  165. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/proxy.py +0 -0
  166. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/proxy.pyi +0 -0
  167. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/py.typed +0 -0
  168. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/retries.py +0 -0
  169. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/runner.py +0 -0
  170. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/runner.pyi +0 -0
  171. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/running_app.py +0 -0
  172. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/sandbox.py +0 -0
  173. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/sandbox.pyi +0 -0
  174. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/schedule.py +0 -0
  175. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/scheduler_placement.py +0 -0
  176. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/server.py +0 -0
  177. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/server.pyi +0 -0
  178. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/serving.py +0 -0
  179. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/serving.pyi +0 -0
  180. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/snapshot.py +0 -0
  181. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/snapshot.pyi +0 -0
  182. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/stream_type.py +0 -0
  183. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/token_flow.py +0 -0
  184. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal/token_flow.pyi +0 -0
  185. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/SOURCES.txt +0 -0
  186. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/dependency_links.txt +0 -0
  187. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/entry_points.txt +0 -0
  188. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/requires.txt +0 -0
  189. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal.egg-info/top_level.txt +0 -0
  190. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/__init__.py +0 -0
  191. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/gen_cli_docs.py +0 -0
  192. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/gen_cli_docs_main.py +0 -0
  193. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/gen_reference_docs.py +0 -0
  194. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/gen_reference_docs_main.py +0 -0
  195. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/mdmd/__init__.py +0 -0
  196. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_docs/mdmd/signatures.py +0 -0
  197. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/__init__.py +0 -0
  198. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/api_grpc.py +0 -0
  199. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/api_pb2_grpc.py +0 -0
  200. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/api_pb2_grpc.pyi +0 -0
  201. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/modal_api_grpc.py +0 -0
  202. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/py.typed +0 -0
  203. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/task_command_router_grpc.py +0 -0
  204. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/task_command_router_pb2.py +0 -0
  205. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/task_command_router_pb2.pyi +0 -0
  206. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  207. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  208. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/modal_version/__main__.py +0 -0
  209. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/pyproject.toml +0 -0
  210. {modal-1.4.2.dev5 → modal-1.4.2.dev7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.4.2.dev5
3
+ Version: 1.4.2.dev7
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -216,7 +216,11 @@ def create_channel(
216
216
  for k, v in metadata.items():
217
217
  event.metadata[k] = v
218
218
 
219
- logger.debug(f"Sending request to {event.method_name}")
219
+ idempotency_key = typing.cast(Optional[str], event.metadata.get("x-idempotency-key"))
220
+ if idempotency_key is None:
221
+ logger.debug(f"Sending request to {event.method_name}")
222
+ else:
223
+ logger.debug(f"Sending request to {event.method_name} ({idempotency_key[:8]})")
220
224
 
221
225
  grpclib.events.listen(channel, grpclib.events.SendRequest, send_request)
222
226
 
@@ -288,12 +292,14 @@ def process_exception_before_retry(
288
292
  n_retries: int,
289
293
  delay: float,
290
294
  idempotency_key: str,
295
+ rpc_elapsed: float,
291
296
  ):
292
297
  """Process exception before retry, used by `_retry_transient_errors`."""
293
298
  with suppress_tb_frame():
294
299
  if final_attempt:
295
300
  logger.debug(
296
- f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})"
301
+ f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} {rpc_elapsed=:0.2f}s "
302
+ f"for {fn_name} ({idempotency_key[:8]})"
297
303
  )
298
304
  if isinstance(exc, OSError):
299
305
  raise ConnectionError(str(exc))
@@ -310,7 +316,10 @@ def process_exception_before_retry(
310
316
  # we handle in the retry logic once we drop this check!
311
317
  raise exc
312
318
 
313
- logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})")
319
+ logger.debug(
320
+ f"Retryable failure {repr(exc)} {n_retries=} {delay=} {rpc_elapsed=:0.2f}s "
321
+ f"for {fn_name} ({idempotency_key[:8]})"
322
+ )
314
323
 
315
324
 
316
325
  async def _retry_transient_errors(
@@ -373,6 +382,7 @@ async def _retry_transient_errors(
373
382
  else:
374
383
  timeout = None
375
384
 
385
+ attempt_started_at = time.monotonic()
376
386
  try:
377
387
  with suppress_tb_frame():
378
388
  return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
@@ -409,7 +419,13 @@ async def _retry_transient_errors(
409
419
 
410
420
  with suppress_tb_frame():
411
421
  process_exception_before_retry(
412
- exc, final_attempt, fn.name, n_retries, server_delay, idempotency_key
422
+ exc,
423
+ final_attempt,
424
+ fn.name,
425
+ n_retries,
426
+ server_delay,
427
+ idempotency_key,
428
+ time.monotonic() - attempt_started_at,
413
429
  )
414
430
 
415
431
  now = time.time()
@@ -438,7 +454,15 @@ async def _retry_transient_errors(
438
454
  final_attempt = False
439
455
 
440
456
  with suppress_tb_frame():
441
- process_exception_before_retry(exc, final_attempt, fn.name, n_retries, delay, idempotency_key)
457
+ process_exception_before_retry(
458
+ exc,
459
+ final_attempt,
460
+ fn.name,
461
+ n_retries,
462
+ delay,
463
+ idempotency_key,
464
+ time.monotonic() - attempt_started_at,
465
+ )
442
466
 
443
467
  n_retries += 1
444
468
 
@@ -502,7 +502,8 @@ class TaskCommandRouterClient:
502
502
  jwt, url = v1_resp.jwt, v1_resp.url
503
503
 
504
504
  # Ensure the server URL remains stable for the lifetime of this client.
505
- assert url == self._server_url, "Task router URL changed during session"
505
+ if url != self._server_url:
506
+ logger.warning("Task router URL changed during session")
506
507
  self._jwt = jwt
507
508
  self._jwt_exp = _parse_jwt_expiration(jwt)
508
509
 
@@ -20,13 +20,22 @@ from modal._utils.async_utils import synchronizer
20
20
  from modal._utils.browser_utils import open_url_and_display
21
21
  from modal.client import _Client
22
22
  from modal.environments import ensure_env
23
+ from modal.exception import InvalidError, NotFoundError
23
24
  from modal.output import OutputManager
24
25
  from modal.runner import DEPLOYMENT_STRATEGY_TYPE, _stop_and_wait_for_containers
25
26
  from modal_proto import api_pb2
26
27
 
27
28
  from .._logs import _FETCH_LIMIT, _MAX_FETCH_RANGE, LogsFilters
28
29
  from .._utils.time_utils import locale_tz, timestamp_to_localized_str
29
- from .utils import ENV_OPTION, display_table, fetch_app_logs, get_app_id_from_name, stream_app_logs, tail_app_logs
30
+ from .utils import (
31
+ ENV_OPTION,
32
+ YES_OPTION,
33
+ confirm_or_suggest_yes,
34
+ display_table,
35
+ fetch_app_logs,
36
+ stream_app_logs,
37
+ tail_app_logs,
38
+ )
30
39
 
31
40
  APP_IDENTIFIER = Argument("", help="App name or ID")
32
41
  NAME_OPTION = typer.Option("", "-n", "--name", help="Deprecated: Pass App name as a positional argument")
@@ -45,12 +54,50 @@ APP_STATE_TO_MESSAGE = {
45
54
  }
46
55
 
47
56
 
48
- @synchronizer.create_blocking
49
- async def get_app_id(app_identifier: str, env: Optional[str], client: Optional[_Client] = None) -> str:
50
- """Resolve an app_identifier that may be a name or an ID into an ID."""
57
+ async def resolve_app_identifier(
58
+ app_identifier: str, env: Optional[str], client: Optional[_Client] = None
59
+ ) -> tuple[str, str, api_pb2.AppLifecycle]: # Return app_id, environment_name, lifecycle
60
+ """Handle an App ID or an App name and return context about the App it points at.
61
+
62
+ When a name is provided, we may retrieve either a currently deployed App or an App that
63
+ was recently stopped (if no other App with that name has been deployed since).
64
+ It is up to callers of this function to decide whether it's valid to use the App ID
65
+ based on the lifecycle returned and their specific operations.
66
+
67
+ Can also raise a NotFoundError if the argument matches the App ID regex but the App
68
+ doesn't exist on the backend, or if there is no currently deployed or recently stopped App
69
+ with that name.
70
+
71
+ The function also always returns a valid environment name for any name-based lookups,
72
+ which may reflect the server-defined default environment when the provided argument was null.
73
+
74
+ """
75
+ if client is None:
76
+ client = await _Client.from_env()
51
77
  if re.match(r"^ap-[a-zA-Z0-9]{22}$", app_identifier):
52
- return app_identifier
53
- return await get_app_id_from_name.aio(app_identifier, env, client)
78
+ # Identifier is an App ID. This is unambiguous, so we can make the request and return
79
+ # the lifecycle. AppGetLifecycle will raise NotFoundError if the ID doesn't point at an App.
80
+ # If we return, it's a real App, but it's up to the caller to decide what to do based on
81
+ # the App's current state as conveyed by the lifecycle. We do propagate a NotFoundError
82
+ # from the server if the App ID doesn't actually exist.
83
+ request = api_pb2.AppGetLifecycleRequest(app_id=app_identifier)
84
+ resp = await client.stub.AppGetLifecycle(request)
85
+ return app_identifier, "", resp.lifecycle
86
+ else:
87
+ # Identifier is treated as a name, which may or may not point at a currently deployed App
88
+ # (inside a specific environment)
89
+ request = api_pb2.AppGetByDeploymentNameRequest(name=app_identifier, environment_name=env or "")
90
+ resp = await client.stub.AppGetByDeploymentName(request)
91
+ if resp.app_id:
92
+ # App is currently deployed
93
+ return resp.app_id, resp.environment_name, resp.lifecycle
94
+ elif resp.previous_app_id:
95
+ # An App with this name was recently stopped. Return the ID of the stopped App
96
+ # and let callers decide what to do based on the lifecycle.
97
+ return resp.previous_app_id, resp.environment_name, resp.lifecycle
98
+ else:
99
+ msg = f"No App with name '{app_identifier}' found in the '{resp.environment_name}' environment."
100
+ raise NotFoundError(msg)
54
101
 
55
102
 
56
103
  @app_cli.command("list")
@@ -129,7 +176,8 @@ _SOURCE_OPTIONS = {
129
176
 
130
177
 
131
178
  @app_cli.command("logs", no_args_is_help=True)
132
- def logs(
179
+ @synchronizer.create_blocking
180
+ async def logs(
133
181
  app_identifier: str = APP_IDENTIFIER,
134
182
  follow: bool = typer.Option(False, "-f", "--follow", help="Stream log output until App stops"),
135
183
  since: Optional[str] = typer.Option(
@@ -229,7 +277,7 @@ def logs(
229
277
  if tail is not None and tail > _FETCH_LIMIT:
230
278
  raise UsageError(f"--tail value must not exceed {_FETCH_LIMIT}.")
231
279
 
232
- app_id = get_app_id(app_identifier, env)
280
+ app_id, _, _ = await resolve_app_identifier(app_identifier, env)
233
281
 
234
282
  if source is not None:
235
283
  if source not in _SOURCE_OPTIONS:
@@ -255,7 +303,7 @@ def logs(
255
303
  )
256
304
 
257
305
  if follow:
258
- stream_app_logs(
306
+ await stream_app_logs.aio(
259
307
  app_id,
260
308
  task_id=container_id or "",
261
309
  show_timestamps=timestamps,
@@ -278,7 +326,7 @@ def logs(
278
326
 
279
327
  if since and tail is None:
280
328
  # Range mode: --since without --tail fetches everything in the range.
281
- fetch_app_logs(
329
+ await fetch_app_logs.aio(
282
330
  app_id,
283
331
  since_dt,
284
332
  until_dt or now,
@@ -290,7 +338,7 @@ def logs(
290
338
  # Tail mode: single fetch with limit.
291
339
  # --since is a hard floor, --until shifts the anchor.
292
340
  effective_tail = tail if tail is not None else _DEFAULT_LOGS_TAIL
293
- tail_app_logs(
341
+ await tail_app_logs.aio(
294
342
  app_id,
295
343
  effective_tail,
296
344
  show_timestamps=timestamps,
@@ -338,29 +386,125 @@ async def rollback(
338
386
  """
339
387
  env = ensure_env(env)
340
388
  client = await _Client.from_env()
341
- app_id = await get_app_id.aio(app_identifier, env, client)
389
+ app_id, environment_name, lifecycle = await resolve_app_identifier(app_identifier, env, client)
390
+ if lifecycle.app_state != api_pb2.APP_STATE_DEPLOYED:
391
+ env_suffix = f" in the '{environment_name}' environment" if environment_name else ""
392
+ raise InvalidError(f"App '{app_identifier}' is not deployed{env_suffix}.")
393
+
342
394
  if not version:
343
395
  version_number = -1
344
396
  else:
345
397
  if m := re.match(r"v(\d+)", version):
346
398
  version_number = int(m.group(1))
347
399
  else:
348
- raise UsageError(f"Invalid version specifer: {version}")
400
+ raise UsageError(f"Invalid version specifier: {version}")
349
401
  req = api_pb2.AppRollbackRequest(app_id=app_id, version=version_number)
350
402
  await client.stub.AppRollback(req)
351
403
  rich.print("[green]✓[/green] Deployment rollback successful!")
352
404
 
353
405
 
406
+ @app_cli.command("rollover", no_args_is_help=True)
407
+ @synchronizer.create_blocking
408
+ async def rollover(
409
+ app_identifier: str = APP_IDENTIFIER,
410
+ *,
411
+ strategy: str = typer.Option(
412
+ "rolling",
413
+ help="Strategy for rollover",
414
+ click_type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE)),
415
+ ),
416
+ env: Optional[str] = ENV_OPTION,
417
+ ):
418
+ """Redeploy an App to get new containers without code changes.
419
+
420
+ A rollover replaces existing containers with fresh ones built from the same
421
+ App version — useful for refreshing containers without changing your code.
422
+ The rollover appears as a new entry in the App's deployment history.
423
+
424
+ **Examples:**
425
+
426
+ Rollover an App using a rolling deployment. Running containers are now considered
427
+ outdated and will be gracefully replaced by new ones.
428
+
429
+ ```
430
+ modal app rollover my-app
431
+ ```
432
+
433
+ Rollover an App by terminating any running containers. Inputs on the queue will
434
+ start new containers.
435
+
436
+ ```
437
+ modal app rollover my-app --strategy recreate
438
+ ```
439
+ """
440
+ env = ensure_env(env)
441
+ client = await _Client.from_env()
442
+
443
+ app_id, environment_name, lifecycle = await resolve_app_identifier(app_identifier, env, client)
444
+ if lifecycle.app_state != api_pb2.APP_STATE_DEPLOYED:
445
+ env_suffix = f" in the '{environment_name}' environment" if environment_name else ""
446
+ raise InvalidError(f"App '{app_identifier}' is not deployed{env_suffix}.")
447
+
448
+ output_mgr = OutputManager.get()
449
+ output_mgr.print(f"🔨 Starting app rollover with {strategy} strategy")
450
+ t0 = time.monotonic()
451
+
452
+ req = api_pb2.AppRolloverRequest(app_id=app_id)
453
+ response = await client.stub.AppRollover(req)
454
+ print_server_warnings(response.server_warnings)
455
+
456
+ if strategy == "recreate":
457
+ try:
458
+ await _stop_and_wait_for_containers(client, app_id, response.deployed_at, env)
459
+ except Exception as exc:
460
+ warnings.warn(f"App updated successfully, but containers did not all terminate. {exc}", UserWarning)
461
+ output_mgr.print(f"\nView Deployment: [magenta]{response.url}[/magenta]")
462
+ sys.exit(1)
463
+
464
+ duration = time.monotonic() - t0
465
+ output_mgr.step_completed(f"Rollover completed in {duration:.3f}s with {strategy} strategy! 🎉")
466
+ output_mgr.print(f"\nView Deployment: [magenta]{response.url}[/magenta]")
467
+
468
+
354
469
  @app_cli.command("stop", no_args_is_help=True)
355
470
  @synchronizer.create_blocking
356
471
  async def stop(
357
472
  app_identifier: str = APP_IDENTIFIER,
358
473
  *,
474
+ yes: bool = YES_OPTION,
359
475
  env: Optional[str] = ENV_OPTION,
360
476
  ):
361
- """Stop an app."""
477
+ """Permanently stop an App and terminate its running containers."""
478
+ env = ensure_env(env)
362
479
  client = await _Client.from_env()
363
- app_id = await get_app_id.aio(app_identifier, env)
480
+ app_id, environment_name, lifecycle = await resolve_app_identifier(app_identifier, env, client)
481
+
482
+ if lifecycle.app_state == api_pb2.APP_STATE_STOPPED:
483
+ msg = "App is already stopped."
484
+ if lifecycle.stopped_at:
485
+ stopped_at = timestamp_to_localized_str(lifecycle.stopped_at)
486
+ verb = "Stopped" if lifecycle.stopped_by else "Finished"
487
+ attribution = f" by '{lifecycle.stopped_by}'" if lifecycle.stopped_by else ""
488
+ msg += f" ({verb} at {stopped_at}{attribution})."
489
+ raise SystemExit(msg)
490
+
491
+ if not yes:
492
+ res = await client.stub.TaskList(api_pb2.TaskListRequest(app_id=app_id))
493
+ num_containers = len(res.tasks)
494
+
495
+ if environment_name:
496
+ msg = f"Are you sure you want to stop App '{app_identifier}' in the '{environment_name}' environment?"
497
+ else:
498
+ msg = f"Are you sure you want to stop App '{app_identifier}'?"
499
+
500
+ if num_containers:
501
+ msg += (
502
+ f" This will immediately terminate {num_containers} running"
503
+ f" container{'s' if num_containers != 1 else ''}."
504
+ )
505
+ else:
506
+ msg += " No containers are currently running."
507
+ confirm_or_suggest_yes(msg)
364
508
  req = api_pb2.AppStopRequest(app_id=app_id, source=api_pb2.APP_STOP_SOURCE_CLI)
365
509
  await client.stub.AppStop(req)
366
510
 
@@ -373,7 +517,7 @@ async def history(
373
517
  env: Optional[str] = ENV_OPTION,
374
518
  json: bool = False,
375
519
  ):
376
- """Show App deployment history, for a currently deployed app
520
+ """Show an App's deployment history.
377
521
 
378
522
  **Examples:**
379
523
 
@@ -383,7 +527,7 @@ async def history(
383
527
  modal app history ap-123456
384
528
  ```
385
529
 
386
- Get the history for a currently deployed App based on its name:
530
+ Get the history for an App based on its name:
387
531
 
388
532
  ```
389
533
  modal app history my-app
@@ -392,7 +536,7 @@ async def history(
392
536
  """
393
537
  env = ensure_env(env)
394
538
  client = await _Client.from_env()
395
- app_id = await get_app_id.aio(app_identifier, env, client)
539
+ app_id, _, _ = await resolve_app_identifier(app_identifier, env, client)
396
540
  resp = await client.stub.AppDeploymentHistory(api_pb2.AppDeploymentHistoryRequest(app_id=app_id))
397
541
 
398
542
  columns = [
@@ -467,66 +611,6 @@ async def dashboard(
467
611
  ```
468
612
  """
469
613
  client = await _Client.from_env()
470
- app_id = await get_app_id.aio(app_identifier, env, client)
471
-
614
+ app_id, _, _ = await resolve_app_identifier(app_identifier, env, client)
472
615
  url = f"https://modal.com/id/{app_id}"
473
616
  open_url_and_display(url, "App dashboard")
474
-
475
-
476
- @app_cli.command("rollover", no_args_is_help=True, context_settings={"ignore_unknown_options": True})
477
- @synchronizer.create_blocking
478
- async def rollover(
479
- app_identifier: str = APP_IDENTIFIER,
480
- *,
481
- strategy: str = typer.Option(
482
- "rolling",
483
- help="Strategy for rollover",
484
- click_type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE)),
485
- ),
486
- env: Optional[str] = ENV_OPTION,
487
- ):
488
- """Rollover an App.
489
-
490
- A rollover replaces existing containers with fresh ones built from the same
491
- App version — useful for refreshing containers without changing your code.
492
- The rollover appears as a new entry in the App's deployment history.
493
-
494
- **Examples:**
495
-
496
- Rollover an App using a rolling deployment. Running containers are now considered
497
- outdated and new containers will replace them.
498
-
499
- ```
500
- modal app rollover my-app
501
- ```
502
-
503
- Rollover an App by termatining all running containers. Inputs on the queue will
504
- start new containers.
505
-
506
- ```
507
- modal app rollover my-app --strategy recreate
508
- ```
509
- """
510
- env = ensure_env(env)
511
- output_mgr = OutputManager.get()
512
- output_mgr.print(f"🔨 Starting app rollover with {strategy} strategy")
513
- t0 = time.monotonic()
514
-
515
- client = await _Client.from_env()
516
- app_id = await get_app_id.aio(app_identifier, env, client)
517
-
518
- req = api_pb2.AppRolloverRequest(app_id=app_id)
519
- response = await client.stub.AppRollover(req)
520
- print_server_warnings(response.server_warnings)
521
-
522
- if strategy == "recreate":
523
- try:
524
- await _stop_and_wait_for_containers(client, app_id, response.deployed_at, env)
525
- except Exception as exc:
526
- warnings.warn(f"App updated successfully, but containers did not all terminate. {exc}", UserWarning)
527
- output_mgr.print(f"\nView Deployment: [magenta]{response.url}[/magenta]")
528
- sys.exit(1)
529
-
530
- duration = time.monotonic() - t0
531
- output_mgr.step_completed(f"Rollover completed in {duration:.3f}s with {strategy} strategy! 🎉")
532
- output_mgr.print(f"\nView Deployment: [magenta]{response.url}[/magenta]")
@@ -14,7 +14,16 @@ from modal._output.pty import get_pty_info
14
14
  from modal._utils.async_utils import synchronizer
15
15
  from modal._utils.time_utils import timestamp_to_localized_str
16
16
  from modal.cli.app import _DEFAULT_LOGS_TAIL, _SOURCE_OPTIONS, _parse_time_arg
17
- from modal.cli.utils import ENV_OPTION, display_table, fetch_app_logs, is_tty, stream_app_logs, tail_app_logs
17
+ from modal.cli.utils import (
18
+ ENV_OPTION,
19
+ YES_OPTION,
20
+ confirm_or_suggest_yes,
21
+ display_table,
22
+ fetch_app_logs,
23
+ is_tty,
24
+ stream_app_logs,
25
+ tail_app_logs,
26
+ )
18
27
  from modal.client import _Client
19
28
  from modal.config import config
20
29
  from modal.container_process import _ContainerProcess
@@ -285,11 +294,20 @@ async def exec(
285
294
 
286
295
  @container_cli.command("stop")
287
296
  @synchronizer.create_blocking
288
- async def stop(container_id: str = typer.Argument(help="Container ID")):
297
+ async def stop(
298
+ container_id: str = typer.Argument(help="Container ID"),
299
+ *,
300
+ yes: bool = YES_OPTION,
301
+ ):
289
302
  """Stop a currently-running container and reassign its in-progress inputs.
290
303
 
291
304
  This will send the container a SIGINT signal that Modal will handle.
292
305
  """
293
306
  client = await _Client.from_env()
307
+ resp = await client.stub.TaskGetInfo(api_pb2.TaskGetInfoRequest(task_id=container_id))
308
+ if resp.info.finished_at:
309
+ raise SystemExit(f"Container '{container_id}' is already stopped.")
310
+ if not yes:
311
+ confirm_or_suggest_yes(f"Are you sure you want to stop container '{container_id}'?")
294
312
  request = api_pb2.ContainerStopRequest(task_id=container_id)
295
313
  await client.stub.ContainerStop(request)
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import asyncio
3
3
  import io
4
+ import sys
4
5
  from collections.abc import Sequence
5
6
  from contextlib import nullcontext
6
7
  from csv import writer as csv_writer
@@ -12,14 +13,11 @@ import typer
12
13
  from rich.table import Column, Table
13
14
  from rich.text import Text
14
15
 
15
- from modal_proto import api_pb2
16
-
17
16
  from .._logs import LogsFilters, fetch_logs, tail_logs
18
17
  from .._output.pty import _build_log_prefix, get_app_logs_loop
19
18
  from .._utils.async_utils import synchronizer
20
19
  from ..client import _Client
21
- from ..environments import ensure_env
22
- from ..exception import InvalidError, NotFoundError
20
+ from ..exception import InvalidError
23
21
  from ..output import OutputManager
24
22
 
25
23
 
@@ -125,19 +123,6 @@ async def fetch_app_logs(
125
123
  await _drain_batches(output_mgr, batches, prefix_fields or [], filters.search_text)
126
124
 
127
125
 
128
- @synchronizer.create_blocking
129
- async def get_app_id_from_name(name: str, env: Optional[str], client: Optional[_Client] = None) -> str:
130
- if client is None:
131
- client = await _Client.from_env()
132
- env_name = ensure_env(env)
133
- request = api_pb2.AppGetByDeploymentNameRequest(name=name, environment_name=env_name)
134
- resp = await client.stub.AppGetByDeploymentName(request)
135
- if not resp.app_id:
136
- env_comment = f" in the '{env_name}' environment" if env_name else ""
137
- raise NotFoundError(f"Could not find a deployed app named '{name}'{env_comment}.")
138
- return resp.app_id
139
-
140
-
141
126
  def _plain(text: Union[Text, str]) -> str:
142
127
  return text.plain if isinstance(text, Text) else text
143
128
 
@@ -185,3 +170,11 @@ Otherwise, raises an error if the workspace has multiple environments.
185
170
  ENV_OPTION = typer.Option(None, "-e", "--env", help=ENV_OPTION_HELP)
186
171
 
187
172
  YES_OPTION = typer.Option(False, "-y", "--yes", help="Run without pausing for confirmation.")
173
+
174
+
175
+ def confirm_or_suggest_yes(msg: str) -> None:
176
+ """Prompt for confirmation, or abort with a hint to use --yes if stdin is not a TTY."""
177
+ if not sys.stdin.isatty():
178
+ typer.echo(f"{msg} [y/N]: ")
179
+ raise SystemExit("Aborted: no interactive terminal detected. Rerun with --yes (-y) to skip confirmation.")
180
+ typer.confirm(msg, default=False, abort=True)
@@ -35,7 +35,7 @@ class _Client:
35
35
  server_url: str,
36
36
  client_type: int,
37
37
  credentials: typing.Optional[tuple[str, str]],
38
- version: str = "1.4.2.dev5",
38
+ version: str = "1.4.2.dev7",
39
39
  ):
40
40
  """mdmd:hidden
41
41
  The Modal client object is not intended to be instantiated directly by users.
@@ -171,7 +171,7 @@ class Client:
171
171
  server_url: str,
172
172
  client_type: int,
173
173
  credentials: typing.Optional[tuple[str, str]],
174
- version: str = "1.4.2.dev5",
174
+ version: str = "1.4.2.dev7",
175
175
  ):
176
176
  """mdmd:hidden
177
177
  The Modal client object is not intended to be instantiated directly by users.
@@ -84,8 +84,8 @@ class DictInfo:
84
84
  class _DictManager:
85
85
  """Namespace with methods for managing named Dict objects."""
86
86
 
87
- @staticmethod
88
87
  async def create(
88
+ self,
89
89
  name: str, # Name to use for the new Dict
90
90
  *,
91
91
  allow_existing: bool = False, # If True, no-op when the Dict already exists
@@ -137,8 +137,8 @@ class _DictManager:
137
137
  if not allow_existing:
138
138
  raise
139
139
 
140
- @staticmethod
141
140
  async def list(
141
+ self,
142
142
  *,
143
143
  max_objects: Optional[int] = None, # Limit results to this size
144
144
  created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
@@ -205,8 +205,8 @@ class _DictManager:
205
205
  ]
206
206
  return dicts[:max_objects] if max_objects is not None else dicts
207
207
 
208
- @staticmethod
209
208
  async def delete(
209
+ self,
210
210
  name: str, # Name of the Dict to delete
211
211
  *,
212
212
  allow_missing: bool = False, # If True, don't raise an error if the Dict doesn't exist
@@ -299,8 +299,8 @@ class _Dict(_Object, type_prefix="di"):
299
299
 
300
300
  @classproperty
301
301
  @classmethod
302
- def objects(cls) -> type[_DictManager]:
303
- return _DictManager
302
+ def objects(cls) -> _DictManager:
303
+ return _DictManager()
304
304
 
305
305
  @property
306
306
  def name(self) -> Optional[str]:
@@ -44,8 +44,8 @@ class DictInfo:
44
44
 
45
45
  class _DictManager:
46
46
  """Namespace with methods for managing named Dict objects."""
47
- @staticmethod
48
47
  async def create(
48
+ self,
49
49
  name: str,
50
50
  *,
51
51
  allow_existing: bool = False,
@@ -80,8 +80,8 @@ class _DictManager:
80
80
  """
81
81
  ...
82
82
 
83
- @staticmethod
84
83
  async def list(
84
+ self,
85
85
  *,
86
86
  max_objects: typing.Optional[int] = None,
87
87
  created_before: typing.Union[datetime.datetime, str, None] = None,
@@ -114,8 +114,8 @@ class _DictManager:
114
114
  """
115
115
  ...
116
116
 
117
- @staticmethod
118
117
  async def delete(
118
+ self,
119
119
  name: str,
120
120
  *,
121
121
  allow_missing: bool = False,
@@ -224,7 +224,7 @@ class DictManager:
224
224
  """
225
225
  ...
226
226
 
227
- create: typing.ClassVar[__create_spec]
227
+ create: __create_spec
228
228
 
229
229
  class __list_spec(typing_extensions.Protocol):
230
230
  def __call__(
@@ -297,7 +297,7 @@ class DictManager:
297
297
  """
298
298
  ...
299
299
 
300
- list: typing.ClassVar[__list_spec]
300
+ list: __list_spec
301
301
 
302
302
  class __delete_spec(typing_extensions.Protocol):
303
303
  def __call__(
@@ -360,7 +360,7 @@ class DictManager:
360
360
  """
361
361
  ...
362
362
 
363
- delete: typing.ClassVar[__delete_spec]
363
+ delete: __delete_spec
364
364
 
365
365
  class _Dict(modal._object._Object):
366
366
  """Distributed dictionary for storage in Modal apps.
@@ -413,7 +413,7 @@ class _Dict(modal._object._Object):
413
413
 
414
414
  @synchronicity.classproperty
415
415
  @classmethod
416
- def objects(cls) -> type[_DictManager]: ...
416
+ def objects(cls) -> _DictManager: ...
417
417
  @property
418
418
  def name(self) -> typing.Optional[str]: ...
419
419
  def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
@@ -655,7 +655,7 @@ class Dict(modal.object.Object):
655
655
 
656
656
  @synchronicity.classproperty
657
657
  @classmethod
658
- def objects(cls) -> type[DictManager]: ...
658
+ def objects(cls) -> DictManager: ...
659
659
  @property
660
660
  def name(self) -> typing.Optional[str]: ...
661
661
  def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...