modAL 1.3.3.dev0__tar.gz → 1.3.4__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 (209) hide show
  1. {modal-1.3.3.dev0 → modal-1.3.4}/PKG-INFO +1 -1
  2. {modal-1.3.3.dev0 → modal-1.3.4}/modal/__init__.py +2 -2
  3. modal-1.3.4/modal/__main__.py +49 -0
  4. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_billing.py +8 -2
  5. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_functions.py +5 -8
  6. modal-1.3.4/modal/_output/__init__.py +1 -0
  7. modal-1.3.4/modal/_output/manager.py +542 -0
  8. modal-1.3.4/modal/_output/pty.py +200 -0
  9. modal-1.3.3.dev0/modal/_output.py → modal-1.3.4/modal/_output/rich.py +326 -266
  10. modal-1.3.4/modal/_output/status.py +101 -0
  11. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_resolver.py +1 -62
  12. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_runtime/asgi.py +2 -2
  13. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_server.py +3 -0
  14. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/browser_utils.py +8 -7
  15. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/function_utils.py +0 -87
  16. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/grpc_utils.py +17 -1
  17. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/shell_utils.py +48 -2
  18. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/task_command_router_client.py +3 -1
  19. modal-1.3.4/modal/_utils/time_utils.py +204 -0
  20. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_watcher.py +2 -3
  21. {modal-1.3.3.dev0 → modal-1.3.4}/modal/app.py +5 -0
  22. {modal-1.3.3.dev0 → modal-1.3.4}/modal/app.pyi +10 -3
  23. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/_traceback.py +3 -36
  24. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/app.py +1 -4
  25. modal-1.3.4/modal/cli/billing.py +153 -0
  26. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/cluster.py +4 -5
  27. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/config.py +2 -3
  28. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/container.py +8 -2
  29. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/dashboard.py +1 -4
  30. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/dict.py +2 -3
  31. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/entry_point.py +15 -10
  32. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/import_refs.py +15 -10
  33. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/launch.py +12 -15
  34. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/network_file_system.py +21 -29
  35. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/profile.py +6 -6
  36. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/queues.py +4 -5
  37. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/run.py +46 -45
  38. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/secret.py +7 -6
  39. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/token.py +11 -12
  40. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/utils.py +28 -9
  41. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/volume.py +19 -24
  42. {modal-1.3.3.dev0 → modal-1.3.4}/modal/client.pyi +2 -10
  43. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cloud_bucket_mount.py +3 -1
  44. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cloud_bucket_mount.pyi +3 -1
  45. {modal-1.3.3.dev0 → modal-1.3.4}/modal/container_process.py +6 -8
  46. {modal-1.3.3.dev0 → modal-1.3.4}/modal/dict.py +42 -0
  47. {modal-1.3.3.dev0 → modal-1.3.4}/modal/dict.pyi +52 -0
  48. {modal-1.3.3.dev0 → modal-1.3.4}/modal/experimental/flash.py +10 -1
  49. {modal-1.3.3.dev0 → modal-1.3.4}/modal/experimental/flash.pyi +3 -0
  50. {modal-1.3.3.dev0 → modal-1.3.4}/modal/gpu.py +3 -0
  51. {modal-1.3.3.dev0 → modal-1.3.4}/modal/image.py +15 -8
  52. {modal-1.3.3.dev0 → modal-1.3.4}/modal/image.pyi +7 -1
  53. {modal-1.3.3.dev0 → modal-1.3.4}/modal/io_streams.py +4 -1
  54. {modal-1.3.3.dev0 → modal-1.3.4}/modal/mount.py +4 -1
  55. {modal-1.3.3.dev0 → modal-1.3.4}/modal/network_file_system.py +1 -0
  56. {modal-1.3.3.dev0 → modal-1.3.4}/modal/network_file_system.pyi +3 -1
  57. {modal-1.3.3.dev0 → modal-1.3.4}/modal/object.pyi +57 -13
  58. modal-1.3.4/modal/output.py +47 -0
  59. {modal-1.3.3.dev0 → modal-1.3.4}/modal/partial_function.pyi +2 -1
  60. {modal-1.3.3.dev0 → modal-1.3.4}/modal/queue.py +42 -0
  61. {modal-1.3.3.dev0 → modal-1.3.4}/modal/queue.pyi +52 -0
  62. {modal-1.3.3.dev0 → modal-1.3.4}/modal/retries.py +0 -3
  63. {modal-1.3.3.dev0 → modal-1.3.4}/modal/runner.py +49 -56
  64. {modal-1.3.3.dev0 → modal-1.3.4}/modal/sandbox.py +149 -19
  65. {modal-1.3.3.dev0 → modal-1.3.4}/modal/sandbox.pyi +203 -29
  66. {modal-1.3.3.dev0 → modal-1.3.4}/modal/serving.py +10 -13
  67. {modal-1.3.3.dev0 → modal-1.3.4}/modal/token_flow.py +18 -19
  68. {modal-1.3.3.dev0 → modal-1.3.4}/modal/volume.py +71 -1
  69. {modal-1.3.3.dev0 → modal-1.3.4}/modal/volume.pyi +56 -0
  70. {modal-1.3.3.dev0 → modal-1.3.4}/modal.egg-info/PKG-INFO +1 -1
  71. {modal-1.3.3.dev0 → modal-1.3.4}/modal.egg-info/SOURCES.txt +6 -2
  72. {modal-1.3.3.dev0 → modal-1.3.4}/modal_docs/gen_reference_docs.py +1 -0
  73. {modal-1.3.3.dev0 → modal-1.3.4}/modal_docs/mdmd/mdmd.py +10 -2
  74. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/api.proto +30 -0
  75. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/api_grpc.py +48 -0
  76. modal-1.3.4/modal_proto/api_pb2.py +5310 -0
  77. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/api_pb2.pyi +99 -0
  78. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/api_pb2_grpc.py +99 -0
  79. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/api_pb2_grpc.pyi +30 -0
  80. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/modal_api_grpc.py +3 -0
  81. modal-1.3.4/modal_proto/task_command_router_pb2.py +180 -0
  82. {modal-1.3.3.dev0 → modal-1.3.4}/modal_version/__init__.py +1 -1
  83. modal-1.3.3.dev0/modal/__main__.py +0 -53
  84. modal-1.3.3.dev0/modal/_pty.py +0 -57
  85. modal-1.3.3.dev0/modal/_utils/time_utils.py +0 -43
  86. modal-1.3.3.dev0/modal/output.py +0 -64
  87. modal-1.3.3.dev0/modal_proto/api_pb2.py +0 -1139
  88. modal-1.3.3.dev0/modal_proto/task_command_router_pb2.py +0 -60
  89. {modal-1.3.3.dev0 → modal-1.3.4}/LICENSE +0 -0
  90. {modal-1.3.3.dev0 → modal-1.3.4}/README.md +0 -0
  91. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_clustered_functions.py +0 -0
  92. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_clustered_functions.pyi +0 -0
  93. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_container_entrypoint.py +0 -0
  94. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_grpc_client.py +0 -0
  95. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_ipython.py +0 -0
  96. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_load_context.py +0 -0
  97. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_location.py +0 -0
  98. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_object.py +0 -0
  99. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_partial_function.py +0 -0
  100. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_resources.py +0 -0
  101. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_runtime/__init__.py +0 -0
  102. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_runtime/container_io_manager.py +0 -0
  103. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_runtime/container_io_manager.pyi +0 -0
  104. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_runtime/execution_context.py +0 -0
  105. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_runtime/execution_context.pyi +0 -0
  106. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  107. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_runtime/telemetry.py +0 -0
  108. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_runtime/user_code_event_loop.py +0 -0
  109. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_runtime/user_code_imports.py +0 -0
  110. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_serialization.py +0 -0
  111. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_traceback.py +0 -0
  112. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_tunnel.py +0 -0
  113. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_tunnel.pyi +0 -0
  114. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_type_manager.py +0 -0
  115. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/__init__.py +0 -0
  116. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/app_utils.py +0 -0
  117. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/async_utils.py +0 -0
  118. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/auth_token_manager.py +0 -0
  119. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/blob_utils.py +0 -0
  120. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/bytes_io_segment_payload.py +0 -0
  121. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/deprecation.py +0 -0
  122. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/docker_utils.py +0 -0
  123. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/git_utils.py +0 -0
  124. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/grpc_testing.py +0 -0
  125. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/hash_utils.py +0 -0
  126. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/http_utils.py +0 -0
  127. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/jwt_utils.py +0 -0
  128. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/logger.py +0 -0
  129. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/mount_utils.py +0 -0
  130. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/name_utils.py +0 -0
  131. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/package_utils.py +0 -0
  132. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/pattern_utils.py +0 -0
  133. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_utils/rand_pb_testing.py +0 -0
  134. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_vendor/__init__.py +0 -0
  135. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  136. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_vendor/cloudpickle.py +0 -0
  137. {modal-1.3.3.dev0 → modal-1.3.4}/modal/_vendor/tblib.py +0 -0
  138. {modal-1.3.3.dev0 → modal-1.3.4}/modal/billing.py +0 -0
  139. {modal-1.3.3.dev0 → modal-1.3.4}/modal/builder/2023.12.312.txt +0 -0
  140. {modal-1.3.3.dev0 → modal-1.3.4}/modal/builder/2023.12.txt +0 -0
  141. {modal-1.3.3.dev0 → modal-1.3.4}/modal/builder/2024.04.txt +0 -0
  142. {modal-1.3.3.dev0 → modal-1.3.4}/modal/builder/2024.10.txt +0 -0
  143. {modal-1.3.3.dev0 → modal-1.3.4}/modal/builder/2025.06.txt +0 -0
  144. {modal-1.3.3.dev0 → modal-1.3.4}/modal/builder/PREVIEW.txt +0 -0
  145. {modal-1.3.3.dev0 → modal-1.3.4}/modal/builder/README.md +0 -0
  146. {modal-1.3.3.dev0 → modal-1.3.4}/modal/builder/base-images.json +0 -0
  147. {modal-1.3.3.dev0 → modal-1.3.4}/modal/call_graph.py +0 -0
  148. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/__init__.py +0 -0
  149. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/_download.py +0 -0
  150. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/environment.py +0 -0
  151. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/programs/__init__.py +0 -0
  152. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/programs/run_jupyter.py +0 -0
  153. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/programs/vscode.py +0 -0
  154. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cli/shell.py +0 -0
  155. {modal-1.3.3.dev0 → modal-1.3.4}/modal/client.py +0 -0
  156. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cls.py +0 -0
  157. {modal-1.3.3.dev0 → modal-1.3.4}/modal/cls.pyi +0 -0
  158. {modal-1.3.3.dev0 → modal-1.3.4}/modal/config.py +0 -0
  159. {modal-1.3.3.dev0 → modal-1.3.4}/modal/container_process.pyi +0 -0
  160. {modal-1.3.3.dev0 → modal-1.3.4}/modal/environments.py +0 -0
  161. {modal-1.3.3.dev0 → modal-1.3.4}/modal/environments.pyi +0 -0
  162. {modal-1.3.3.dev0 → modal-1.3.4}/modal/exception.py +0 -0
  163. {modal-1.3.3.dev0 → modal-1.3.4}/modal/experimental/__init__.py +0 -0
  164. {modal-1.3.3.dev0 → modal-1.3.4}/modal/experimental/ipython.py +0 -0
  165. {modal-1.3.3.dev0 → modal-1.3.4}/modal/file_io.py +0 -0
  166. {modal-1.3.3.dev0 → modal-1.3.4}/modal/file_io.pyi +0 -0
  167. {modal-1.3.3.dev0 → modal-1.3.4}/modal/file_pattern_matcher.py +0 -0
  168. {modal-1.3.3.dev0 → modal-1.3.4}/modal/functions.py +0 -0
  169. {modal-1.3.3.dev0 → modal-1.3.4}/modal/functions.pyi +0 -0
  170. {modal-1.3.3.dev0 → modal-1.3.4}/modal/io_streams.pyi +0 -0
  171. {modal-1.3.3.dev0 → modal-1.3.4}/modal/mount.pyi +0 -0
  172. {modal-1.3.3.dev0 → modal-1.3.4}/modal/object.py +0 -0
  173. {modal-1.3.3.dev0 → modal-1.3.4}/modal/parallel_map.py +0 -0
  174. {modal-1.3.3.dev0 → modal-1.3.4}/modal/parallel_map.pyi +0 -0
  175. {modal-1.3.3.dev0 → modal-1.3.4}/modal/partial_function.py +0 -0
  176. {modal-1.3.3.dev0 → modal-1.3.4}/modal/proxy.py +0 -0
  177. {modal-1.3.3.dev0 → modal-1.3.4}/modal/proxy.pyi +0 -0
  178. {modal-1.3.3.dev0 → modal-1.3.4}/modal/py.typed +0 -0
  179. {modal-1.3.3.dev0 → modal-1.3.4}/modal/runner.pyi +0 -0
  180. {modal-1.3.3.dev0 → modal-1.3.4}/modal/running_app.py +0 -0
  181. {modal-1.3.3.dev0 → modal-1.3.4}/modal/schedule.py +0 -0
  182. {modal-1.3.3.dev0 → modal-1.3.4}/modal/scheduler_placement.py +0 -0
  183. {modal-1.3.3.dev0 → modal-1.3.4}/modal/secret.py +0 -0
  184. {modal-1.3.3.dev0 → modal-1.3.4}/modal/secret.pyi +0 -0
  185. {modal-1.3.3.dev0 → modal-1.3.4}/modal/server.py +0 -0
  186. {modal-1.3.3.dev0 → modal-1.3.4}/modal/server.pyi +0 -0
  187. {modal-1.3.3.dev0 → modal-1.3.4}/modal/serving.pyi +0 -0
  188. {modal-1.3.3.dev0 → modal-1.3.4}/modal/snapshot.py +0 -0
  189. {modal-1.3.3.dev0 → modal-1.3.4}/modal/snapshot.pyi +0 -0
  190. {modal-1.3.3.dev0 → modal-1.3.4}/modal/stream_type.py +0 -0
  191. {modal-1.3.3.dev0 → modal-1.3.4}/modal/token_flow.pyi +0 -0
  192. {modal-1.3.3.dev0 → modal-1.3.4}/modal.egg-info/dependency_links.txt +0 -0
  193. {modal-1.3.3.dev0 → modal-1.3.4}/modal.egg-info/entry_points.txt +0 -0
  194. {modal-1.3.3.dev0 → modal-1.3.4}/modal.egg-info/requires.txt +0 -0
  195. {modal-1.3.3.dev0 → modal-1.3.4}/modal.egg-info/top_level.txt +0 -0
  196. {modal-1.3.3.dev0 → modal-1.3.4}/modal_docs/__init__.py +0 -0
  197. {modal-1.3.3.dev0 → modal-1.3.4}/modal_docs/gen_cli_docs.py +0 -0
  198. {modal-1.3.3.dev0 → modal-1.3.4}/modal_docs/mdmd/__init__.py +0 -0
  199. {modal-1.3.3.dev0 → modal-1.3.4}/modal_docs/mdmd/signatures.py +0 -0
  200. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/__init__.py +0 -0
  201. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/py.typed +0 -0
  202. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/task_command_router.proto +0 -0
  203. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/task_command_router_grpc.py +0 -0
  204. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/task_command_router_pb2.pyi +0 -0
  205. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  206. {modal-1.3.3.dev0 → modal-1.3.4}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  207. {modal-1.3.3.dev0 → modal-1.3.4}/modal_version/__main__.py +0 -0
  208. {modal-1.3.3.dev0 → modal-1.3.4}/pyproject.toml +0 -0
  209. {modal-1.3.3.dev0 → modal-1.3.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.3.3.dev0
3
+ Version: 1.3.4
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -9,6 +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
13
  from ._runtime.execution_context import current_function_call_id, current_input_id, interact, is_local
13
14
  from ._tunnel import Tunnel, forward
14
15
  from .app import App
@@ -41,7 +42,6 @@ try:
41
42
  from .schedule import Cron, Period
42
43
  from .scheduler_placement import SchedulerPlacement
43
44
  from .secret import Secret
44
- from .server import Server
45
45
  from .snapshot import SandboxSnapshot
46
46
  from .volume import Volume
47
47
  except Exception:
@@ -76,11 +76,11 @@ __all__ = [
76
76
  "SandboxSnapshot",
77
77
  "SchedulerPlacement",
78
78
  "Secret",
79
- "Server",
80
79
  "Tunnel",
81
80
  "Volume",
82
81
  "asgi_app",
83
82
  "batched",
83
+ "billing",
84
84
  "concurrent",
85
85
  "current_function_call_id",
86
86
  "current_input_id",
@@ -0,0 +1,49 @@
1
+ # Copyright Modal Labs 2022
2
+ import sys
3
+
4
+ from ._traceback import reduce_traceback_to_user_code
5
+ from .cli._traceback import highlight_modal_warnings, setup_rich_traceback
6
+ from .cli.entry_point import entrypoint_cli
7
+ from .cli.import_refs import _CliUserExecutionError
8
+ from .config import config
9
+ from .output import OutputManager, enable_output
10
+
11
+
12
+ def main():
13
+ # Setup rich tracebacks, but only on user's end, when using the Modal CLI.
14
+ setup_rich_traceback()
15
+ highlight_modal_warnings()
16
+
17
+ # Enable rich output for the entire CLI session
18
+ with enable_output():
19
+ try:
20
+ entrypoint_cli()
21
+
22
+ except _CliUserExecutionError as exc:
23
+ if config.get("traceback"):
24
+ raise
25
+
26
+ assert exc.__cause__ # We should always raise this class from another error
27
+ tb = reduce_traceback_to_user_code(exc.__cause__.__traceback__, exc.user_source)
28
+ sys.excepthook(type(exc.__cause__), exc.__cause__, tb)
29
+ sys.exit(1)
30
+
31
+ except Exception as exc:
32
+ if (
33
+ # User has asked to alway see full tracebacks
34
+ config.get("traceback")
35
+ # The exception message is empty, so we need to provide _some_ actionable information
36
+ or not str(exc)
37
+ ):
38
+ raise
39
+
40
+ content = str(exc)
41
+ if notes := getattr(exc, "__notes__", []):
42
+ content = f"{content}\n\nNote: {' '.join(notes)}"
43
+
44
+ OutputManager.get().print_error(content)
45
+ sys.exit(1)
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
@@ -40,8 +40,14 @@ async def _workspace_billing_report(
40
40
  partial `end` intervals will be excluded.
41
41
 
42
42
  Additional user-provided metadata can be included in the report if the objects have tags
43
- and `tag_names` (i.e., keys) are specified in the request. Note that tags will be attributed
44
- to the entire interval even if they were added or removed at some point within it.
43
+ and `tag_names` (i.e., keys) are specified in the request. Alternatively, pass `tag_names=["*"]`
44
+ to include all tags in the report. Note that tags will be attributed to the entire interval even
45
+ if they were added or removed at some point within it. If the tag name was not in use during an
46
+ interval, it will be absent from the tags dictionary in that output row.
47
+
48
+ In most cases, billing data will be available in the database that this API queries within
49
+ minutes, although there may be collection delays. If completeness is important for your use
50
+ case, we recommend leaving a buffer after the end of the query interval.
45
51
 
46
52
  """
47
53
  if client is None:
@@ -21,7 +21,8 @@ from modal_proto.modal_api_grpc import ModalClientModal
21
21
 
22
22
  from ._load_context import LoadContext
23
23
  from ._object import _Object, live_method, live_method_gen
24
- from ._pty import get_pty_info
24
+ from ._output.pty import get_pty_info
25
+ from ._output.status import FunctionCreationStatus
25
26
  from ._resolver import Resolver
26
27
  from ._resources import convert_fn_config_to_resources_config
27
28
  from ._runtime.execution_context import current_input_id, is_local
@@ -47,7 +48,6 @@ from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
47
48
  from ._utils.function_utils import (
48
49
  ATTEMPT_TIMEOUT_GRACE_PERIOD,
49
50
  OUTPUTS_TIMEOUT,
50
- FunctionCreationStatus,
51
51
  FunctionInfo,
52
52
  _create_input,
53
53
  _process_result,
@@ -71,7 +71,7 @@ from .gpu import GPU_T, parse_gpu_config
71
71
  from .image import _Image
72
72
  from .mount import _get_client_mount, _Mount
73
73
  from .network_file_system import _NetworkFileSystem, network_file_system_mount_protos
74
- from .output import _get_output_manager
74
+ from .output import OutputManager
75
75
  from .parallel_map import (
76
76
  _experimental_spawn_map_async,
77
77
  _experimental_spawn_map_sync,
@@ -922,7 +922,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
922
922
  async def _load(
923
923
  self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
924
924
  ):
925
- with FunctionCreationStatus(resolver, tag) as function_creation_status:
925
+ with FunctionCreationStatus(tag) as function_creation_status:
926
926
  timeout_secs = timeout
927
927
 
928
928
  if app and app.is_interactive and not is_builder_function:
@@ -1639,10 +1639,7 @@ Use the `Function.get_web_url()` method instead.
1639
1639
  raise InvalidError("A generator function cannot be called with `.map(...)`.")
1640
1640
 
1641
1641
  assert self._function_name
1642
- if output_mgr := _get_output_manager():
1643
- count_update_callback = output_mgr.function_progress_callback(self._function_name, total=None)
1644
- else:
1645
- count_update_callback = None
1642
+ count_update_callback = OutputManager.get().function_progress_callback(self._function_name, total=None)
1646
1643
 
1647
1644
  if self._input_plane_url:
1648
1645
  async with aclosing(
@@ -0,0 +1 @@
1
+ # Copyright Modal Labs 2025
@@ -0,0 +1,542 @@
1
+ # Copyright Modal Labs 2022
2
+ """Output management interface for Modal CLI.
3
+
4
+ This module defines the interface for output management and provides a no-op
5
+ implementation for when output is disabled. The rich-based implementation lives
6
+ in rich.py to avoid importing rich when it's not needed.
7
+ """
8
+
9
+ import asyncio
10
+ import contextlib
11
+ import sys
12
+ from abc import ABC, abstractmethod
13
+ from collections.abc import Generator
14
+ from contextlib import AbstractContextManager
15
+ from typing import TYPE_CHECKING, Any, Callable, Optional
16
+
17
+ from modal._utils.shell_utils import write_to_fd
18
+
19
+ if TYPE_CHECKING:
20
+ from modal_proto import api_pb2
21
+
22
+
23
+ class StatusContext(ABC):
24
+ """Abstract base class for status context managers that support manual start/stop control."""
25
+
26
+ @abstractmethod
27
+ def start(self) -> None:
28
+ """Start showing the status spinner."""
29
+ ...
30
+
31
+ @abstractmethod
32
+ def stop(self) -> None:
33
+ """Stop showing the status spinner."""
34
+ ...
35
+
36
+ @abstractmethod
37
+ def update(self, status: str) -> None:
38
+ """Update the status message."""
39
+ ...
40
+
41
+ @abstractmethod
42
+ def __enter__(self) -> "StatusContext": ...
43
+
44
+ @abstractmethod
45
+ def __exit__(self, *args: Any) -> None: ...
46
+
47
+
48
+ class StatusRow(ABC):
49
+ """Abstract base class describing a row in the object creation status tree."""
50
+
51
+ @abstractmethod
52
+ def message(self, message: str) -> None:
53
+ """Update the primary message shown for this row."""
54
+ ...
55
+
56
+ @abstractmethod
57
+ def warn(self, warning: "api_pb2.Warning") -> None:
58
+ """Append a warning message associated with this row."""
59
+ ...
60
+
61
+ @abstractmethod
62
+ def finish(self, message: str) -> None:
63
+ """Mark the row as finished with the given message."""
64
+ ...
65
+
66
+
67
+ class DisabledStatusRow(StatusRow):
68
+ """No-op StatusRow used when output is disabled."""
69
+
70
+ def message(self, message: str) -> None:
71
+ pass
72
+
73
+ def warn(self, warning: "api_pb2.Warning") -> None:
74
+ pass
75
+
76
+ def finish(self, message: str) -> None:
77
+ pass
78
+
79
+
80
+ class TransferProgressContext(ABC):
81
+ """Abstract base class for transfer progress tracking context."""
82
+
83
+ @abstractmethod
84
+ def progress(
85
+ self,
86
+ task_id: Any = None,
87
+ advance: Optional[float] = None,
88
+ name: Optional[str] = None,
89
+ size: Optional[float] = None,
90
+ reset: Optional[bool] = False,
91
+ complete: Optional[bool] = False,
92
+ ) -> Any:
93
+ """Update progress. Returns task_id when creating a new task."""
94
+ ...
95
+
96
+
97
+ class _DisabledTransferProgress(TransferProgressContext):
98
+ """No-op transfer progress context for when output is disabled."""
99
+
100
+ def progress(
101
+ self,
102
+ task_id: Any = None,
103
+ advance: Optional[float] = None,
104
+ name: Optional[str] = None,
105
+ size: Optional[float] = None,
106
+ reset: Optional[bool] = False,
107
+ complete: Optional[bool] = False,
108
+ ) -> None:
109
+ return None
110
+
111
+
112
+ class OutputManager(ABC):
113
+ """Abstract base class defining the interface for output management.
114
+
115
+ This class allows for different implementations:
116
+ - RichOutputManager: Full rich-based terminal output with progress spinners, trees, etc.
117
+ - DisabledOutputManager: No-op implementation for when output is disabled.
118
+
119
+ Subclasses must implement all abstract methods. Use OutputManager.get() to get
120
+ the current output manager instance.
121
+ """
122
+
123
+ # Settings that can be modified at runtime - shared by all implementations
124
+ _quiet_mode: bool = False
125
+ _show_timestamps: bool = False
126
+
127
+ @classmethod
128
+ def get(cls) -> "OutputManager":
129
+ """Get the current output manager.
130
+
131
+ Returns a RichOutputManager when output is enabled, otherwise returns
132
+ a DisabledOutputManager that provides no-op implementations of all methods.
133
+
134
+ This allows code to call output methods without checking if output is enabled,
135
+ simplifying the calling code.
136
+ """
137
+ return _current_output_manager
138
+
139
+ @classmethod
140
+ def _set(cls, manager: "OutputManager") -> None:
141
+ """Set the current output manager. Used internally by enable_output()."""
142
+ global _current_output_manager
143
+ _current_output_manager = manager
144
+
145
+ @property
146
+ @abstractmethod
147
+ def is_enabled(self) -> bool:
148
+ """Whether output is currently being displayed somewhere.
149
+
150
+ Returns False if using DisabledOutputManager OR if quiet mode is active.
151
+ This reflects whether print(), status(), etc. will actually produce visible output.
152
+ """
153
+ ...
154
+
155
+ @property
156
+ @abstractmethod
157
+ def is_terminal(self) -> bool:
158
+ """Whether the output is connected to a terminal (TTY)."""
159
+ ...
160
+
161
+ @property
162
+ @abstractmethod
163
+ def _show_image_logs(self) -> bool:
164
+ """Whether to show image logs."""
165
+ ...
166
+
167
+ @abstractmethod
168
+ @contextlib.contextmanager
169
+ def display_object_tree(self) -> Generator[None, None, None]:
170
+ """Context manager that displays a tree of objects being created."""
171
+ ...
172
+
173
+ @abstractmethod
174
+ def add_status_row(self) -> StatusRow:
175
+ """Add a status row to the current object tree."""
176
+ ...
177
+
178
+ @abstractmethod
179
+ def print(
180
+ self,
181
+ renderable: Any,
182
+ *,
183
+ stderr: bool = False,
184
+ highlight: bool = True,
185
+ style: Optional[str] = None,
186
+ sep: str = " ",
187
+ end: str = "\n",
188
+ ) -> None:
189
+ """Print a renderable to the console.
190
+
191
+ Args:
192
+ renderable: The content to print.
193
+ stderr: If True, print to stderr instead of stdout.
194
+ highlight: If True, apply syntax highlighting.
195
+ style: Optional Rich style string (e.g., "green", "bold cyan").
196
+ sep: The separator to use between items.
197
+ end: The string to use at the end of the output.
198
+ """
199
+ ...
200
+
201
+ @abstractmethod
202
+ def print_error(self, error_text: str) -> None:
203
+ """Print an error message to stderr, ignoring quiet/disabled mode.
204
+
205
+ This method always prints the error message regardless of quiet mode
206
+ or whether output is disabled. It should be used for critical error
207
+ messages that must always be visible to the user.
208
+
209
+ Args:
210
+ error_text: The error message text to display.
211
+ """
212
+ ...
213
+
214
+ @abstractmethod
215
+ def show_warning(
216
+ self,
217
+ warning: Warning,
218
+ category: type[Warning],
219
+ filename: str,
220
+ lineno: int,
221
+ base_showwarning: Callable[..., None],
222
+ ) -> None:
223
+ """Display a warning message.
224
+
225
+ This method is called by the patched warnings.showwarning for all warnings.
226
+
227
+ Args:
228
+ warning: The warning instance.
229
+ category: The warning category (class).
230
+ filename: The source file where the warning originated.
231
+ lineno: The line number where the warning originated.
232
+ base_showwarning: The original warnings.showwarning function to fall back to.
233
+ """
234
+ ...
235
+
236
+ @abstractmethod
237
+ def print_json(self, data: str) -> None:
238
+ """Print JSON data with formatting."""
239
+ ...
240
+
241
+ @abstractmethod
242
+ def status(self, message: str) -> "StatusContext":
243
+ """Context manager that displays a status spinner with a message.
244
+
245
+ Returns a context manager that shows a spinner while active.
246
+ The returned object has start(), stop(), and update() methods for manual control.
247
+ """
248
+ ...
249
+
250
+ @abstractmethod
251
+ def make_live_spinner(self, message: str) -> AbstractContextManager[None]:
252
+ """Context manager that shows a live spinner with a message.
253
+
254
+ This combines spinner creation and live display into a single interface,
255
+ hiding the implementation details of how spinners are rendered.
256
+ """
257
+ ...
258
+
259
+ @abstractmethod
260
+ def enable_image_logs(self) -> None:
261
+ """Enable showing image logs."""
262
+ ...
263
+
264
+ @abstractmethod
265
+ def show_status_spinner(self, status_text: str = "Running app...") -> AbstractContextManager[None]:
266
+ """Context manager that shows a status spinner.
267
+
268
+ Args:
269
+ status_text: The text to display next to the spinner. Defaults to "Running app...".
270
+ """
271
+ ...
272
+
273
+ @abstractmethod
274
+ def update_app_page_url(self, app_page_url: str) -> None:
275
+ """Update the app page URL for display."""
276
+ ...
277
+
278
+ @abstractmethod
279
+ def function_progress_callback(self, tag: str, total: Optional[int]) -> Callable[[int, int], None]:
280
+ """Get a callback for updating function progress."""
281
+ ...
282
+
283
+ @abstractmethod
284
+ def update_task_state(self, task_id: str, state: int) -> None:
285
+ """Update the state of a task."""
286
+ ...
287
+
288
+ @abstractmethod
289
+ def update_snapshot_progress(self, image_id: str, task_progress: "api_pb2.TaskProgress") -> None:
290
+ """Update snapshot upload progress."""
291
+ ...
292
+
293
+ @abstractmethod
294
+ def update_queueing_progress(
295
+ self, *, function_id: str, completed: int, total: Optional[int], description: Optional[str]
296
+ ) -> None:
297
+ """Update function queueing progress."""
298
+ ...
299
+
300
+ @abstractmethod
301
+ async def put_log_content(self, log: "api_pb2.TaskLogs") -> None:
302
+ """Process and display log content.
303
+
304
+ Note: In RichOutputManager, log output is always displayed even when quiet mode
305
+ is enabled. This is intentional - quiet mode suppresses progress indicators and
306
+ status messages, but not actual log output from running functions/images.
307
+ In contrast, DisabledOutputManager suppresses all output including logs.
308
+ """
309
+ ...
310
+
311
+ async def put_pty_content(self, log: "api_pb2.TaskLogs") -> None:
312
+ """Write PTY content to stdout for interactive terminal sessions.
313
+
314
+ This handles raw PTY output from interactive shells/debuggers, writing
315
+ directly to stdout with proper handling of non-blocking writes.
316
+
317
+ Note: This method is implemented in the base class (not abstract) because
318
+ interactive/raw terminal content is always forwarded to stdout regardless
319
+ of whether output is enabled or quiet mode is active. This ensures that
320
+ interactive debugging sessions (e.g., pdb breakpoints) work correctly.
321
+ """
322
+ stdout = sys.stdout
323
+ if hasattr(stdout, "buffer"):
324
+ data = log.data.encode("utf-8")
325
+ fd = None
326
+ try:
327
+ if hasattr(stdout, "fileno"):
328
+ fd = stdout.fileno()
329
+ except Exception:
330
+ fd = None
331
+
332
+ if fd is not None:
333
+ await write_to_fd(fd, data)
334
+ else:
335
+ # For streams without fileno(), use the normal write/flush path.
336
+ written = 0
337
+ n_retries = 0
338
+ while written < len(data):
339
+ try:
340
+ written += stdout.buffer.write(data[written:])
341
+ stdout.flush()
342
+ except BlockingIOError:
343
+ if n_retries >= 5:
344
+ raise
345
+ n_retries += 1
346
+ await asyncio.sleep(0.1)
347
+ else:
348
+ # stdout isn't always buffered (e.g. %%capture in Jupyter notebooks redirects it to io.StringIO)
349
+ stdout.write(log.data)
350
+ stdout.flush()
351
+
352
+ @abstractmethod
353
+ def flush_lines(self) -> None:
354
+ """Flush any buffered output."""
355
+ ...
356
+
357
+ @abstractmethod
358
+ def transfer_progress(self, type: str) -> AbstractContextManager["TransferProgressContext"]:
359
+ """Context manager for tracking file transfer progress.
360
+
361
+ Args:
362
+ type: Either "download" or "upload".
363
+
364
+ Returns:
365
+ A context manager that yields a TransferProgressContext with a progress() method.
366
+ """
367
+ ...
368
+
369
+ @abstractmethod
370
+ def stop_status_spinner(self) -> None:
371
+ """Stop the status spinner if it's running.
372
+
373
+ This is used to cleanly stop the spinner before entering PTY mode or
374
+ other situations where the spinner would interfere with output.
375
+ """
376
+ ...
377
+
378
+ def set_quiet_mode(self, quiet: bool) -> None:
379
+ """Enable or disable quiet mode.
380
+
381
+ When quiet mode is enabled:
382
+ - All print() output is suppressed, including stderr
383
+ - Progress indicators (spinners, progress bars) are suppressed
384
+ - Warnings (via show_warning) are still displayed
385
+ - Log output (via put_log_content) is still displayed
386
+ - Interactive/raw PTY content (via put_pty_content) is still forwarded to stdout
387
+ - Error output (via print_error) is still displayed
388
+
389
+ Note: This differs from DisabledOutputManager, which suppresses all output
390
+ including logs. Quiet mode is meant for reducing noise while still showing
391
+ relevant content.
392
+ """
393
+ self._quiet_mode = quiet
394
+
395
+ def set_timestamps(self, show: bool) -> None:
396
+ """Enable or disable timestamp display in log output."""
397
+ self._show_timestamps = show
398
+
399
+ @abstractmethod
400
+ def step_completed(self, message: str) -> None:
401
+ """Print a step completed message with appropriate formatting."""
402
+ ...
403
+
404
+
405
+ class _DisabledStatus(StatusContext):
406
+ """No-op status context manager for when output is disabled."""
407
+
408
+ def start(self) -> None:
409
+ pass
410
+
411
+ def stop(self) -> None:
412
+ pass
413
+
414
+ def update(self, status: str) -> None:
415
+ pass
416
+
417
+ def __enter__(self) -> "_DisabledStatus":
418
+ return self
419
+
420
+ def __exit__(self, *args: Any) -> None:
421
+ pass
422
+
423
+
424
+ class DisabledOutputManager(OutputManager):
425
+ """No-op implementation of OutputManager for when output is disabled.
426
+
427
+ Most methods are no-ops that do nothing, allowing code to call output methods
428
+ without checking if the output manager exists.
429
+
430
+ Exceptions:
431
+ - Interactive/raw PTY content (via put_pty_content) is always forwarded to stdout
432
+ to ensure interactive debugging sessions work correctly.
433
+ - Warnings (via show_warning) fall back to Python's default warning display.
434
+ """
435
+
436
+ @property
437
+ def is_enabled(self) -> bool:
438
+ return False
439
+
440
+ @property
441
+ def is_terminal(self) -> bool:
442
+ return sys.stdout.isatty()
443
+
444
+ @property
445
+ def _show_image_logs(self) -> bool:
446
+ return True # Always "show" logs when disabled (don't filter them)
447
+
448
+ @contextlib.contextmanager
449
+ def display_object_tree(self) -> Generator[None, None, None]:
450
+ yield
451
+
452
+ def add_status_row(self) -> StatusRow:
453
+ return DisabledStatusRow()
454
+
455
+ def print(
456
+ self,
457
+ renderable: Any,
458
+ *,
459
+ stderr: bool = False,
460
+ highlight: bool = True,
461
+ style: Optional[str] = None,
462
+ sep: str = " ",
463
+ end: str = "\n",
464
+ ) -> None:
465
+ pass
466
+
467
+ def print_error(self, error_text: str) -> None:
468
+ # Print error to stderr without any formatting (Rich is not available)
469
+ sys.stderr.write(error_text + "\n")
470
+
471
+ def show_warning(
472
+ self,
473
+ warning: Warning,
474
+ category: type[Warning],
475
+ filename: str,
476
+ lineno: int,
477
+ base_showwarning: Callable[..., None],
478
+ ) -> None:
479
+ # When output is disabled, fall back to the default warning display
480
+ base_showwarning(warning, category, filename, lineno, file=None, line=None)
481
+
482
+ def print_json(self, data: str) -> None:
483
+ pass
484
+
485
+ def status(self, message: str) -> StatusContext:
486
+ return _DisabledStatus()
487
+
488
+ @contextlib.contextmanager
489
+ def make_live_spinner(self, message: str) -> Generator[None, None, None]:
490
+ yield
491
+
492
+ def enable_image_logs(self) -> None:
493
+ pass
494
+
495
+ @contextlib.contextmanager
496
+ def show_status_spinner(self, status_text: str = "Running app...") -> Generator[None, None, None]:
497
+ yield
498
+
499
+ def update_app_page_url(self, app_page_url: str) -> None:
500
+ pass
501
+
502
+ def function_progress_callback(self, tag: str, total: Optional[int]) -> Callable[[int, int], None]:
503
+ def noop(completed: int, total: int) -> None:
504
+ pass
505
+
506
+ return noop
507
+
508
+ def update_task_state(self, task_id: str, state: int) -> None:
509
+ pass
510
+
511
+ def update_snapshot_progress(self, image_id: str, task_progress: "api_pb2.TaskProgress") -> None:
512
+ pass
513
+
514
+ def update_queueing_progress(
515
+ self, *, function_id: str, completed: int, total: Optional[int], description: Optional[str]
516
+ ) -> None:
517
+ pass
518
+
519
+ async def put_log_content(self, log: "api_pb2.TaskLogs") -> None:
520
+ pass
521
+
522
+ def flush_lines(self) -> None:
523
+ pass
524
+
525
+ @contextlib.contextmanager
526
+ def transfer_progress(self, type: str) -> Generator[TransferProgressContext, None, None]:
527
+ """No-op transfer progress context manager."""
528
+ yield _DisabledTransferProgress()
529
+
530
+ def stop_status_spinner(self) -> None:
531
+ pass
532
+
533
+ def step_completed(self, message: str) -> None:
534
+ pass
535
+
536
+
537
+ # Singleton instance of the disabled output manager
538
+ _DISABLED_OUTPUT_MANAGER = DisabledOutputManager()
539
+
540
+
541
+ # Module-level state for output management
542
+ _current_output_manager: OutputManager = _DISABLED_OUTPUT_MANAGER