modal 1.4.3.dev2__tar.gz → 1.4.3.dev4__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 (213) hide show
  1. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/PKG-INFO +1 -2
  2. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/blob_utils.py +73 -6
  3. modal-1.4.3.dev4/modal/cli/_help.py +264 -0
  4. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/_traceback.py +1 -2
  5. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/app.py +74 -58
  6. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/billing.py +27 -21
  7. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/bootstrap.py +15 -11
  8. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/changelog.py +21 -18
  9. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/cluster.py +12 -11
  10. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/config.py +13 -8
  11. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/container.py +57 -43
  12. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/dashboard.py +6 -2
  13. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/dict.py +59 -24
  14. modal-1.4.3.dev4/modal/cli/entry_point.py +132 -0
  15. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/environment.py +26 -19
  16. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/launch.py +39 -15
  17. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/network_file_system.py +56 -45
  18. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/profile.py +12 -8
  19. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/queues.py +62 -40
  20. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/run.py +72 -35
  21. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/secret.py +38 -16
  22. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/shell.py +98 -99
  23. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/token.py +27 -31
  24. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/utils.py +12 -5
  25. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/volume.py +89 -80
  26. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/client.pyi +2 -2
  27. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/functions.pyi +6 -6
  28. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/mount.py +37 -27
  29. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/volume.py +3 -0
  30. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/PKG-INFO +1 -2
  31. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/SOURCES.txt +1 -0
  32. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/requires.txt +0 -1
  33. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/gen_cli_docs.py +1 -1
  34. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_version/__init__.py +1 -1
  35. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/pyproject.toml +0 -1
  36. modal-1.4.3.dev2/modal/cli/entry_point.py +0 -145
  37. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/LICENSE +0 -0
  38. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/README.md +0 -0
  39. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/__init__.py +0 -0
  40. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/__main__.py +0 -0
  41. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_billing.py +0 -0
  42. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_clustered_functions.py +0 -0
  43. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_clustered_functions.pyi +0 -0
  44. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_container_entrypoint.py +0 -0
  45. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_functions.py +0 -0
  46. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_grpc_client.py +0 -0
  47. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_ipython.py +0 -0
  48. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_load_context.py +0 -0
  49. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_location.py +0 -0
  50. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_logs.py +0 -0
  51. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_object.py +0 -0
  52. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_output/__init__.py +0 -0
  53. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_output/manager.py +0 -0
  54. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_output/pty.py +0 -0
  55. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_output/rich.py +0 -0
  56. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_output/status.py +0 -0
  57. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_partial_function.py +0 -0
  58. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_resolver.py +0 -0
  59. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_resources.py +0 -0
  60. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/__init__.py +0 -0
  61. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/asgi.py +0 -0
  62. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/container_io_manager.py +0 -0
  63. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/container_io_manager.pyi +0 -0
  64. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/execution_context.py +0 -0
  65. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/execution_context.pyi +0 -0
  66. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  67. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/telemetry.py +0 -0
  68. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/user_code_event_loop.py +0 -0
  69. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_runtime/user_code_imports.py +0 -0
  70. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_serialization.py +0 -0
  71. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_server.py +0 -0
  72. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_traceback.py +0 -0
  73. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_tunnel.py +0 -0
  74. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_tunnel.pyi +0 -0
  75. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_type_manager.py +0 -0
  76. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/__init__.py +0 -0
  77. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/app_utils.py +0 -0
  78. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/async_utils.py +0 -0
  79. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/auth_token_manager.py +0 -0
  80. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/browser_utils.py +0 -0
  81. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/bytes_io_segment_payload.py +0 -0
  82. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/deprecation.py +0 -0
  83. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/docker_utils.py +0 -0
  84. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/function_utils.py +0 -0
  85. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/git_utils.py +0 -0
  86. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/grpc_testing.py +0 -0
  87. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/grpc_utils.py +0 -0
  88. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/hash_utils.py +0 -0
  89. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/http_utils.py +0 -0
  90. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/jwt_utils.py +0 -0
  91. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/logger.py +0 -0
  92. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/mount_utils.py +0 -0
  93. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/name_utils.py +0 -0
  94. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/package_utils.py +0 -0
  95. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/pattern_utils.py +0 -0
  96. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/rand_pb_testing.py +0 -0
  97. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/sandbox_fs_utils.py +0 -0
  98. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/shell_utils.py +0 -0
  99. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/task_command_router_client.py +0 -0
  100. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_utils/time_utils.py +0 -0
  101. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_vendor/__init__.py +0 -0
  102. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  103. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_vendor/cloudpickle.py +0 -0
  104. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_vendor/tblib.py +0 -0
  105. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_vendor/version.py +0 -0
  106. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/_watcher.py +0 -0
  107. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/app.py +0 -0
  108. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/app.pyi +0 -0
  109. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/billing.py +0 -0
  110. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/2023.12.312.txt +0 -0
  111. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/2023.12.txt +0 -0
  112. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/2024.04.txt +0 -0
  113. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/2024.10.txt +0 -0
  114. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/2025.06.txt +0 -0
  115. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/PREVIEW.txt +0 -0
  116. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/README.md +0 -0
  117. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/builder/base-images.json +0 -0
  118. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/call_graph.py +0 -0
  119. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/__init__.py +0 -0
  120. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/_download.py +0 -0
  121. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/import_refs.py +0 -0
  122. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/logo.py +0 -0
  123. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/programs/__init__.py +0 -0
  124. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/programs/run_jupyter.py +0 -0
  125. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/programs/vscode.py +0 -0
  126. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cli/selector.py +0 -0
  127. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/client.py +0 -0
  128. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cloud_bucket_mount.py +0 -0
  129. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cloud_bucket_mount.pyi +0 -0
  130. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cls.py +0 -0
  131. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/cls.pyi +0 -0
  132. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/config.py +0 -0
  133. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/container_process.py +0 -0
  134. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/container_process.pyi +0 -0
  135. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/dict.py +0 -0
  136. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/dict.pyi +0 -0
  137. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/environments.py +0 -0
  138. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/environments.pyi +0 -0
  139. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/exception.py +0 -0
  140. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/experimental/__init__.py +0 -0
  141. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/experimental/flash.py +0 -0
  142. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/experimental/flash.pyi +0 -0
  143. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/experimental/ipython.py +0 -0
  144. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/file_io.py +0 -0
  145. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/file_io.pyi +0 -0
  146. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/file_pattern_matcher.py +0 -0
  147. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/functions.py +0 -0
  148. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/image.py +0 -0
  149. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/image.pyi +0 -0
  150. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/io_streams.py +0 -0
  151. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/io_streams.pyi +0 -0
  152. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/mount.pyi +0 -0
  153. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/network_file_system.py +0 -0
  154. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/network_file_system.pyi +0 -0
  155. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/object.py +0 -0
  156. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/object.pyi +0 -0
  157. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/output.py +0 -0
  158. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/parallel_map.py +0 -0
  159. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/parallel_map.pyi +0 -0
  160. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/partial_function.py +0 -0
  161. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/partial_function.pyi +0 -0
  162. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/proxy.py +0 -0
  163. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/proxy.pyi +0 -0
  164. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/py.typed +0 -0
  165. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/queue.py +0 -0
  166. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/queue.pyi +0 -0
  167. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/retries.py +0 -0
  168. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/runner.py +0 -0
  169. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/runner.pyi +0 -0
  170. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/running_app.py +0 -0
  171. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/sandbox.py +0 -0
  172. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/sandbox.pyi +0 -0
  173. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/sandbox_fs.py +0 -0
  174. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/sandbox_fs.pyi +0 -0
  175. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/schedule.py +0 -0
  176. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/scheduler_placement.py +0 -0
  177. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/secret.py +0 -0
  178. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/secret.pyi +0 -0
  179. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/server.py +0 -0
  180. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/server.pyi +0 -0
  181. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/serving.py +0 -0
  182. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/serving.pyi +0 -0
  183. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/snapshot.py +0 -0
  184. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/snapshot.pyi +0 -0
  185. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/stream_type.py +0 -0
  186. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/token_flow.py +0 -0
  187. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/token_flow.pyi +0 -0
  188. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal/volume.pyi +0 -0
  189. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/dependency_links.txt +0 -0
  190. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/entry_points.txt +0 -0
  191. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal.egg-info/top_level.txt +0 -0
  192. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/__init__.py +0 -0
  193. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/gen_cli_docs_main.py +0 -0
  194. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/gen_reference_docs.py +0 -0
  195. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/gen_reference_docs_main.py +0 -0
  196. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/mdmd/__init__.py +0 -0
  197. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/mdmd/mdmd.py +0 -0
  198. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_docs/mdmd/signatures.py +0 -0
  199. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/__init__.py +0 -0
  200. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/api_grpc.py +0 -0
  201. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/api_pb2.py +0 -0
  202. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/api_pb2.pyi +0 -0
  203. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/api_pb2_grpc.py +0 -0
  204. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/api_pb2_grpc.pyi +0 -0
  205. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/modal_api_grpc.py +0 -0
  206. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/py.typed +0 -0
  207. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/task_command_router_grpc.py +0 -0
  208. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/task_command_router_pb2.py +0 -0
  209. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/task_command_router_pb2.pyi +0 -0
  210. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  211. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  212. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/modal_version/__main__.py +0 -0
  213. {modal-1.4.3.dev2 → modal-1.4.3.dev4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.4.3.dev2
3
+ Version: 1.4.3.dev4
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -25,7 +25,6 @@ Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
25
25
  Requires-Dist: rich>=12.0.0
26
26
  Requires-Dist: synchronicity~=0.12.1
27
27
  Requires-Dist: toml
28
- Requires-Dist: typer>=0.9
29
28
  Requires-Dist: types-certifi
30
29
  Requires-Dist: types-toml
31
30
  Requires-Dist: watchfiles
@@ -6,7 +6,7 @@ import os
6
6
  import platform
7
7
  import time
8
8
  from collections.abc import AsyncIterator
9
- from contextlib import AbstractContextManager, contextmanager
9
+ from contextlib import AbstractContextManager, asynccontextmanager, contextmanager
10
10
  from io import BytesIO, FileIO
11
11
  from pathlib import Path, PurePosixPath
12
12
  from typing import (
@@ -21,11 +21,13 @@ from typing import (
21
21
  )
22
22
  from urllib.parse import urlparse
23
23
 
24
+ from typing_extensions import Self
25
+
24
26
  from modal_proto import api_pb2
25
27
  from modal_proto.modal_api_grpc import ModalClientModal
26
28
 
27
29
  from ..exception import ExecutionError
28
- from .async_utils import TaskContext, retry
30
+ from .async_utils import TaskContext, asyncnullcontext, retry
29
31
  from .hash_utils import UploadHashes, get_upload_hashes
30
32
  from .http_utils import ClientSessionRegistry
31
33
  from .logger import logger
@@ -54,10 +56,60 @@ DEFAULT_SEGMENT_CHUNK_SIZE = 2**24
54
56
  # TODO(dano): remove this once we stop requiring md5 for blobs
55
57
  MULTIPART_UPLOAD_THRESHOLD = 1024**3
56
58
 
59
+ # Memory budget for multipart uploads.
60
+ MULTIPART_INFLIGHT_BYTES_MAX = 2 * 1024**3 # 2 GiB
61
+ MULTIPART_INFLIGHT_BYTES_MIN = 256 * 1024 * 1024 # 256 MiB
62
+ MULTIPART_INFLIGHT_MEMORY_FRACTION = 0.5 # 50% of RAM
63
+
64
+
57
65
  # For block based storage like volumefs2: the size of a block
58
66
  BLOCK_SIZE: int = 8 * 1024 * 1024
59
67
 
60
68
 
69
+ class _ByteBudget:
70
+ """Limits total in-flight bytes across concurrent operations."""
71
+
72
+ def __init__(self, total: int):
73
+ self._total = total
74
+ self._available = total
75
+ self._cond = asyncio.Condition()
76
+
77
+ @classmethod
78
+ def from_system_memory(cls) -> Self:
79
+ return cls(_get_multipart_inflight_budget())
80
+
81
+ @asynccontextmanager
82
+ async def acquire(self, n: int):
83
+ async with self._cond:
84
+ # Wait until enough budget is free. If a single part exceeds the
85
+ # total budget, allow it through once nothing else is in-flight.
86
+ while self._available < min(n, self._total):
87
+ await self._cond.wait()
88
+ self._available -= n
89
+ try:
90
+ yield
91
+ finally:
92
+ async with self._cond:
93
+ self._available += n
94
+ self._cond.notify_all()
95
+
96
+
97
+ def _get_multipart_inflight_budget() -> int:
98
+ """Return a byte budget for concurrent part uploads, scaled to available system memory."""
99
+ try:
100
+ import psutil
101
+
102
+ available = psutil.virtual_memory().available
103
+ except Exception:
104
+ try:
105
+ available = os.sysconf("SC_AVPHYS_PAGES") * os.sysconf("SC_PAGE_SIZE")
106
+ except (AttributeError, ValueError, OSError):
107
+ return MULTIPART_INFLIGHT_BYTES_MIN
108
+
109
+ budget = int(available * MULTIPART_INFLIGHT_MEMORY_FRACTION)
110
+ return max(MULTIPART_INFLIGHT_BYTES_MIN, min(budget, MULTIPART_INFLIGHT_BYTES_MAX))
111
+
112
+
61
113
  @retry(n_attempts=3, base_delay=0.3, attempt_timeout=None)
62
114
  async def _upload_to_s3_url(
63
115
  upload_url,
@@ -116,6 +168,7 @@ async def perform_multipart_upload(
116
168
  completion_url: str,
117
169
  upload_chunk_size: int = DEFAULT_SEGMENT_CHUNK_SIZE,
118
170
  progress_report_cb: Optional[Callable] = None,
171
+ byte_budget: Optional[_ByteBudget] = None,
119
172
  ) -> None:
120
173
  from .bytes_io_segment_payload import BytesIOSegmentPayload
121
174
 
@@ -133,6 +186,14 @@ async def perform_multipart_upload(
133
186
  filename = data_file.name
134
187
  data_file_readers = [open(filename, "rb") for _ in range(len(part_urls))]
135
188
 
189
+ async def _upload_part(part_url: str, part_payload: "BytesIOSegmentPayload") -> str:
190
+ # BytesIOSegmentPayload limits the amount of memory used, but here we
191
+ # acquire some extra for additional copies for various buffers based on
192
+ # empirical testing.
193
+ bytes_to_acquire = 4 * part_payload.chunk_size
194
+ async with byte_budget.acquire(bytes_to_acquire) if byte_budget else asyncnullcontext():
195
+ return await _upload_to_s3_url(part_url, payload=part_payload, content_type=None)
196
+
136
197
  for part_number, (data_file_rdr, part_url) in enumerate(zip(data_file_readers, part_urls), start=1):
137
198
  part_length_bytes = min(num_bytes_left, max_part_size)
138
199
  part_payload = BytesIOSegmentPayload(
@@ -142,7 +203,7 @@ async def perform_multipart_upload(
142
203
  chunk_size=upload_chunk_size,
143
204
  progress_report_cb=progress_report_cb,
144
205
  )
145
- upload_coros.append(_upload_to_s3_url(part_url, payload=part_payload, content_type=None))
206
+ upload_coros.append(_upload_part(part_url, part_payload))
146
207
  num_bytes_left -= part_length_bytes
147
208
  file_offset += part_length_bytes
148
209
 
@@ -211,7 +272,11 @@ async def _blob_upload_with_fallback(
211
272
 
212
273
 
213
274
  async def _blob_upload(
214
- upload_hashes: UploadHashes, data: Union[bytes, BinaryIO], stub, progress_report_cb: Optional[Callable] = None
275
+ upload_hashes: UploadHashes,
276
+ data: Union[bytes, BinaryIO],
277
+ stub,
278
+ progress_report_cb: Optional[Callable] = None,
279
+ byte_budget: Optional[_ByteBudget] = None,
215
280
  ) -> tuple[str, bool, int]:
216
281
  if isinstance(data, bytes):
217
282
  data = BytesIO(data)
@@ -236,6 +301,7 @@ async def _blob_upload(
236
301
  completion_url=part.completion_url,
237
302
  upload_chunk_size=DEFAULT_SEGMENT_CHUNK_SIZE,
238
303
  progress_report_cb=progress_report_cb,
304
+ byte_budget=byte_budget,
239
305
  )
240
306
 
241
307
  blob_id, r2_failed, r2_throughput_bytes_s = await _blob_upload_with_fallback(
@@ -304,9 +370,10 @@ async def blob_upload_file(
304
370
  progress_report_cb: Optional[Callable] = None,
305
371
  sha256_hex: Optional[str] = None,
306
372
  md5_hex: Optional[str] = None,
373
+ byte_budget: Optional[_ByteBudget] = None,
307
374
  ) -> str:
308
375
  upload_hashes = get_upload_hashes(file_obj, sha256_hex=sha256_hex, md5_hex=md5_hex)
309
- blob_id, _, _ = await _blob_upload(upload_hashes, file_obj, stub, progress_report_cb)
376
+ blob_id, _, _ = await _blob_upload(upload_hashes, file_obj, stub, progress_report_cb, byte_budget=byte_budget)
310
377
  return blob_id
311
378
 
312
379
 
@@ -650,7 +717,7 @@ def use_md5(url: str) -> bool:
650
717
  host = urlparse(url).netloc.split(":")[0]
651
718
  if host.endswith(".amazonaws.com") or host.endswith(".r2.cloudflarestorage.com"):
652
719
  return True
653
- elif host in ["127.0.0.1", "localhost", "172.21.0.1"]:
720
+ elif host in ["127.0.0.1", "localhost", "172.20.0.1", "172.21.0.1"]:
654
721
  return False
655
722
  else:
656
723
  raise Exception(f"Unknown S3 host: {host}")
@@ -0,0 +1,264 @@
1
+ # Copyright Modal Labs 2026
2
+ """Custom help formatting for the Modal CLI."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import inspect
7
+ import os
8
+ import sys
9
+ from typing import Any, Optional
10
+
11
+ import click
12
+ from rich.console import Console
13
+ from rich.markdown import Markdown
14
+ from rich.padding import Padding
15
+ from rich.panel import Panel
16
+ from rich.table import Table
17
+ from rich.text import Text
18
+
19
+ _HEADING_STYLE = "bold bright_green"
20
+ _COMMAND_NAME_STYLE = ""
21
+ _COMMAND_DESC_STYLE = "dim"
22
+ _OPTION_FLAG_STYLE = "green"
23
+ _OPTION_METAVAR_STYLE = "dim"
24
+ _ERROR_STYLE = "red"
25
+
26
+ _MAX_HELP_WIDTH = 80
27
+
28
+
29
+ def use_rich_style() -> bool:
30
+ """Whether help output should be rendered in the rich style."""
31
+ env = os.environ.get("MODAL_RICH_CLI") # TODO move to config
32
+ if env in ("0", "1"):
33
+ return env == "1"
34
+ return sys.stdout.isatty()
35
+
36
+
37
+ def _make_console(file: Optional[Any] = None) -> Console:
38
+ return Console(file=file, highlight=False)
39
+
40
+
41
+ def _render_usage(cmd: click.Command, ctx: click.Context, console: Console) -> None:
42
+ pieces = " ".join(cmd.collect_usage_pieces(ctx))
43
+ usage = Text()
44
+ usage.append("Usage: ", style=_HEADING_STYLE)
45
+ usage.append(f"{ctx.command_path} {pieces}".rstrip())
46
+ console.print(usage)
47
+ console.print()
48
+
49
+
50
+ def _render_help_text(cmd: click.Command, console: Console) -> None:
51
+ text = cmd.help or cmd.short_help or ""
52
+ if not text:
53
+ return
54
+ console.print(Padding(Markdown(inspect.cleandoc(text)), (0, 2)))
55
+ console.print()
56
+
57
+
58
+ def _option_label(param: click.Parameter, ctx: click.Context) -> Text:
59
+ text = Text()
60
+ for i, opt in enumerate(param.opts):
61
+ if i > 0:
62
+ text.append(", ")
63
+ text.append(opt, style=_OPTION_FLAG_STYLE)
64
+ if param.secondary_opts:
65
+ text.append(" / ")
66
+ for i, opt in enumerate(param.secondary_opts):
67
+ if i > 0:
68
+ text.append(", ")
69
+ text.append(opt, style=_OPTION_FLAG_STYLE)
70
+ if not getattr(param, "is_flag", False) and not getattr(param, "count", False):
71
+ text.append(" ")
72
+ text.append(param.make_metavar(ctx), style=_OPTION_METAVAR_STYLE)
73
+ return text
74
+
75
+
76
+ def _render_options(cmd: click.Command, ctx: click.Context, console: Console) -> None:
77
+ rows: list[tuple[Text, str]] = []
78
+ for param in cmd.get_params(ctx):
79
+ rec = param.get_help_record(ctx)
80
+ if rec is None: # skips arguments and hidden options
81
+ continue
82
+ rows.append((_option_label(param, ctx), rec[1] or ""))
83
+ if not rows:
84
+ return
85
+
86
+ console.print(Text("Options", style=_HEADING_STYLE))
87
+ table = Table(box=None, show_header=False, pad_edge=False, padding=(0, 2))
88
+ table.add_column(no_wrap=True) # styling lives in the Text cells
89
+ table.add_column(overflow="fold")
90
+ for label, help_str in rows:
91
+ table.add_row(label, help_str)
92
+ console.print(table)
93
+ console.print()
94
+
95
+
96
+ def _render_epilog(cmd: click.Command, console: Console) -> None:
97
+ if cmd.epilog:
98
+ console.print(cmd.epilog)
99
+ console.print()
100
+
101
+
102
+ def _group_commands_by_panel(group: click.Group) -> dict[str, list[tuple[str, click.Command]]]:
103
+ """Bucket visible subcommands, preserving registration order."""
104
+ panels: dict[str, list[tuple[str, click.Command]]] = {}
105
+ for name, sub in group.commands.items():
106
+ if sub.hidden:
107
+ continue
108
+ panels.setdefault(getattr(sub, "panel", None) or "Commands", []).append((name, sub))
109
+ return panels
110
+
111
+
112
+ def _render_commands(group: click.Group, console: Console) -> None:
113
+ panels = _group_commands_by_panel(group)
114
+ if not panels:
115
+ return
116
+
117
+ # We want the name / description columns to be the same widths across groups
118
+ name_width = max(len(name) for items in panels.values() for name, _ in items)
119
+ table_width = min(_MAX_HELP_WIDTH, console.width)
120
+
121
+ for panel_name, items in panels.items():
122
+ console.print(Text(panel_name.ljust(table_width), style=f"{_HEADING_STYLE} underline"))
123
+ _render_command_table(console, items, name_width, table_width)
124
+ console.print()
125
+
126
+
127
+ def build_command_table(name_width: int, table_width: Optional[int] = None) -> Table:
128
+ kwargs: dict[str, Any] = {"box": None, "show_header": False, "pad_edge": False, "padding": (0, 2)}
129
+ if table_width is not None:
130
+ kwargs["width"] = table_width
131
+ table = Table(**kwargs)
132
+ table.add_column(style=_COMMAND_NAME_STYLE, no_wrap=True, width=name_width)
133
+ # ratio=1 claims any extra width for the description column so col1 stays
134
+ # fixed at name_width across panels — otherwise Rich distributes slack into
135
+ # both columns and the name column grows in panels with shorter content.
136
+ table.add_column(style=_COMMAND_DESC_STYLE, overflow="fold", ratio=1)
137
+ return table
138
+
139
+
140
+ def _render_command_table(
141
+ console: Console,
142
+ items: list[tuple[str, click.Command]],
143
+ name_width: int,
144
+ table_width: int,
145
+ ) -> None:
146
+ table = build_command_table(name_width, table_width)
147
+ for name, sub in items:
148
+ table.add_row(name, sub.get_short_help_str(limit=80))
149
+ console.print(table)
150
+
151
+
152
+ # -- Public Command / Group subclasses ---------------------------------------
153
+
154
+
155
+ class ModalCommand(click.Command):
156
+ """click.Command that renders --help with custom rich output."""
157
+
158
+ def __init__(self, *args: Any, panel: Optional[str] = None, **kwargs: Any) -> None:
159
+ super().__init__(*args, **kwargs)
160
+ self.panel = panel
161
+
162
+ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
163
+ if not use_rich_style():
164
+ return super().format_help(ctx, formatter)
165
+ console = _make_console()
166
+ with console.capture() as capture:
167
+ _render_usage(self, ctx, console)
168
+ _render_help_text(self, console)
169
+ _render_options(self, ctx, console)
170
+ _render_epilog(self, console)
171
+ formatter.write(capture.get())
172
+
173
+
174
+ class ModalGroup(click.Group):
175
+ """click.Group whose subcommands and subgroups inherit `ModalCommand`."""
176
+
177
+ command_class = ModalCommand
178
+ group_class = type # nested @group.group() reuses the enclosing class
179
+
180
+ def __init__(self, *args: Any, panel: Optional[str] = None, **kwargs: Any) -> None:
181
+ # Default to showing help when a group is invoked with no subcommand.
182
+ # Callers can opt out by passing no_args_is_help=False explicitly.
183
+ kwargs.setdefault("no_args_is_help", True)
184
+ super().__init__(*args, **kwargs)
185
+ self.panel = panel
186
+
187
+ def add_command(
188
+ self,
189
+ cmd: click.Command,
190
+ name: Optional[str] = None,
191
+ *,
192
+ panel: Optional[str] = None,
193
+ hidden: Optional[bool] = None,
194
+ ) -> None:
195
+ super().add_command(cmd, name)
196
+ if panel is not None:
197
+ cmd.panel = panel # type: ignore[attr-defined]
198
+ if hidden is not None:
199
+ cmd.hidden = hidden
200
+
201
+ def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
202
+ # Replaces click's single flat "Commands:" section with one section per
203
+ # panel so the simple-style help output still preserves grouping.
204
+ for panel_name, items in _group_commands_by_panel(self).items():
205
+ rows = [(name, sub.get_short_help_str(limit=80)) for name, sub in items]
206
+ with formatter.section(panel_name):
207
+ formatter.write_dl(rows)
208
+
209
+ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
210
+ if not use_rich_style():
211
+ return super().format_help(ctx, formatter)
212
+ console = _make_console()
213
+ with console.capture() as capture:
214
+ _render_usage(self, ctx, console)
215
+ _render_help_text(self, console)
216
+ _render_options(self, ctx, console)
217
+ _render_commands(self, console)
218
+ _render_epilog(self, console)
219
+ formatter.write(capture.get())
220
+
221
+
222
+ # -- Error rendering ---------------------------------------------------------
223
+
224
+
225
+ def _render_click_exception(exc: click.ClickException, file: Any) -> None:
226
+ console = _make_console(file if file is not None else sys.stderr)
227
+ title = "Error"
228
+
229
+ if isinstance(exc, click.UsageError) and exc.ctx is not None:
230
+ ctx = exc.ctx
231
+ console.print(ctx.get_usage())
232
+ if ctx.command.get_help_option(ctx) is not None:
233
+ option = ctx.help_option_names[0] if ctx.help_option_names else "--help"
234
+ console.print(f"Try [bold]'{ctx.command_path} {option}'[/bold] for help.")
235
+
236
+ console.print(
237
+ Panel(
238
+ Text(exc.format_message()),
239
+ title=title,
240
+ title_align="left",
241
+ border_style=_ERROR_STYLE,
242
+ expand=True,
243
+ )
244
+ )
245
+
246
+
247
+ _orig_click_show = click.ClickException.show
248
+ _orig_usage_show = click.UsageError.show
249
+
250
+
251
+ def _click_show(self: click.ClickException, file: Any = None) -> None:
252
+ if not use_rich_style():
253
+ return _orig_click_show(self, file)
254
+ _render_click_exception(self, file)
255
+
256
+
257
+ def _usage_show(self: click.UsageError, file: Any = None) -> None:
258
+ if not use_rich_style():
259
+ return _orig_usage_show(self, file)
260
+ _render_click_exception(self, file)
261
+
262
+
263
+ click.ClickException.show = _click_show # type: ignore[method-assign]
264
+ click.UsageError.show = _usage_show # type: ignore[method-assign]
@@ -154,9 +154,8 @@ def setup_rich_traceback() -> None:
154
154
  import click
155
155
  import grpclib
156
156
  import synchronicity
157
- import typer
158
157
 
159
- install(suppress=[synchronicity, grpclib, click, typer], extra_lines=1)
158
+ install(suppress=[synchronicity, grpclib, click], extra_lines=1)
160
159
 
161
160
 
162
161
  def highlight_modal_warnings() -> None:
@@ -8,11 +8,9 @@ from typing import Optional, Union, get_args
8
8
 
9
9
  import click
10
10
  import rich
11
- import typer
12
11
  from click import UsageError
13
12
  from rich.table import Column
14
13
  from rich.text import Text
15
- from typer import Argument
16
14
 
17
15
  from modal._object import _get_environment_name
18
16
  from modal._traceback import print_server_warnings
@@ -27,20 +25,18 @@ from modal_proto import api_pb2
27
25
 
28
26
  from .._logs import _FETCH_LIMIT, _MAX_FETCH_RANGE, LogsFilters
29
27
  from .._utils.time_utils import locale_tz, timestamp_to_localized_str
28
+ from ._help import ModalGroup
30
29
  from .utils import (
31
- ENV_OPTION,
32
- YES_OPTION,
33
30
  confirm_or_suggest_yes,
34
31
  display_table,
32
+ env_option,
35
33
  fetch_app_logs,
36
34
  stream_app_logs,
37
35
  tail_app_logs,
36
+ yes_option,
38
37
  )
39
38
 
40
- APP_IDENTIFIER = Argument("", help="App name or ID")
41
- NAME_OPTION = typer.Option("", "-n", "--name", help="Deprecated: Pass App name as a positional argument")
42
-
43
- app_cli = typer.Typer(name="app", help="Manage deployed and running apps.", no_args_is_help=True)
39
+ app_cli = ModalGroup(name="app", help="Manage deployed and running apps.")
44
40
 
45
41
  APP_STATE_TO_MESSAGE = {
46
42
  api_pb2.APP_STATE_DEPLOYED: Text("deployed", style="green"),
@@ -101,9 +97,11 @@ async def resolve_app_identifier(
101
97
 
102
98
 
103
99
  @app_cli.command("list")
100
+ @env_option
101
+ @click.option("--json", is_flag=True, default=False)
104
102
  @synchronizer.create_blocking
105
- async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
106
- """List Modal apps that are currently deployed/running or recently stopped."""
103
+ async def list_(env: Optional[str] = None, json: bool = False):
104
+ """List Apps that are running, deployed or recently stopped."""
107
105
  env = ensure_env(env)
108
106
  client = await _Client.from_env()
109
107
 
@@ -176,38 +174,43 @@ _SOURCE_OPTIONS = {
176
174
 
177
175
 
178
176
  @app_cli.command("logs", no_args_is_help=True)
177
+ @click.argument("app_identifier")
178
+ @click.option("-f", "--follow", is_flag=True, default=False, help="Stream log output until App stops")
179
+ @click.option(
180
+ "--since",
181
+ default=None,
182
+ help="Start of time range. Accepts ISO 8601 datetime or relative time, e.g. '1d' (1 day ago), '2h', '30m', etc.",
183
+ )
184
+ @click.option("--until", default=None, help="End of time range; accepts same argument types as --since")
185
+ @click.option("-n", "--tail", default=None, type=int, help="Show only the last N log entries")
186
+ @click.option("--search", default=None, help="Filter by search text")
187
+ @click.option("--function", "function_id", default="", help="Filter by Function ID (fu-*)")
188
+ @click.option("--function-call", "function_call_id", default="", help="Filter by FunctionCall ID (fc-*)")
189
+ @click.option("--container", "container_id", default="", help="Filter by Container ID (ta-*)")
190
+ @click.option("-s", "--source", default=None, help="Filter by source: 'stdout', 'stderr', or 'system'")
191
+ @click.option("--timestamps", is_flag=True, default=False, help="Prefix each line with its timestamp")
192
+ @click.option("--show-function-id", is_flag=True, default=False, help="Prefix each line with its Function ID")
193
+ @click.option("--show-function-call-id", is_flag=True, default=False, help="Prefix each line with its FunctionCall ID")
194
+ @click.option("--show-container-id", is_flag=True, default=False, help="Prefix each line with its Container ID")
195
+ @env_option
179
196
  @synchronizer.create_blocking
180
197
  async def logs(
181
- app_identifier: str = APP_IDENTIFIER,
182
- follow: bool = typer.Option(False, "-f", "--follow", help="Stream log output until App stops"),
183
- since: Optional[str] = typer.Option(
184
- None,
185
- "--since",
186
- help=(
187
- "Start of time range. Accepts ISO 8601 datetime or relative time, e.g. '1d' (1 day ago), '2h', '30m', etc."
188
- ),
189
- ),
190
- until: Optional[str] = typer.Option(
191
- None,
192
- "--until",
193
- help="End of time range; accepts same argument types as --since",
194
- ),
195
- tail: Optional[int] = typer.Option(None, "--tail", "-n", help="Show only the last N log entries"),
196
- search: Optional[str] = typer.Option(None, "--search", help="Filter by search text"),
197
- function_id: Optional[str] = typer.Option("", "--function", help="Filter by Function ID (fu-*)"),
198
- function_call_id: Optional[str] = typer.Option("", "--function-call", help="Filter by FunctionCall ID (fc-*)"),
199
- container_id: Optional[str] = typer.Option("", "--container", help="Filter by Container ID (ta-*)"),
200
- source: Optional[str] = typer.Option(
201
- None, "--source", "-s", help="Filter by source: 'stdout', 'stderr', or 'system'"
202
- ),
203
- timestamps: bool = typer.Option(False, "--timestamps", help="Prefix each line with its timestamp"),
204
- show_function_id: bool = typer.Option(False, "--show-function-id", help="Prefix each line with its Function ID"),
205
- show_function_call_id: bool = typer.Option(
206
- False, "--show-function-call-id", help="Prefix each line with its FunctionCall ID"
207
- ),
208
- show_container_id: bool = typer.Option(False, "--show-container-id", help="Prefix each line with its Container ID"),
198
+ app_identifier: str,
199
+ follow: bool = False,
200
+ since: Optional[str] = None,
201
+ until: Optional[str] = None,
202
+ tail: Optional[int] = None,
203
+ search: Optional[str] = None,
204
+ function_id: str = "",
205
+ function_call_id: str = "",
206
+ container_id: str = "",
207
+ source: Optional[str] = None,
208
+ timestamps: bool = False,
209
+ show_function_id: bool = False,
210
+ show_function_call_id: bool = False,
211
+ show_container_id: bool = False,
209
212
  *,
210
- env: Optional[str] = ENV_OPTION,
213
+ env: Optional[str] = None,
211
214
  ):
212
215
  """Fetch or stream App logs.
213
216
 
@@ -265,8 +268,6 @@ async def logs(
265
268
  ```
266
269
 
267
270
  """
268
- if not app_identifier:
269
- raise UsageError("Either an App ID or name must be provided.")
270
271
 
271
272
  if follow and (since or until or tail):
272
273
  raise UsageError("--follow cannot be combined with --since, --until, or --tail.")
@@ -350,12 +351,15 @@ async def logs(
350
351
 
351
352
 
352
353
  @app_cli.command("rollback", no_args_is_help=True, context_settings={"ignore_unknown_options": True})
354
+ @click.argument("app_identifier")
355
+ @click.argument("version", default="")
356
+ @env_option
353
357
  @synchronizer.create_blocking
354
358
  async def rollback(
355
- app_identifier: str = APP_IDENTIFIER,
356
- version: str = typer.Argument("", help="Target version for rollback."),
359
+ app_identifier: str,
360
+ version: str = "",
357
361
  *,
358
- env: Optional[str] = ENV_OPTION,
362
+ env: Optional[str] = None,
359
363
  ):
360
364
  """Redeploy a previous version of an App.
361
365
 
@@ -404,16 +408,20 @@ async def rollback(
404
408
 
405
409
 
406
410
  @app_cli.command("rollover", no_args_is_help=True)
411
+ @click.argument("app_identifier")
412
+ @click.option(
413
+ "--strategy",
414
+ default="rolling",
415
+ type=click.Choice(get_args(DEPLOYMENT_STRATEGY_TYPE)),
416
+ help="Strategy for rollover",
417
+ )
418
+ @env_option
407
419
  @synchronizer.create_blocking
408
420
  async def rollover(
409
- app_identifier: str = APP_IDENTIFIER,
421
+ app_identifier: str,
422
+ strategy: str = "rolling",
410
423
  *,
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,
424
+ env: Optional[str] = None,
417
425
  ):
418
426
  """Redeploy an App to get new containers without code changes.
419
427
 
@@ -467,12 +475,15 @@ async def rollover(
467
475
 
468
476
 
469
477
  @app_cli.command("stop", no_args_is_help=True)
478
+ @click.argument("app_identifier")
479
+ @yes_option
480
+ @env_option
470
481
  @synchronizer.create_blocking
471
482
  async def stop(
472
- app_identifier: str = APP_IDENTIFIER,
483
+ app_identifier: str,
473
484
  *,
474
- yes: bool = YES_OPTION,
475
- env: Optional[str] = ENV_OPTION,
485
+ yes: bool = False,
486
+ env: Optional[str] = None,
476
487
  ):
477
488
  """Permanently stop an App and terminate its running containers."""
478
489
  env = ensure_env(env)
@@ -510,11 +521,14 @@ async def stop(
510
521
 
511
522
 
512
523
  @app_cli.command("history", no_args_is_help=True)
524
+ @click.argument("app_identifier")
525
+ @env_option
526
+ @click.option("--json", is_flag=True, default=False)
513
527
  @synchronizer.create_blocking
514
528
  async def history(
515
- app_identifier: str = APP_IDENTIFIER,
529
+ app_identifier: str,
516
530
  *,
517
- env: Optional[str] = ENV_OPTION,
531
+ env: Optional[str] = None,
518
532
  json: bool = False,
519
533
  ):
520
534
  """Show an App's deployment history.
@@ -588,11 +602,13 @@ async def history(
588
602
 
589
603
 
590
604
  @app_cli.command("dashboard", no_args_is_help=True)
605
+ @click.argument("app_identifier")
606
+ @env_option
591
607
  @synchronizer.create_blocking
592
608
  async def dashboard(
593
- app_identifier: str = APP_IDENTIFIER,
609
+ app_identifier: str,
594
610
  *,
595
- env: Optional[str] = ENV_OPTION,
611
+ env: Optional[str] = None,
596
612
  ):
597
613
  """Open an App's dashboard page in your web browser.
598
614