modal 1.5.6.dev10__tar.gz → 1.5.6.dev12__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 (238) hide show
  1. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/PKG-INFO +1 -1
  2. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_functions.py +8 -1
  3. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/execution_context.py +1 -1
  4. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_server.py +19 -1
  5. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/task_command_router_client.py +4 -1
  6. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/time_utils.py +11 -0
  7. modal-1.5.6.dev12/modal/cli/_stats.py +50 -0
  8. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/app.py +8 -9
  9. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/entry_point.py +2 -0
  10. modal-1.5.6.dev12/modal/cli/function.py +402 -0
  11. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/image.py +4 -2
  12. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/import_refs.py +5 -2
  13. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/profile.py +21 -6
  14. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/token.py +9 -4
  15. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/client.pyi +2 -2
  16. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/config.py +46 -6
  17. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/functions.pyi +10 -8
  18. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/runner.py +13 -3
  19. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/server.pyi +37 -0
  20. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/token_flow.py +8 -1
  21. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/types.py +51 -0
  22. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/volume.py +138 -8
  23. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/volume.pyi +28 -0
  24. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/PKG-INFO +1 -1
  25. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/SOURCES.txt +2 -0
  26. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/api_pb2.py +1033 -976
  27. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/api_pb2.pyi +186 -63
  28. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/task_command_router_grpc.py +16 -0
  29. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2.py +45 -25
  30. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2.pyi +29 -0
  31. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2_grpc.py +34 -0
  32. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/task_command_router_pb2_grpc.pyi +12 -0
  33. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_version/__init__.py +1 -1
  34. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/LICENSE +0 -0
  35. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/README.md +0 -0
  36. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/__init__.py +0 -0
  37. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/__main__.py +0 -0
  38. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_billing.py +0 -0
  39. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_clustered_functions.py +0 -0
  40. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_clustered_functions.pyi +0 -0
  41. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_container_entrypoint.py +0 -0
  42. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_environments.py +0 -0
  43. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_function_variants.py +0 -0
  44. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_grpc_client.py +0 -0
  45. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_image.py +0 -0
  46. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_ipython.py +0 -0
  47. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_load_context.py +0 -0
  48. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_location.py +0 -0
  49. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_logs.py +0 -0
  50. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_logs_manager.py +0 -0
  51. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_logs_manager.pyi +0 -0
  52. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_object.py +0 -0
  53. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_output/__init__.py +0 -0
  54. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_output/manager.py +0 -0
  55. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_output/pty.py +0 -0
  56. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_output/rich.py +0 -0
  57. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_output/status.py +0 -0
  58. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_partial_function.py +0 -0
  59. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_resolver.py +0 -0
  60. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_resources.py +0 -0
  61. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/__init__.py +0 -0
  62. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/asgi.py +0 -0
  63. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/container_io_manager.py +0 -0
  64. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/container_io_manager.pyi +0 -0
  65. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/execution_context.pyi +0 -0
  66. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  67. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/task_lifecycle_manager.py +0 -0
  68. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  69. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/telemetry.py +0 -0
  70. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/user_code_event_loop.py +0 -0
  71. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_runtime/user_code_imports.py +0 -0
  72. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_serialization.py +0 -0
  73. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_supports_logs.py +0 -0
  74. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_traceback.py +0 -0
  75. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_tunnel.py +0 -0
  76. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_tunnel.pyi +0 -0
  77. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_type_manager.py +0 -0
  78. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/__init__.py +0 -0
  79. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/app_utils.py +0 -0
  80. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/async_utils.py +0 -0
  81. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/auth_token_manager.py +0 -0
  82. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/blob_utils.py +0 -0
  83. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/browser_utils.py +0 -0
  84. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/bytes_io_segment_payload.py +0 -0
  85. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/curl_utils.py +0 -0
  86. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/deprecation.py +0 -0
  87. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/display_utils.py +0 -0
  88. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/docker_utils.py +0 -0
  89. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/function_utils.py +0 -0
  90. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/git_utils.py +0 -0
  91. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/grpc_testing.py +0 -0
  92. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/grpc_utils.py +0 -0
  93. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/hash_utils.py +0 -0
  94. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/http_utils.py +0 -0
  95. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/jwt_utils.py +0 -0
  96. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/logger.py +0 -0
  97. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/mount_utils.py +0 -0
  98. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/name_utils.py +0 -0
  99. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/package_utils.py +0 -0
  100. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/pattern_utils.py +0 -0
  101. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/proxy_support.py +0 -0
  102. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/rand_pb_testing.py +0 -0
  103. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/sandbox_fs_utils.py +0 -0
  104. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_utils/shell_utils.py +0 -0
  105. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_vendor/__init__.py +0 -0
  106. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  107. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_vendor/cloudpickle.py +0 -0
  108. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_vendor/tblib.py +0 -0
  109. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_vendor/version.py +0 -0
  110. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_watcher.py +0 -0
  111. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/_workspace.py +0 -0
  112. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/app.py +0 -0
  113. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/app.pyi +0 -0
  114. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/billing.py +0 -0
  115. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/billing.pyi +0 -0
  116. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/2023.12.312.txt +0 -0
  117. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/2023.12.txt +0 -0
  118. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/2024.04.txt +0 -0
  119. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/2024.10.txt +0 -0
  120. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/2025.06.txt +0 -0
  121. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/PREVIEW.txt +0 -0
  122. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/README.md +0 -0
  123. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/builder/base-images.json +0 -0
  124. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/call_graph.py +0 -0
  125. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/__init__.py +0 -0
  126. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/_download.py +0 -0
  127. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/_help.py +0 -0
  128. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/_traceback.py +0 -0
  129. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/billing.py +0 -0
  130. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/bootstrap.py +0 -0
  131. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/changelog.py +0 -0
  132. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/cluster.py +0 -0
  133. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/config.py +0 -0
  134. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/container.py +0 -0
  135. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/curl.py +0 -0
  136. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/dashboard.py +0 -0
  137. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/dict.py +0 -0
  138. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/endpoint.py +0 -0
  139. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/environment.py +0 -0
  140. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/launch.py +0 -0
  141. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/logo.py +0 -0
  142. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/network_file_system.py +0 -0
  143. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/programs/__init__.py +0 -0
  144. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/programs/run_jupyter.py +0 -0
  145. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/programs/vscode.py +0 -0
  146. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/queues.py +0 -0
  147. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/run.py +0 -0
  148. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/secret.py +0 -0
  149. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/selector.py +0 -0
  150. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/shell.py +0 -0
  151. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/skills.py +0 -0
  152. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/utils.py +0 -0
  153. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/volume.py +0 -0
  154. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cli/workspace.py +0 -0
  155. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/client.py +0 -0
  156. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cloud_bucket_mount.py +0 -0
  157. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cloud_bucket_mount.pyi +0 -0
  158. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cls.py +0 -0
  159. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/cls.pyi +0 -0
  160. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/container_process.py +0 -0
  161. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/container_process.pyi +0 -0
  162. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/dict.py +0 -0
  163. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/dict.pyi +0 -0
  164. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/environments.py +0 -0
  165. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/environments.pyi +0 -0
  166. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/exception.py +0 -0
  167. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/experimental/__init__.py +0 -0
  168. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/experimental/flash.py +0 -0
  169. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/experimental/flash.pyi +0 -0
  170. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/experimental/ipython.py +0 -0
  171. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/file_io.py +0 -0
  172. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/file_io.pyi +0 -0
  173. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/file_pattern_matcher.py +0 -0
  174. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/functions.py +0 -0
  175. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/image.py +0 -0
  176. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/image.pyi +0 -0
  177. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/io_streams.py +0 -0
  178. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/io_streams.pyi +0 -0
  179. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/mount.py +0 -0
  180. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/mount.pyi +0 -0
  181. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/network_file_system.py +0 -0
  182. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/network_file_system.pyi +0 -0
  183. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/object.py +0 -0
  184. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/object.pyi +0 -0
  185. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/output.py +0 -0
  186. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/parallel_map.py +0 -0
  187. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/parallel_map.pyi +0 -0
  188. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/partial_function.py +0 -0
  189. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/partial_function.pyi +0 -0
  190. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/proxy.py +0 -0
  191. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/proxy.pyi +0 -0
  192. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/py.typed +0 -0
  193. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/queue.py +0 -0
  194. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/queue.pyi +0 -0
  195. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/retries.py +0 -0
  196. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/runner.pyi +0 -0
  197. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/running_app.py +0 -0
  198. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/sandbox.py +0 -0
  199. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/sandbox.pyi +0 -0
  200. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/sandbox_fs.py +0 -0
  201. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/sandbox_fs.pyi +0 -0
  202. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/schedule.py +0 -0
  203. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/scheduler_placement.py +0 -0
  204. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/secret.py +0 -0
  205. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/secret.pyi +0 -0
  206. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/server.py +0 -0
  207. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/serving.py +0 -0
  208. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/serving.pyi +0 -0
  209. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/skills/modal/SKILL.md +0 -0
  210. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/snapshot.py +0 -0
  211. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/snapshot.pyi +0 -0
  212. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/stream_type.py +0 -0
  213. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/token_flow.pyi +0 -0
  214. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/workspace.py +0 -0
  215. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal/workspace.pyi +0 -0
  216. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/dependency_links.txt +0 -0
  217. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/entry_points.txt +0 -0
  218. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/requires.txt +0 -0
  219. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal.egg-info/top_level.txt +0 -0
  220. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/__init__.py +0 -0
  221. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/gen_cli_docs.py +0 -0
  222. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/gen_cli_docs_main.py +0 -0
  223. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/gen_reference_docs.py +0 -0
  224. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/gen_reference_docs_main.py +0 -0
  225. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/mdmd/__init__.py +0 -0
  226. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/mdmd/mdmd.py +0 -0
  227. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/mdmd/signatures.py +0 -0
  228. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/mdmd/types.py +0 -0
  229. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_docs/reference_intro.md +0 -0
  230. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/__init__.py +0 -0
  231. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/api_grpc.py +0 -0
  232. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/api_pb2_grpc.py +0 -0
  233. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/api_pb2_grpc.pyi +0 -0
  234. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/modal_api_grpc.py +0 -0
  235. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_proto/py.typed +0 -0
  236. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/modal_version/__main__.py +0 -0
  237. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/pyproject.toml +0 -0
  238. {modal-1.5.6.dev10 → modal-1.5.6.dev12}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.6.dev10
