modal 1.5.6.dev23__tar.gz → 1.5.6.dev24__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 (240) hide show
  1. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/PKG-INFO +1 -1
  2. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_functions.py +68 -2
  3. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_output/manager.py +8 -0
  4. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_output/rich.py +5 -1
  5. modal-1.5.6.dev24/modal/_output/status.py +92 -0
  6. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_server.py +32 -0
  7. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/config.py +8 -6
  8. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/function.py +225 -1
  9. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/server.py +127 -1
  10. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/utils.py +28 -3
  11. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/client.pyi +2 -2
  12. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/config.py +57 -46
  13. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/functions.pyi +36 -6
  14. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/mount.py +1 -1
  15. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/server.pyi +19 -0
  16. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal.egg-info/PKG-INFO +1 -1
  17. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_version/__init__.py +1 -1
  18. modal-1.5.6.dev23/modal/_output/status.py +0 -98
  19. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/LICENSE +0 -0
  20. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/README.md +0 -0
  21. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/__init__.py +0 -0
  22. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/__main__.py +0 -0
  23. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_billing.py +0 -0
  24. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_clustered_functions.py +0 -0
  25. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_clustered_functions.pyi +0 -0
  26. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_container_entrypoint.py +0 -0
  27. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_environments.py +0 -0
  28. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_function_variants.py +0 -0
  29. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_grpc_client.py +0 -0
  30. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_image.py +0 -0
  31. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_ipython.py +0 -0
  32. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_load_context.py +0 -0
  33. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_location.py +0 -0
  34. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_logs.py +0 -0
  35. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_logs_manager.py +0 -0
  36. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_logs_manager.pyi +0 -0
  37. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_object.py +0 -0
  38. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_output/__init__.py +0 -0
  39. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_output/pty.py +0 -0
  40. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_partial_function.py +0 -0
  41. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_resolver.py +0 -0
  42. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_resources.py +0 -0
  43. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/__init__.py +0 -0
  44. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/asgi.py +0 -0
  45. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/container_io_manager.py +0 -0
  46. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/container_io_manager.pyi +0 -0
  47. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/execution_context.py +0 -0
  48. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/execution_context.pyi +0 -0
  49. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  50. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/task_lifecycle_manager.py +0 -0
  51. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  52. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/telemetry.py +0 -0
  53. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/user_code_event_loop.py +0 -0
  54. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_runtime/user_code_imports.py +0 -0
  55. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_serialization.py +0 -0
  56. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_supports_logs.py +0 -0
  57. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_traceback.py +0 -0
  58. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_tunnel.py +0 -0
  59. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_tunnel.pyi +0 -0
  60. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_type_manager.py +0 -0
  61. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/__init__.py +0 -0
  62. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/app_utils.py +0 -0
  63. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/async_utils.py +0 -0
  64. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/auth_token_manager.py +0 -0
  65. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/blob_utils.py +0 -0
  66. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/browser_utils.py +0 -0
  67. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/bytes_io_segment_payload.py +0 -0
  68. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/curl_utils.py +0 -0
  69. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/deprecation.py +0 -0
  70. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/display_utils.py +0 -0
  71. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/docker_utils.py +0 -0
  72. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/function_utils.py +0 -0
  73. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/git_utils.py +0 -0
  74. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/grpc_testing.py +0 -0
  75. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/grpc_utils.py +0 -0
  76. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/hash_utils.py +0 -0
  77. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/http_utils.py +0 -0
  78. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/idle_countdown.py +0 -0
  79. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/jwt_utils.py +0 -0
  80. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/logger.py +0 -0
  81. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/mount_utils.py +0 -0
  82. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/name_utils.py +0 -0
  83. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/package_utils.py +0 -0
  84. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/pattern_utils.py +0 -0
  85. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/proxy_support.py +0 -0
  86. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/rand_pb_testing.py +0 -0
  87. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/sandbox_fs_utils.py +0 -0
  88. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/shell_utils.py +0 -0
  89. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/task_command_router_client.py +0 -0
  90. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_utils/time_utils.py +0 -0
  91. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_vendor/__init__.py +0 -0
  92. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  93. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_vendor/cloudpickle.py +0 -0
  94. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_vendor/tblib.py +0 -0
  95. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_vendor/version.py +0 -0
  96. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_watcher.py +0 -0
  97. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/_workspace.py +0 -0
  98. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/app.py +0 -0
  99. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/app.pyi +0 -0
  100. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/billing.py +0 -0
  101. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/billing.pyi +0 -0
  102. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/builder/2023.12.312.txt +0 -0
  103. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/builder/2023.12.txt +0 -0
  104. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/builder/2024.04.txt +0 -0
  105. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/builder/2024.10.txt +0 -0
  106. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/builder/2025.06.txt +0 -0
  107. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/builder/PREVIEW.txt +0 -0
  108. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/builder/README.md +0 -0
  109. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/builder/base-images.json +0 -0
  110. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/call_graph.py +0 -0
  111. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/__init__.py +0 -0
  112. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/_download.py +0 -0
  113. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/_help.py +0 -0
  114. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/_logs.py +0 -0
  115. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/_stats.py +0 -0
  116. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/_traceback.py +0 -0
  117. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/app.py +0 -0
  118. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/billing.py +0 -0
  119. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/changelog.py +0 -0
  120. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/cluster.py +0 -0
  121. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/container.py +0 -0
  122. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/curl.py +0 -0
  123. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/dashboard.py +0 -0
  124. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/dict.py +0 -0
  125. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/endpoint.py +0 -0
  126. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/entry_point.py +0 -0
  127. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/environment.py +0 -0
  128. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/image.py +0 -0
  129. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/import_refs.py +0 -0
  130. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/logo.py +0 -0
  131. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/network_file_system.py +0 -0
  132. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/profile.py +0 -0
  133. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/queues.py +0 -0
  134. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/run.py +0 -0
  135. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/secret.py +0 -0
  136. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/selector.py +0 -0
  137. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/shell.py +0 -0
  138. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/skills.py +0 -0
  139. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/token.py +0 -0
  140. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/volume.py +0 -0
  141. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cli/workspace.py +0 -0
  142. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/client.py +0 -0
  143. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cloud_bucket_mount.py +0 -0
  144. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cloud_bucket_mount.pyi +0 -0
  145. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cls.py +0 -0
  146. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/cls.pyi +0 -0
  147. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/container_process.py +0 -0
  148. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/container_process.pyi +0 -0
  149. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/dict.py +0 -0
  150. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/dict.pyi +0 -0
  151. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/environments.py +0 -0
  152. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/environments.pyi +0 -0
  153. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/exception.py +0 -0
  154. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/experimental/__init__.py +0 -0
  155. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/experimental/_flash_container_entry.py +0 -0
  156. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/experimental/flash.py +0 -0
  157. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/experimental/flash.pyi +0 -0
  158. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/experimental/flash_server.py +0 -0
  159. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/experimental/flash_server.pyi +0 -0
  160. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/experimental/ipython.py +0 -0
  161. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/file_pattern_matcher.py +0 -0
  162. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/functions.py +0 -0
  163. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/image.py +0 -0
  164. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/image.pyi +0 -0
  165. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/io_streams.py +0 -0
  166. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/io_streams.pyi +0 -0
  167. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/mount.pyi +0 -0
  168. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/network_file_system.py +0 -0
  169. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/network_file_system.pyi +0 -0
  170. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/object.py +0 -0
  171. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/object.pyi +0 -0
  172. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/outbound_policy.py +0 -0
  173. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/outbound_policy.pyi +0 -0
  174. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/output.py +0 -0
  175. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/parallel_map.py +0 -0
  176. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/parallel_map.pyi +0 -0
  177. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/partial_function.py +0 -0
  178. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/partial_function.pyi +0 -0
  179. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/proxy.py +0 -0
  180. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/proxy.pyi +0 -0
  181. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/py.typed +0 -0
  182. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/queue.py +0 -0
  183. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/queue.pyi +0 -0
  184. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/retries.py +0 -0
  185. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/runner.py +0 -0
  186. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/runner.pyi +0 -0
  187. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/running_app.py +0 -0
  188. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/sandbox.py +0 -0
  189. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/sandbox.pyi +0 -0
  190. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/sandbox_fs.py +0 -0
  191. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/sandbox_fs.pyi +0 -0
  192. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/schedule.py +0 -0
  193. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/scheduler_placement.py +0 -0
  194. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/secret.py +0 -0
  195. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/secret.pyi +0 -0
  196. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/server.py +0 -0
  197. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/serving.py +0 -0
  198. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/serving.pyi +0 -0
  199. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/skills/modal/SKILL.md +0 -0
  200. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/snapshot.py +0 -0
  201. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/snapshot.pyi +0 -0
  202. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/stream_type.py +0 -0
  203. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/token_flow.py +0 -0
  204. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/token_flow.pyi +0 -0
  205. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/types.py +0 -0
  206. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/volume.py +0 -0
  207. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/volume.pyi +0 -0
  208. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/workspace.py +0 -0
  209. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal/workspace.pyi +0 -0
  210. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal.egg-info/SOURCES.txt +0 -0
  211. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal.egg-info/dependency_links.txt +0 -0
  212. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal.egg-info/entry_points.txt +0 -0
  213. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal.egg-info/requires.txt +0 -0
  214. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal.egg-info/top_level.txt +0 -0
  215. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_docs/__init__.py +0 -0
  216. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_docs/gen_cli_docs.py +0 -0
  217. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_docs/gen_cli_docs_main.py +0 -0
  218. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_docs/gen_reference_docs.py +0 -0
  219. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_docs/gen_reference_docs_main.py +0 -0
  220. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_docs/mdmd/__init__.py +0 -0
  221. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_docs/mdmd/mdmd.py +0 -0
  222. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_docs/mdmd/signatures.py +0 -0
  223. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_docs/mdmd/types.py +0 -0
  224. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_docs/reference_intro.md +0 -0
  225. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/__init__.py +0 -0
  226. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/api_grpc.py +0 -0
  227. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/api_pb2.py +0 -0
  228. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/api_pb2.pyi +0 -0
  229. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/api_pb2_grpc.py +0 -0
  230. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/api_pb2_grpc.pyi +0 -0
  231. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/modal_api_grpc.py +0 -0
  232. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/py.typed +0 -0
  233. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/task_command_router_grpc.py +0 -0
  234. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/task_command_router_pb2.py +0 -0
  235. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/task_command_router_pb2.pyi +0 -0
  236. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  237. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  238. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/modal_version/__main__.py +0 -0
  239. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/pyproject.toml +0 -0
  240. {modal-1.5.6.dev23 → modal-1.5.6.dev24}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.6.dev23