3
+ Version: 1.5.6.dev12
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -16,6 +16,7 @@ from google.protobuf.message import Message
16
16
  from grpclib import Status
17
17
  from synchronicity.combined_types import MethodWithAio
18
18
 
19
+ from modal._utils.logger import logger
19
20
  from modal.types import CloudBucketMountInfo, FunctionInfo, HttpInfo, VolumeMountInfo
20
21
  from modal_proto import api_pb2
21
22
  from modal_proto.modal_api_grpc import ModalClientModal
@@ -2306,7 +2307,8 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2306
2307
  """Fetch information about the graph of Inputs this FunctionCall is part of.
2307
2308
 
2308
2309
  Note: the call graph data is not populated in real-time, and its capture is best-effort.
2309
- We do not recommend relying on this method for critical use cases.
2310
+ Large call graphs may be truncated. We do not recommend relying on this method
2311
+ for critical use cases.
2310
2312
 
2311
2313
  See the [`modal.types`](/docs/sdk/py/latest/types) reference for information
2312
2314
  on the return values.
@@ -2317,6 +2319,11 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2317
2319
  assert self._client and self._client.stub
2318
2320
  request = api_pb2.FunctionGetCallGraphRequest(function_call_id=self.object_id)
2319
2321
  response = await self._client.stub.FunctionGetCallGraph(request)
2322
+ if response.truncated:
2323
+ logger.warning(
2324
+ f"Call graph for {self.object_id} was truncated; "
2325
+ f"returning the first {len(response.inputs)} inputs. The graph may be incomplete."
2326
+ )
2320
2327
  return _reconstruct_call_graph(response)
2321
2328
 
2322
2329
  @live_method
@@ -109,7 +109,7 @@ _current_input_id: ContextVar = ContextVar("_current_input_id")
109
109
  _current_function_call_id: ContextVar = ContextVar("_current_function_call_id")
110
110
  _current_attempt_token: ContextVar = ContextVar("_current_attempt_token")
111
111
 
112
- _is_currently_importing = False # we set this to True while a container is importing user code
112
+ _is_currently_importing = False
113
113
 
114
114
 
115
115
  @contextmanager
@@ -15,7 +15,7 @@ from ._supports_logs import _LogQueryData
15
15
  from .client import _Client
16
16
  from .cls import is_parameter
17
17
  from .exception import InvalidError
18
- from .types import ServerAutoscalerSettings
18
+ from .types import ServerAutoscalerSettings, ServerInfo
19
19
 
20
20
  if typing.TYPE_CHECKING:
21
21
  import modal.app
@@ -101,6 +101,24 @@ class _Server:
101
101
  """
102
102
  return _ServerLogsManager(self)
103
103
 
104
+ async def info(self, *, refresh: bool = False) -> ServerInfo:
105
+ """Get an overview of a Server's resource requests, associated mounts, http config, etc.
106
+
107
+ This method performs a network request to populate this information if the Server handle is
108
+ a remote lookup whose information has not yet been fetched (e.g. from `Server.from_name(...)`),
109
+ or if `refresh=True`.
110
+
111
+ Args:
112
+ refresh: Always perform a network request. Pass `refresh=True` to ensure that this method
113
+ returns the most up to date information.
114
+
115
+ Returns:
116
+ This returns a [`modal.types.ServerInfo`](https://modal.com/docs/sdk/py/latest/types#ServerInfo)
117
+ dataclass.
118
+ """
119
+
120
+ return ServerInfo._from_function_info(await self._get_service_function().info(refresh=refresh))
121
+
104
122
  @staticmethod
105
123
  def _extract_user_cls(wrapped_user_cls: "type | _PartialFunction") -> type:
106
124
  if isinstance(wrapped_user_cls, _PartialFunction):
@@ -840,8 +840,11 @@ class TaskCommandRouterClient:
840
840
  # We successfully authenticated after a JWT refresh, reset the auth retry flag.
841
841
  if did_auth_retry:
842
842
  did_auth_retry = False
843
- # Reset retry backoff after any successful chunk.
843
+ # Any received chunk is progress: reset the backoff and refill the
844
+ # retry budget so it bounds consecutive failures, not failures over
845
+ # the stream's lifetime.
844
846
  delay_secs = self.stream_stdio_retry_delay_secs
847
+ num_retries_remaining = self.stream_stdio_max_retries
845
848
  # Track it so transient reconnects request the
846
849
  # correct next byte.
847
850
  if is_first_chunk_of_attempt and isinstance(item, sr_pb2.SandboxStdioReadV2Response):
@@ -3,6 +3,17 @@ import re
3
3
  from datetime import datetime, timedelta, timezone, tzinfo
4
4
  from zoneinfo import ZoneInfo
5
5
 