3
+ Version: 1.5.6.dev24
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -10,7 +10,7 @@ from collections.abc import AsyncGenerator, AsyncIterator, Callable, Collection,
10
10
  from dataclasses import dataclass
11
11
  from datetime import datetime, timedelta, timezone
12
12
  from pathlib import PurePosixPath
13
- from typing import TYPE_CHECKING, Any
13
+ from typing import TYPE_CHECKING, Any, Literal
14
14
 
15
15
  import typing_extensions
16
16
  from google.protobuf.message import Message
@@ -945,7 +945,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
945
945
  self._hydrate(response.function_id, load_context.client, response.handle_metadata)
946
946
 
947
947
  async def _load(self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
948
- with FunctionCreationStatus(tag) as function_creation_status:
948
+ with FunctionCreationStatus("Server" if is_server else "Function", tag) as function_creation_status:
949
949
  timeout_secs = timeout
950
950
 
951
951
  if app and app._is_interactive_ and not is_builder_function:
@@ -1388,6 +1388,72 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1388
1388
  _load_remote, rep, skip_reload=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
1389
1389
  )
1390
1390
 
1391
+ @classmethod
1392
+ def _from_id(
1393
+ cls,
1394
+ function_id: str,
1395
+ *,
1396
+ load_context_overrides: LoadContext,
1397
+ called_from: Literal["Function", "Server"] = "Function",
1398
+ ):
1399
+ # internal function lookup implementation that allows lookup of class "service functions"
1400
+ # in addition to non-class functions
1401
+ async def _load_remote(
1402
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None
1403
+ ):
1404
+ request = api_pb2.FunctionGetByIdRequest(function_id=function_id)
1405
+ try:
1406
+ response = await load_context.client.stub.FunctionGetById(request)
1407
+ except NotFoundError as exc:
1408
+ # refine the error message
1409
+ msg = f"Lookup failed for {called_from} '{function_id}': {exc}."
1410
+ raise NotFoundError(msg) from None
1411
+ if called_from == "Function" and (response.function.is_server or response.function.is_class):
1412
+ if response.function.is_server:
1413
+ raise InvalidError(f"{function_id} is a Server. Use \n ``modal.Server.from_id('{function_id}')``")
1414
+ raise InvalidError(f"{function_id} is a Cls and cannot be loaded with Function.from_id().")
1415
+ if called_from == "Server" and not response.function.is_server:
1416
+ if response.function.is_class:
1417
+ raise InvalidError(f"{function_id} is a Cls and cannot be loaded with Server.from_id().")
1418
+ raise InvalidError(f"{function_id} is a Function. Use \n ``modal.Function.from_id('{function_id}')``")
1419
+ self._hydrate(function_id, load_context.client, response.handle_metadata)
1420
+ self._function_info = FunctionInfo._from_function_proto(response.function)
1421
+
1422
+ rep = f"modal.Function.from_id('{function_id}')"
1423
+ return cls._from_loader(
1424
+ _load_remote, rep, skip_reload=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
1425
+ )
1426
+
1427
+ @classmethod
1428
+ def from_id(
1429
+ cls: type["_Function"],
1430
+ function_id: str,
1431
+ *,
1432
+ client: _Client | None = None,
1433
+ ):
1434
+ """Reference a Function from a deployed or running App by its ID.
1435
+
1436
+ This is a lazy method that defers hydrating the local
1437
+ object with metadata from Modal servers until the first
1438
+ time it is actually used.
1439
+
1440
+ Args:
1441
+ function_id: ID of the function.
1442
+ client: Modal client to use; defaults to `Client.from_env()` when omitted.
1443
+
1444
+ Returns:
1445
+ A lazy `Function` handle.
1446
+
1447
+ Examples:
1448
+ ```python
1449
+ f = modal.Function.from_id("fu-123")
1450
+ ```
1451
+ """
1452
+ return cls._from_id(
1453
+ function_id,
1454
+ load_context_overrides=LoadContext(client=client),
1455
+ )
1456
+
1391
1457
  @classmethod
1392
1458
  def from_name(
1393
1459
  cls: type["_Function"],
@@ -58,6 +58,11 @@ class StatusRow(ABC):
58
58
  """Append a warning message associated with this row."""
59
59
  ...
60
60
 
61
+ @abstractmethod
62
+ def details(self, message: str) -> None:
63
+ """Append additional details to the row."""
64
+ ...
65
+
61
66
  @abstractmethod
62
67
  def finish(self, message: str) -> None:
63
68
  """Mark the row as finished with the given message."""
@@ -73,6 +78,9 @@ class DisabledStatusRow(StatusRow):
73
78
  def warn(self, warning: "api_pb2.Warning") -> None:
74
79
  pass
75
80
 
81
+ def details(self, message: str) -> None:
82
+ pass
83
+
76
84
  def finish(self, message: str) -> None:
77
85
  pass
78
86
 
@@ -194,6 +194,10 @@ class RichStatusRow(StatusRow):
194
194
  if self._spinner is not None:
195
195
  self._spinner.update(text=message)
196
196
 
197
+ def details(self, message: str) -> None:
198
+ if self._step_node is not None:
199
+ self._step_node.add(f"{message}")
200
+
197
201
  def warn(self, warning: api_pb2.Warning) -> None:
198
202
  if self._step_node is not None:
199
203
  self._step_node.add(f"[yellow]:warning:[/yellow] {warning.message}")
@@ -201,7 +205,7 @@ class RichStatusRow(StatusRow):
201
205
  def finish(self, message: str) -> None:
202
206
  if self._step_node is not None and self._spinner is not None:
203
207
  self._spinner.update(text=message)
204
- self._step_node.label = f"🔨 {message}"
208
+ self._step_node.label = f"[green]✓[/green] {message}"
205
209
 
206
210
 
207
211
  _LOG_FLUSH_THRESHOLD = 32768 # 32KB — flush batched log output roughly every ~400 lines
@@ -0,0 +1,92 @@
1
+ # Copyright Modal Labs 2022
2
+ """Function creation status tracking for Modal CLI.
3
+
4
+ This module contains the FunctionCreationStatus context manager used to
5
+ track and display function creation progress.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from modal_proto import api_pb2
11
+
12
+ from .manager import OutputManager
13
+
14
+
15
+ def _get_annotation_for_web_url(url_info: api_pb2.WebUrlInfo) -> str:
16
+ if url_info.truncated:
17
+ suffix = " [grey70](label truncated)[/grey70]"
18
+ elif url_info.label_stolen:
19
+ suffix = " [grey70](label stolen)[/grey70]"
20
+ else:
21
+ suffix = ""
22
+ return suffix
23
+
24
+
25
+ class FunctionCreationStatus:
26
+ """Context manager for tracking and displaying function creation progress."""
27
+
28
+ type: str
29
+ name: str
30
+ response: api_pb2.FunctionCreateResponse | None = None
31
+
32
+ def __init__(self, type: str, name: str):
33
+ self.type = type
34
+ self.name = name
35
+ self._output_mgr = OutputManager.get()
36
+
37
+ def __enter__(self):
38
+ self.status_row = self._output_mgr.add_status_row()
39
+ self.status_row.message(f"Creating {self.type} {self.name}...")
40
+ return self
41
+
42
+ def set_response(self, resp: api_pb2.FunctionCreateResponse):
43
+ self.response = resp
44
+
45
+ def __exit__(self, exc_type, exc_val, exc_tb):
46
+ if exc_type:
47
+ raise exc_val
48
+
49
+ if not self.response:
50
+ self.status_row.finish(f"Unknown error when creating {self.type} {self.name}")
51
+ return
52
+
53
+ for warning in self.response.server_warnings:
54
+ self.status_row.warn(warning)
55
+
56
+ if web_url := self.response.handle_metadata.web_url:
57
+ url_info = self.response.function.web_url_info
58
+ authenticated = self.response.function.webhook_config.requires_proxy_auth
59
+ auth_suffix = " 🔑" if authenticated else " [yellow](unauthenticated)[/yellow]"
60
+ suffix = _get_annotation_for_web_url(url_info)
61
+
62
+ self.status_row.finish(f"Created {self.type} [green]{self.name}[/green]")
63
+ self.status_row.details(f"[dim underline]{web_url}[/dim underline]{auth_suffix}{suffix}")
64
+ for custom_domain in self.response.function.custom_domain_info:
65
+ self.status_row.details(f"Custom domain: [dim underline]{custom_domain.url}[/dim underline]")
66
+
67
+ elif self.response.function.flash_service_urls:
68
+ # Despite the gRPC API types, Servers only have one URL
69
+ authenticated = self.response.function.is_server and not self.response.function.http_config.unauthenticated
70
+ auth_suffix = " 🔑" if authenticated else " [yellow](unauthenticated)[/yellow]"
71
+ self.status_row.finish(f"Created {self.type} [green]{self.name}[/green]")
72
+ url = self.response.function.flash_service_urls[0]
73
+ self.status_row.details(f"[dim underline]{url}[/dim underline]{auth_suffix}")
74
+
75
+ else:
76
+ self.status_row.finish(f"Created {self.type} [green]{self.name}[/green].")
77
+ if self.response.function.method_definitions_set:
78
+ for method_definition in self.response.function.method_definitions.values():
79
+ if method_definition.web_url:
80
+ url_info = method_definition.web_url_info
81
+ suffix = _get_annotation_for_web_url(url_info)
82
+ authenticated = method_definition.webhook_config.requires_proxy_auth
83
+ auth_suffix = " 🔑" if authenticated else " [yellow](unauthenticated)[/yellow]"
84
+ self.status_row.details(
85
+ f"{method_definition.function_name} -> "
86
+ f"[dim underline]{method_definition.web_url}[/dim underline]{auth_suffix}{suffix}"
87
+ )
88
+ for custom_domain in method_definition.custom_domain_info:
89
+ indent = len(method_definition.function_name) * " "
90
+ self.status_row.details(
91
+ f"{indent} -> [dim underline]{custom_domain.url}[/dim underline]{auth_suffix}"
92
+ )
@@ -273,6 +273,38 @@ class _Server:
273
273
  )
274
274
  return server
275
275
 
276
+ @classmethod
277
+ def from_id(
278
+ cls: type["_Server"],
279
+ server_id: str,
280
+ *,
281
+ client: _Client | None = None,
282
+ ):
283
+ """Reference a Server from a deployed or running App by its ID.
284
+
285
+ This is a lazy method that defers hydrating the local
286
+ object with metadata from Modal servers until the first
287
+ time it is actually used.
288
+
289
+ Args:
290
+ server_id: The ID of the server.
291
+ client: Modal client instance for this session.
292
+
293
+ Examples:
294
+ ```python notest
295
+ server = modal.Server.from_id("fu-456")
296
+ ```
297
+ """
298
+ load_context_overrides = LoadContext(client=client)
299
+
300
+ server = _Server()
301
+ server._service_function = _Function._from_id(
302
+ server_id,
303
+ load_context_overrides=load_context_overrides,
304
+ called_from="Server",
305
+ )
306
+ return server
307
+
276
308
  def _is_local(self) -> bool:
277
309
  """Returns True if this Server has local source code available."""
278
310
  return self._user_cls is not None
@@ -1,5 +1,5 @@
1
1
  # Copyright Modal Labs 2022
2
- import json
2
+ from json import dumps
3
3
 
4
4
  import click
5
5
 
@@ -21,11 +21,12 @@ config_cli = ModalGroup(
21
21
  )
22
22
 
23
23
 
24
- @config_cli.command("show", help="Show current configuration values (debugging command).")
24
+ @config_cli.command("show", help="Show current configuration values.")
25
25
  @click.option("--redact/--no-redact", default=True, help="Redact secret credential values.")
26
- def show(redact: bool):
27
- # This is just a test command
28
- config_dict = config.to_dict()
26
+ @click.option("--include-internal", is_flag=True, help="Include internal feature flags.", hidden=True)
27
+ @click.option("--json", is_flag=True, default=False)
28
+ def show(redact: bool, include_internal: bool, json: bool):
29
+ config_dict = config.to_dict(include_internal=include_internal)
29
30
  if redact and config_dict.get("token_secret"):
30
31
  config_dict["token_secret"] = "***"
31
32
  if redact and config_dict.get("oauth_refresh_token"):
@@ -33,7 +34,8 @@ def show(redact: bool):
33
34
  if redact and config_dict.get("oauth_client_secret"):
34
35
  config_dict["oauth_client_secret"] = "***"
35
36
 
36
- OutputManager.get().print_json(json.dumps(config_dict))
37
+ # Accept --json as a no-op for consistency; we may change the default formatting later
38
+ OutputManager.get().print_json(dumps(config_dict))
37
39
 
38
40
 
39
41
  SET_DEFAULT_ENV_HELP = """Set the default Modal environment for the active profile
@@ -37,7 +37,14 @@ from ._stats import (
37
37
  stats_style,
38
38
  success_style,
39
39
  )
40
- from .utils import _resolve_function_id, display_table, env_option, humanize_filesize
40
+ from .utils import (
41
+ HEADER_ONLY,
42
+ _resolve_function_id,
43
+ display_table,
44
+ env_option,
45
+ grouped_utc_timestamp,
46
+ humanize_filesize,
47
+ )
41
48
 
42
49
  function_cli = ModalGroup(name="function", help="Inspect Modal Functions.")
43
50
 
@@ -48,6 +55,46 @@ _CONTAINER_METRIC_ORDER = (
48
55
  "Memory Usage (GiB)",
49
56
  "GPU Utilization (%)",
50
57
  )
58
+ _DEFAULT_CALL_TAIL = 100
59
+ _MAX_CALL_TAIL = 1000
60
+ _FAILURE_STATUSES = {
61
+ api_pb2.FUNCTION_CALL_INPUT_STATUS_FAILURE: "Failure",
62
+ api_pb2.FUNCTION_CALL_INPUT_STATUS_TIMEOUT: "Timeout",
63
+ api_pb2.FUNCTION_CALL_INPUT_STATUS_TERMINATED: "Terminated",
64
+ api_pb2.FUNCTION_CALL_INPUT_STATUS_INIT_FAILURE: "Failure",
65
+ api_pb2.FUNCTION_CALL_INPUT_STATUS_INTERNAL_FAILURE: "Failure",
66
+ api_pb2.FUNCTION_CALL_INPUT_STATUS_IDLE_TIMEOUT: "Timeout (idle)",
67
+ api_pb2.FUNCTION_CALL_INPUT_STATUS_MEMORY_MANAGER_EVICTION: "Evicted (over memory request)",
68
+ }
69
+
70
+
71
+ def _optional_duration(value: float, present: bool) -> str:
72
+ if not present:
73
+ return "—"
74
+ return f"{value:.2f}"
75
+
76
+
77
+ def _function_call_status(status: api_pb2.FunctionCallInputStatus.ValueType, json_output: bool = False) -> str | int:
78
+ try:
79
+ name = api_pb2.FunctionCallInputStatus.Name(status)
80
+ except ValueError:
81
+ return status
82
+ label = name.removeprefix("FUNCTION_CALL_INPUT_STATUS_").replace("_", " ")
83
+ if json_output:
84
+ return label.lower()
85
+ return _FAILURE_STATUSES.get(status, label.title())
86
+
87
+
88
+ def _function_call_status_cell(status: api_pb2.FunctionCallInputStatus.ValueType, no_color: bool = False) -> Text:
89
+ style = None
90
+ if status == api_pb2.FUNCTION_CALL_INPUT_STATUS_PENDING:
91
+ style = "yellow"
92
+ elif status == api_pb2.FUNCTION_CALL_INPUT_STATUS_RUNNING:
93
+ style = "green"
94
+ elif status in _FAILURE_STATUSES:
95
+ style = "red"
96
+ label = str(_function_call_status(status))
97
+ return Text(label, style=style) if style and not no_color else Text(label)
51
98
 
52
99
 
53
100
  def _time_range_stats_json(response: api_pb2.FunctionGetTimeRangeStatsResponse) -> dict[str, object]:
@@ -307,6 +354,183 @@ async def stats(
307
354
  output.print(_percentile_table(container_rows, use_color))
308
355
 
309
356
 
357
+ @function_cli.command("calls", no_args_is_help=True)
358
+ @click.argument("function_identifier", metavar="FUNCTION")
359
+ @click.option(
360
+ "-n",
361
+ "--tail",
362
+ type=click.IntRange(min=1, max=_MAX_CALL_TAIL),
363
+ default=_DEFAULT_CALL_TAIL,
364
+ show_default=True,
365
+ help="Show up to the last N Function inputs.",
366
+ )
367
+ @click.option(
368
+ "--all-variants",
369
+ is_flag=True,
370
+ default=False,
371
+ help="Include inputs from the base Function and all its variants.",
372
+ )
373
+ @click.option(
374
+ "--show-function-call-id",
375
+ is_flag=True,
376
+ default=False,
377
+ help="Include the Function Call ID in table output.",
378
+ )
379
+ @click.option("--json", "json_output", is_flag=True, default=False, help="Output calls as JSON.")
380
+ @click.option("--no-color", "no_color", is_flag=True, default=False, help="Disable colors in the output.")
381
+ @env_option
382
+ @synchronizer.create_blocking
383
+ async def calls(
384
+ function_identifier: str,
385
+ tail: int = _DEFAULT_CALL_TAIL,
386
+ all_variants: bool = False,
387
+ show_function_call_id: bool = False,
388
+ json_output: bool = False,
389
+ no_color: bool = False,
390
+ *,
391
+ env: str | None = None,
392
+ ) -> None:
393
+ """Show recent inputs for a Modal Function.
394
+
395
+ FUNCTION may be a Function ID or a deployed Function name in the form
396
+ ``APP_NAME/FUNCTION_NAME``. Each unique input corresponds to one entry
397
+ in the output.
398
+
399
+ Examples:
400
+
401
+ ```
402
+ modal function calls my-app/my-function
403
+ ```
404
+
405
+ Show recent calls across all variants of a Cls:
406
+
407
+ ```
408
+ modal function calls 'my-app/MyClass.*' --all-variants --tail 500
409
+ ```
410
+
411
+ Disable color in the output:
412
+
413
+ ```
414
+ modal function calls my-app/my-function --no-color
415
+ ```
416
+ """
417
+ environment_name = _get_environment_name(ensure_env(env))
418
+ client = await _Client.from_env()
419
+ function_id, _ = await _resolve_function_id(
420
+ client,
421
+ function_identifier,
422
+ environment_name,
423
+ command="calls",
424
+ )
425
+ response = await client.stub.FunctionCallFetch(
426
+ api_pb2.FunctionCallFetchRequest(
427
+ function_id=function_id,
428
+ tail=api_pb2.FunctionCallFetchRequest.Tail(count=tail),
429
+ all_variants=all_variants,
430
+ )
431
+ )
432
+
433
+ if json_output:
434
+ OutputManager.get().print_json(
435
+ json_lib.dumps(
436
+ [
437
+ {
438
+ "function_call_id": call.function_call_id,
439
+ "service_method_name": (
440
+ call.service_method_name if call.HasField("service_method_name") else None
441
+ ),
442
+ "enqueued_at": (call.enqueued_at.ToJsonString() if call.HasField("enqueued_at") else None),
443
+ "started_at": (call.started_at.ToJsonString() if call.HasField("started_at") else None),
444
+ "container_id": call.container_id if call.HasField("container_id") else None,
445
+ "startup_time_seconds": (
446
+ call.startup_time_seconds if call.HasField("startup_time_seconds") else None
447
+ ),
448
+ "execution_time_seconds": (
449
+ call.execution_time_seconds if call.HasField("execution_time_seconds") else None
450
+ ),
451
+ "status": _function_call_status(call.status, True),
452
+ }
453
+ for call in response.function_call_inputs
454
+ ]
455
+ )
456
+ )
457
+ return
458
+
459
+ show_service_method_name = bool(response.function_call_inputs) and all(
460
+ call.HasField("service_method_name") for call in response.function_call_inputs
461
+ )
462
+ rows: list[list[Text | str]] = []
463
+ previous_enqueued_date = None
464
+ for call in response.function_call_inputs:
465
+ if call.HasField("enqueued_at"):
466
+ enqueued_at, enqueued_date = grouped_utc_timestamp(call.enqueued_at, previous_enqueued_date)
467
+ previous_enqueued_date = enqueued_date
468
+ else:
469
+ enqueued_at = "—"
470
+ enqueued_date = None
471
+
472
+ if call.HasField("started_at") and enqueued_date:
473
+ started_secs = (
474
+ call.started_at.ToDatetime(tzinfo=timezone.utc) - call.enqueued_at.ToDatetime(tzinfo=timezone.utc)
475
+ ).total_seconds()
476
+ started_at = f"{started_secs:.2f}"
477
+ else:
478
+ started_at = "—"
479
+
480
+ row: list[Text | str] = [Text(enqueued_at), Text(started_at)]
481
+ if show_function_call_id:
482
+ row.append(Text(call.function_call_id or "—"))
483
+ if show_service_method_name:
484
+ row.append(Text(call.service_method_name))
485
+ row.extend(
486
+ [
487
+ Text(
488
+ call.container_id if call.HasField("container_id") else "—",
489
+ ),
490
+ Text(
491
+ _optional_duration(call.execution_time_seconds, call.HasField("execution_time_seconds")),
492
+ ),
493
+ _function_call_status_cell(call.status, no_color=no_color),
494
+ ]
495
+ )
496
+ rows.append(row)
497
+
498
+ title = f"Function calls for {function_id}"
499
+ if all_variants:
500
+ title += " · all variants"
501
+ output = OutputManager.get()
502
+ output.print("")
503
+ output.print(Text(title))
504
+ output.print(
505
+ Text(
506
+ f"Displaying {len(rows):,} {'row' if len(rows) == 1 else 'rows'}",
507
+ style=STATS_METADATA_STYLE if not no_color else "",
508
+ )
509
+ )
510
+ output.print("")
511
+ columns: list[str | Column] = [
512
+ Column("Enqueued (UTC)", no_wrap=True, justify="right"),
513
+ Column("Queue Time (s)", justify="right"),
514
+ ]
515
+ if show_function_call_id:
516
+ columns.append("Function Call ID")
517
+ if show_service_method_name:
518
+ columns.append("Method")
519
+ columns.extend(
520
+ [
521
+ Column("Container", width=29),
522
+ Column("Execution (s)", justify="right"),
523
+ "Status",
524
+ ]
525
+ )
526
+ display_table(
527
+ columns,
528
+ rows,
529
+ table_box=HEADER_ONLY,
530
+ border_style=STATS_METADATA_STYLE if not no_color else "",
531
+ )
532
+
533
+
310
534
  @function_cli.command("logs", no_args_is_help=True)
311
535
  @click.argument("function_ref")
312
536
  @click.option("-f", "--follow", is_flag=True, default=False, help="Stream log output until interrupted")