6
+ _DURATION_UNITS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
7
+ _DURATION_EXPR = re.compile(r"^(?P<n>\d+)(?P<unit>s|m|h|d)$")
8
+
9
+
10
+ def parse_duration(value: str) -> timedelta:
11
+ """Parse a compact duration such as ``30m``, ``2h``, or ``1d``."""
12
+ match = _DURATION_EXPR.match(value)
13
+ if not match:
14
+ raise ValueError(f"Invalid duration: '{value}'")
15
+ return timedelta(seconds=int(match.group("n")) * _DURATION_UNITS[match.group("unit")])
16
+
6
17
 
7
18
  def is_leap(year: int):
8
19
  return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
@@ -0,0 +1,50 @@
1
+ # Copyright Modal Labs 2026
2
+ """Shared color styling for stats CLI commands."""
3
+
4
+ from rich.style import Style
5
+
6
+ STATS_HEADING_STYLE = "white"
7
+ STATS_METADATA_STYLE = "bright_black"
8
+ STATS_SECTION_STYLE = "bold white"
9
+ STATS_SUCCESS_STYLE = "green"
10
+ STATS_PROGRESS_STYLE = "cyan"
11
+ STATS_WARNING_STYLE = "yellow"
12
+ STATS_PROBLEM_STYLE = "red"
13
+
14
+ _HIGH_PROBLEM_RATE = 0.05
15
+
16
+
17
+ def stats_style(style: str, use_color: bool) -> str | Style:
18
+ if use_color:
19
+ return style
20
+ if style == STATS_SECTION_STYLE:
21
+ return "bold"
22
+ return Style()
23
+
24
+
25
+ def success_style(count: int, total: int, use_color: bool) -> str | None:
26
+ if use_color and total and count / total > 0.5:
27
+ return STATS_SUCCESS_STYLE
28
+ return None
29
+
30
+
31
+ def progress_style(count: int, use_color: bool) -> str | None:
32
+ if use_color and count:
33
+ return STATS_PROGRESS_STYLE
34
+ return None
35
+
36
+
37
+ def problem_style(count: int, total: int, use_color: bool) -> str | None:
38
+ if not use_color or count == 0:
39
+ return None
40
+ if not total or count / total >= _HIGH_PROBLEM_RATE:
41
+ return STATS_PROBLEM_STYLE
42
+ return STATS_WARNING_STYLE
43
+
44
+
45
+ def percentile_style(p50: float | None, p99: float | None, use_color: bool) -> str | Style:
46
+ if not use_color or p50 is None or p99 is None or p99 <= 0:
47
+ return Style()
48
+ if p50 == 0 or p99 >= 2 * p50:
49
+ return STATS_WARNING_STYLE
50
+ return Style()
@@ -3,7 +3,7 @@ import re
3
3
  import sys
4
4
  import time
5
5
  import warnings
6
- from datetime import datetime, timedelta, timezone
6
+ from datetime import datetime, timezone
7
7
  from json import dumps
8
8
  from typing import Literal, get_args
9
9
 
@@ -25,7 +25,7 @@ from modal.runner import DEPLOYMENT_STRATEGY_TYPE, _stop_and_wait_for_containers
25
25
  from modal_proto import api_pb2
26
26
 
27
27
  from .._logs import _FETCH_LIMIT, _MAX_FETCH_RANGE, LogsFilters
28
- from .._utils.time_utils import locale_tz, timestamp_to_localized_str
28
+ from .._utils.time_utils import locale_tz, parse_duration, timestamp_to_localized_str
29
29
  from ._help import ModalGroup
30
30
  from .utils import (
31
31
  _fetch_app_logs,
@@ -136,9 +136,6 @@ async def list_(env: str | None = None, json: bool = False):
136
136
  display_table(columns, rows, json, title=f"Apps{env_part}")
137
137
 
138
138
 
139
- _RELATIVE_TIME_UNITS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
140
-
141
-
142
139
  def _parse_time_arg(value: str | None, default: datetime) -> datetime:
143
140
  """Parse a time argument that can be a relative duration (e.g. '2h', '30m') or ISO 8601 datetime.
144
141
 
@@ -148,10 +145,12 @@ def _parse_time_arg(value: str | None, default: datetime) -> datetime:
148
145
  if value is None:
149
146
  return default
150
147
 
151
- # Try relative duration: digits followed by a unit letter
152
- if len(value) >= 2 and value[-1] in _RELATIVE_TIME_UNITS and value[:-1].isdigit():
153
- seconds = int(value[:-1]) * _RELATIVE_TIME_UNITS[value[-1]]
154
- return datetime.now(timezone.utc) - timedelta(seconds=seconds)
148
+ try:
149
+ duration = parse_duration(value)
150
+ except ValueError:
151
+ pass
152
+ else:
153
+ return datetime.now(timezone.utc) - duration
155
154
 
156
155
  # Try ISO 8601 datetime
157
156
  try:
@@ -21,6 +21,7 @@ from .dashboard import dashboard
21
21
  from .dict import dict_cli
22
22
  from .endpoint import endpoint_cli
23
23
  from .environment import environment_cli
24
+ from .function import function_cli
24
25
  from .image import image_cli
25
26
  from .launch import launch_cli
26
27
  from .logo import print_logo
@@ -117,6 +118,7 @@ entrypoint_cli.add_command(launch_cli, hidden=True)
117
118
  entrypoint_cli.add_command(app_cli, panel="Deployments")
118
119
  entrypoint_cli.add_command(container_cli, panel="Deployments")
119
120
  entrypoint_cli.add_command(endpoint_cli, panel="Deployments")
121
+ entrypoint_cli.add_command(function_cli, panel="Deployments")
120
122
  # cluster is hidden while multi-node is in beta/experimental
121
123
  entrypoint_cli.add_command(cluster_cli, panel="Deployments", hidden=True)
122
124
 
@@ -0,0 +1,402 @@
1
+ # Copyright Modal Labs 2026
2
+ import json as json_lib
3
+ from datetime import datetime, timedelta, timezone
4
+ from typing import cast
5
+
6
+ import click
7
+ from click import UsageError
8
+ from google.protobuf.timestamp_pb2 import Timestamp
9
+ from rich.style import Style
10
+ from rich.table import Table
11
+ from rich.text import Text
12
+
13
+ from modal._environments import ensure_env
14
+ from modal._object import _get_environment_name
15
+ from modal._utils.async_utils import synchronizer
16
+ from modal._utils.time_utils import parse_duration
17
+ from modal.client import _Client
18
+ from modal.output import OutputManager
19
+ from modal_proto import api_pb2
20
+
21
+ from ._help import ModalGroup
22
+ from ._stats import (
23
+ STATS_HEADING_STYLE,
24
+ STATS_METADATA_STYLE,
25
+ STATS_SECTION_STYLE,
26
+ percentile_style,
27
+ problem_style,
28
+ progress_style,
29
+ stats_style,
30
+ success_style,
31
+ )
32
+ from .app import _parse_time_arg
33
+ from .utils import env_option
34
+
35
+ function_cli = ModalGroup(name="function", help="Inspect deployed Modal Functions.")
36
+
37
+ _DEFAULT_STATS_WINDOW = timedelta(hours=1)
38
+ _DEFAULT_METRIC_PRECISION = 2
39
+ _DISPLAYED_PERCENTILES = ((5000, "p50"), (9000, "p90"), (9900, "p99"))
40
+ _INPUT_METRIC_ORDER = ("Execution time (s)", "End-to-end latency (s)")
41
+ _CONTAINER_METRIC_ORDER = (
42
+ "Startup time (s)",
43
+ "CPU Usage (cores)",
44
+ "Memory Usage (GiB)",
45
+ "GPU Utilization (%)",
46
+ )
47
+
48
+
49
+ async def _resolve_function_id(client: _Client, function_identifier: str, environment_name: str) -> str:
50
+ if function_identifier == "":
51
+ raise UsageError("FUNCTION must be a Function ID (fu-…) or a deployed Function name (APP_NAME/FUNCTION_NAME).")
52
+
53
+ if "/" in function_identifier:
54
+ app_name, function_name = function_identifier.split("/", 1)
55
+
56
+ if not (app_name and function_name):
57
+ raise UsageError(
58
+ "FUNCTION must be a Function ID (fu-…) or a deployed Function name (APP_NAME/FUNCTION_NAME)."
59
+ )
60
+
61
+ response = await client.stub.FunctionGet(
62
+ api_pb2.FunctionGetRequest(
63
+ app_name=app_name,
64
+ object_tag=function_name,
65
+ environment_name=environment_name,
66
+ )
67
+ )
68
+ return response.function_id
69
+
70
+ if function_identifier.startswith("fu-"):
71
+ return function_identifier
72
+
73
+ raise UsageError("FUNCTION must be a Function ID (fu-…) or a deployed Function name (APP_NAME/FUNCTION_NAME).")
74
+
75
+
76
+ def _timestamp(value: datetime) -> Timestamp:
77
+ timestamp = Timestamp()
78
+ timestamp.FromDatetime(value.astimezone(timezone.utc))
79
+ return timestamp
80
+
81
+
82
+ def _percentile_table(rows: list[tuple[Text, Text, Text, Text]], use_color: bool) -> Table:
83
+ table = Table(box=None, pad_edge=False, padding=(0, 2), header_style="")
84
+ table.add_column("", min_width=30)
85
+ percentile_header_style = stats_style(STATS_METADATA_STYLE, use_color)
86
+ table.add_column(Text("p50", style=percentile_header_style), justify="right", min_width=8)
87
+ table.add_column(Text("p90", style=percentile_header_style), justify="right", min_width=8)
88
+ table.add_column(Text("p99", style=percentile_header_style), justify="right", min_width=8)
89
+ for row in rows:
90
+ table.add_row(*row)
91
+ return table
92
+
93
+
94
+ def _count_with_percentage(count: int, total: int, label: str) -> str:
95
+ percentage = count / total if total else 0
96
+ return f"{count:,} {label} ({percentage:.1%})"
97
+
98
+
99
+ def _percentile_name(percentile_basis_points: int) -> str:
100
+ return f"p{percentile_basis_points / 100:g}"
101
+
102
+
103
+ def _distribution_json(distribution: api_pb2.StatsPercentileDistribution) -> dict[str, object]:
104
+ return {
105
+ "unit": distribution.unit,
106
+ "percentiles": {
107
+ _percentile_name(percentile.percentile_basis_points): percentile.value
108
+ for percentile in distribution.percentiles
109
+ },
110
+ }
111
+
112
+
113
+ def _distribution_map_json(
114
+ distributions: dict[str, api_pb2.StatsPercentileDistribution],
115
+ ) -> dict[str, object]:
116
+ return {name: _distribution_json(distribution) for name, distribution in distributions.items()}
117
+
118
+
119
+ def _time_range_stats_json(response: api_pb2.FunctionGetTimeRangeStatsResponse) -> dict[str, object]:
120
+ return {
121
+ "since": response.since.ToDatetime(tzinfo=timezone.utc).isoformat(),
122
+ "until": response.until.ToDatetime(tzinfo=timezone.utc).isoformat(),
123
+ "input_success_count": response.input_success_count,
124
+ "input_failure_count": response.input_failure_count,
125
+ "input_timeout_count": response.input_timeout_count,
126
+ "input_running_at_end_count": response.input_running_at_end_count,
127
+ "input_percentile_stats": _distribution_map_json(
128
+ cast(dict[str, api_pb2.StatsPercentileDistribution], response.input_percentile_stats)
129
+ ),
130
+ "container_started_count": response.container_started_count,
131
+ "container_error_count": response.container_error_count,
132
+ "container_creating_at_end_count": response.container_creating_at_end_count,
133
+ "container_percentile_stats": _distribution_map_json(
134
+ cast(dict[str, api_pb2.StatsPercentileDistribution], response.container_percentile_stats)
135
+ ),
136
+ }
137
+
138
+
139
+ def _stats_json(
140
+ function_id: str,
141
+ history: api_pb2.FunctionGetTimeRangeStatsResponse,
142
+ all_variants: bool,
143
+ ) -> dict[str, object]:
144
+ return {
145
+ "function_id": function_id,
146
+ "all_variants": all_variants,
147
+ "variant_count": history.variant_count,
148
+ **_time_range_stats_json(history),
149
+ }
150
+
151
+
152
+ def _metric_rows(
153
+ distributions: dict[str, api_pb2.StatsPercentileDistribution],
154
+ expected_order: tuple[str, ...],
155
+ use_color: bool,
156
+ ) -> list[tuple[Text, Text, Text, Text]]:
157
+ rows: list[tuple[Text, Text, Text, Text]] = []
158
+ ordered_names = [name for name in expected_order if name in distributions]
159
+ ordered_names.extend(sorted(set(distributions) - set(expected_order)))
160
+ for name in ordered_names:
161
+ distribution = distributions[name]
162
+ values_by_percentile = {
163
+ percentile.percentile_basis_points: percentile.value for percentile in distribution.percentiles
164
+ }
165
+ if not any(basis_points in values_by_percentile for basis_points, _ in _DISPLAYED_PERCENTILES):
166
+ continue
167
+
168
+ p50 = values_by_percentile.get(5000)
169
+ p99 = values_by_percentile.get(9900)
170
+ values = []
171
+ for basis_points, _ in _DISPLAYED_PERCENTILES:
172
+ value = values_by_percentile.get(basis_points)
173
+ value_text = "—" if value is None else f"{value:.{_DEFAULT_METRIC_PRECISION}f}"
174
+ style = percentile_style(p50, p99, use_color) if basis_points == 9900 else Style()
175
+ values.append(Text(value_text, style=style))
176
+ rows.append(
177
+ (
178
+ Text(f" {name}", style=stats_style(STATS_METADATA_STYLE, use_color)),
179
+ values[0],
180
+ values[1],
181
+ values[2],
182
+ )
183
+ )
184
+ return rows
185
+
186
+
187
+ @function_cli.command("stats", no_args_is_help=True)
188
+ @click.argument("function_identifier", metavar="FUNCTION")
189
+ @click.option(
190
+ "--since",
191
+ default=None,
192
+ help="Start of time range. Treated as UTC if timezone not supplied."
193
+ "Accepts an ISO 8601 datetime or relative time such as '2h' or '30m'.",
194
+ )
195
+ @click.option(
196
+ "--until",
197
+ default=None,
198
+ help="End of time range. Treated as UTC if timezone not supplied. Accepts the same argument types as --since.",
199
+ )
200
+ @click.option(
201
+ "--all-variants",
202
+ is_flag=True,
203
+ default=False,
204
+ help="Aggregate the base Function and its variants.",
205
+ )
206
+ @click.option(
207
+ "--container",
208
+ type=str,
209
+ default=None,
210
+ metavar="CONTAINER",
211
+ help="Compute the stats only for this container. Takes precedence over --all-variants.",
212
+ )
213
+ @click.option("--no-color", is_flag=True, default=False, help="Disable colors in the output.")
214
+ @click.option("--json", "json_output", is_flag=True, default=False, help="Output stats as JSON.")
215
+ @env_option
216
+ @synchronizer.create_blocking
217
+ async def stats(
218
+ function_identifier: str,
219
+ since: str | None = None,
220
+ until: str | None = None,
221
+ all_variants: bool = False,
222
+ container: str | None = None,
223
+ no_color: bool = False,
224
+ json_output: bool = False,
225
+ *,
226
+ env: str | None = None,
227
+ ):
228
+ """Show aggregate statistics for a modal Function.
229
+
230
+ FUNCTION may be a Function ID or a deployed Function name in the form
231
+ ``APP_NAME/FUNCTION_NAME``. By default, stats describe the specified Function ID.
232
+ Pass ``--all-variants`` to aggregate across all its direct variants.
233
+
234
+ If --since and --until are omitted, the stats are returned for the last hour.
235
+
236
+ The available metrics and their definitions are subject to change and are provided
237
+ on a best-effort basis. They may be delayed or incomplete. Do not rely on this command
238
+ for autoscaler management.
239
+
240
+ Examples:
241
+
242
+ Show stats from the last hour using a Function ID:
243
+
244
+ ```
245
+ modal function stats fu-abc123
246
+ ```
247
+
248
+ Show stats for a deployed Function by name:
249
+
250
+ ```
251
+ modal function stats my-app/my-function
252
+ ```
253
+
254
+ Show stats across all parameterized instances of a deployed `modal.Cls`:
255
+
256
+ ```
257
+ modal function stats 'my-app/MyClass.*' --all-variants
258
+ ```
259
+
260
+ Show stats from a specific relative window:
261
+
262
+ ```
263
+ modal function stats my-app/my-function --since 5h --until 2h
264
+ ```
265
+
266
+ Show stats for an explicit time range as JSON:
267
+
268
+ ```
269
+ modal function stats my-app/my-function \
270
+ --since 2026-08-28T14:00:00Z \
271
+ --until 2026-08-28T16:00:00Z \
272
+ --json
273
+ ```
274
+
275
+ Show stats for a specific container:
276
+
277
+ ```
278
+ modal function stats my-app/my-function --container ta-12345
279
+ ```
280
+ """
281
+ now = datetime.now(timezone.utc)
282
+ until_dt = _parse_time_arg(until, default=now)
283
+ since_dt = _parse_time_arg(since, default=until_dt - _DEFAULT_STATS_WINDOW)
284
+ if since_dt >= until_dt:
285
+ raise UsageError("--since must be before --until.")
286
+
287
+ history_duration: timedelta | None = _DEFAULT_STATS_WINDOW if since is None else None
288
+ if since is not None and until is None:
289
+ try:
290
+ history_duration = parse_duration(since)
291
+ except ValueError:
292
+ pass
293
+
294
+ environment_name = _get_environment_name(ensure_env(env))
295
+ client = await _Client.from_env()
296
+ function_id = await _resolve_function_id(client, function_identifier, environment_name)
297
+ req = api_pb2.FunctionGetTimeRangeStatsRequest(
298
+ function_id=function_id,
299
+ since=_timestamp(since_dt),
300
+ until=_timestamp(until_dt),
301
+ rollup=all_variants,
302
+ )
303
+ if container:
304
+ req.container_id = container
305
+
306
+ resp = await client.stub.FunctionGetTimeRangeStats(req)
307
+
308
+ if json_output:
309
+ OutputManager.get().print_json(json_lib.dumps(_stats_json(function_id, resp, all_variants)))
310
+ return
311
+
312
+ output = OutputManager.get()
313
+ output.print("")
314
+ heading = f"Function stats for {function_id}"
315
+ if all_variants:
316
+ variant_label = "variant" if resp.variant_count == 1 else "variants"
317
+ heading += f" · all variants ({resp.variant_count:,} {variant_label})"
318
+ use_color = not no_color
319
+ output.print(Text(heading, style=stats_style(STATS_HEADING_STYLE, use_color)))
320
+ output.print("")
321
+ since_label = resp.since.ToDatetime(tzinfo=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
322
+ until_label = resp.until.ToDatetime(tzinfo=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
323
+ range_heading = ""
324
+ if history_duration is not None:
325
+ range_heading = f"Duration {str(history_duration)} · "
326
+ output.print(
327
+ Text(
328
+ f"{range_heading}{since_label} to {until_label} UTC",
329
+ style=stats_style(STATS_METADATA_STYLE, use_color),
330
+ )
331
+ )
332
+ output.print("")
333
+
334
+ completed_input_count = resp.input_success_count + resp.input_failure_count + resp.input_timeout_count
335
+ input_count = completed_input_count + resp.input_running_at_end_count
336
+ input_heading = Text("Inputs", style=stats_style(STATS_SECTION_STYLE, use_color))
337
+ input_heading.append(" " * (46 - len("Inputs")))
338
+ input_heading.append(f"{input_count:,} total (")
339
+ input_heading.append(
340
+ f"{resp.input_running_at_end_count:,} running",
341
+ style=progress_style(resp.input_running_at_end_count, use_color),
342
+ )
343
+ input_heading.append(")")
344
+ output.print(input_heading)
345
+
346
+ input_counts = Text(" ")
347
+ input_counts.append(
348
+ _count_with_percentage(resp.input_success_count, completed_input_count, "succeeded"),
349
+ style=success_style(resp.input_success_count, completed_input_count, use_color),
350
+ )
351
+ input_counts.append(" · ")
352
+ input_counts.append(
353
+ _count_with_percentage(resp.input_failure_count, completed_input_count, "failed"),
354
+ style=problem_style(resp.input_failure_count, completed_input_count, use_color),
355
+ )
356
+ input_counts.append(" · ")
357
+ input_counts.append(
358
+ _count_with_percentage(resp.input_timeout_count, completed_input_count, "timed out"),
359
+ style=problem_style(resp.input_timeout_count, completed_input_count, use_color),
360
+ )
361
+ output.print(input_counts)
362
+ input_rows = _metric_rows(
363
+ cast(dict[str, api_pb2.StatsPercentileDistribution], resp.input_percentile_stats),
364
+ _INPUT_METRIC_ORDER,
365
+ use_color,
366
+ )
367
+ if input_rows:
368
+ output.print("")
369
+ output.print(_percentile_table(input_rows, use_color))
370
+
371
+ output.print("")
372
+ container_count = resp.container_started_count + resp.container_error_count
373
+ container_heading = Text("Containers", style=stats_style(STATS_SECTION_STYLE, use_color))
374
+ container_heading.append(" " * (46 - len("Containers")))
375
+ container_heading.append(f"{container_count:,} total (")
376
+ container_heading.append(
377
+ f"{resp.container_creating_at_end_count:,} creating",
378
+ style=progress_style(resp.container_creating_at_end_count, use_color),
379
+ )
380
+ container_heading.append(")")
381
+ output.print(container_heading)
382
+
383
+ container_counts = Text(" ")
384
+ container_counts.append(
385
+ _count_with_percentage(resp.container_started_count, container_count, "started"),
386
+ style=success_style(resp.container_started_count, container_count, use_color),
387
+ )
388
+ container_counts.append(" · ")
389
+ container_counts.append(
390
+ _count_with_percentage(resp.container_error_count, container_count, "errored"),
391
+ style=problem_style(resp.container_error_count, container_count, use_color),
392
+ )
393
+ output.print(container_counts)
394
+
395
+ container_rows = _metric_rows(
396
+ cast(dict[str, api_pb2.StatsPercentileDistribution], resp.container_percentile_stats),
397
+ _CONTAINER_METRIC_ORDER,
398
+ use_color,
399
+ )
400
+ if container_rows:
401
+ output.print("")
402
+ output.print(_percentile_table(container_rows, use_color))
@@ -58,7 +58,7 @@ async def logs(image_id: str, layers: int | None, all_layers: bool) -> None:
58
58
  effective_layers = None if all_layers else layers or 1
59
59
  image = Image.from_id(image_id)
60
60
  output_mgr = OutputManager.get()
61
- last_message = ""
61
+ last_message: str | None = None
62
62
  async for entry in image.logs.fetch.aio(layers=effective_layers):
63
63
  last_message = entry.message
64
64
  await output_mgr.put_fetched_log(
@@ -69,7 +69,9 @@ async def logs(image_id: str, layers: int | None, all_layers: bool) -> None:
69
69
  )
70
70
  )
71
71
  output_mgr.flush_lines()
72
- if last_message and not last_message.endswith("\n"):
72
+ if last_message is None:
73
+ output_mgr.print(f"No build logs found for Image {image_id}.")
74
+ elif not last_message.endswith("\n"):
73
75
  output_mgr.print("")
74
76
 
75
77
 
@@ -27,6 +27,7 @@ from modal.exception import InvalidError, _CliUserExecutionError
27
27
  from modal.functions import Function
28
28
  from modal.output import OutputManager
29
29
 
30
+ from .._runtime import execution_context
30
31
  from .._utils.async_utils import synchronizer
31
32
 
32
33
 
@@ -74,7 +75,8 @@ def import_file_or_module(import_ref: ImportRef, base_cmd: str = ""):
74
75
  f"Use `{base_cmd} -m {import_ref.file_or_module}` instead.",
75
76
  )
76
77
  try:
77
- module = importlib.import_module(import_ref.file_or_module)
78
+ with execution_context._import_context():
79
+ module = importlib.import_module(import_ref.file_or_module)
78
80
  except Exception as exc:
79
81
  raise _CliUserExecutionError(import_ref.file_or_module) from exc
80
82
  else:
@@ -97,7 +99,8 @@ def import_file_or_module(import_ref: ImportRef, base_cmd: str = ""):
97
99
  sys.modules[module_name] = module
98
100
  try:
99
101
  assert spec.loader
100
- spec.loader.exec_module(module)
102
+ with execution_context._import_context():
103
+ spec.loader.exec_module(module)
101
104
  except Exception as exc:
102
105
  if isinstance(exc, ImportError) and "attempted relative import with no known parent package" in str(exc):
103
106
  if import_ref.file_or_module and import_ref.command and not import_ref.use_module_mode: