modal 1.5.6.dev23__tar.gz → 1.5.6.dev25__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.dev25}/PKG-INFO +1 -1
  2. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/__init__.py +2 -1
  3. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_functions.py +68 -2
  4. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_output/manager.py +8 -0
  5. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_output/rich.py +5 -1
  6. modal-1.5.6.dev25/modal/_output/status.py +92 -0
  7. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_server.py +32 -0
  8. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_traceback.py +14 -2
  9. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/grpc_utils.py +43 -1
  10. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/task_command_router_client.py +45 -5
  11. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/config.py +8 -6
  12. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/function.py +225 -1
  13. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/server.py +127 -1
  14. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/token.py +20 -2
  15. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/utils.py +28 -3
  16. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/client.pyi +2 -2
  17. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/config.py +57 -46
  18. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/functions.pyi +36 -6
  19. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/mount.py +1 -1
  20. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/secret.py +69 -1
  21. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/secret.pyi +70 -0
  22. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/server.pyi +19 -0
  23. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/token_flow.py +4 -2
  24. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/token_flow.pyi +15 -3
  25. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/types.py +1 -0
  26. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/PKG-INFO +1 -1
  27. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/api_grpc.py +16 -0
  28. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/api_pb2.py +409 -389
  29. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/api_pb2.pyi +34 -1
  30. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/api_pb2_grpc.py +33 -0
  31. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/api_pb2_grpc.pyi +10 -0
  32. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/modal_api_grpc.py +1 -0
  33. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_version/__init__.py +1 -1
  34. modal-1.5.6.dev23/modal/_output/status.py +0 -98
  35. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/LICENSE +0 -0
  36. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/README.md +0 -0
  37. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/__main__.py +0 -0
  38. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_billing.py +0 -0
  39. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_clustered_functions.py +0 -0
  40. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_clustered_functions.pyi +0 -0
  41. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_container_entrypoint.py +0 -0
  42. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_environments.py +0 -0
  43. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_function_variants.py +0 -0
  44. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_grpc_client.py +0 -0
  45. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_image.py +0 -0
  46. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_ipython.py +0 -0
  47. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_load_context.py +0 -0
  48. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_location.py +0 -0
  49. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_logs.py +0 -0
  50. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_logs_manager.py +0 -0
  51. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_logs_manager.pyi +0 -0
  52. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_object.py +0 -0
  53. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_output/__init__.py +0 -0
  54. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_output/pty.py +0 -0
  55. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_partial_function.py +0 -0
  56. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_resolver.py +0 -0
  57. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_resources.py +0 -0
  58. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/__init__.py +0 -0
  59. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/asgi.py +0 -0
  60. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/container_io_manager.py +0 -0
  61. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/container_io_manager.pyi +0 -0
  62. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/execution_context.py +0 -0
  63. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/execution_context.pyi +0 -0
  64. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  65. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/task_lifecycle_manager.py +0 -0
  66. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/task_lifecycle_manager.pyi +0 -0
  67. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/telemetry.py +0 -0
  68. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/user_code_event_loop.py +0 -0
  69. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_runtime/user_code_imports.py +0 -0
  70. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_serialization.py +0 -0
  71. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_supports_logs.py +0 -0
  72. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_tunnel.py +0 -0
  73. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_tunnel.pyi +0 -0
  74. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_type_manager.py +0 -0
  75. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/__init__.py +0 -0
  76. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/app_utils.py +0 -0
  77. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/async_utils.py +0 -0
  78. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/auth_token_manager.py +0 -0
  79. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/blob_utils.py +0 -0
  80. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/browser_utils.py +0 -0
  81. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/bytes_io_segment_payload.py +0 -0
  82. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/curl_utils.py +0 -0
  83. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/deprecation.py +0 -0
  84. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/display_utils.py +0 -0
  85. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/docker_utils.py +0 -0
  86. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/function_utils.py +0 -0
  87. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/git_utils.py +0 -0
  88. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/grpc_testing.py +0 -0
  89. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/hash_utils.py +0 -0
  90. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/http_utils.py +0 -0
  91. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/idle_countdown.py +0 -0
  92. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/jwt_utils.py +0 -0
  93. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/logger.py +0 -0
  94. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/mount_utils.py +0 -0
  95. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/name_utils.py +0 -0
  96. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/package_utils.py +0 -0
  97. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/pattern_utils.py +0 -0
  98. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/proxy_support.py +0 -0
  99. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/rand_pb_testing.py +0 -0
  100. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/sandbox_fs_utils.py +0 -0
  101. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/shell_utils.py +0 -0
  102. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_utils/time_utils.py +0 -0
  103. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_vendor/__init__.py +0 -0
  104. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  105. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_vendor/cloudpickle.py +0 -0
  106. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_vendor/tblib.py +0 -0
  107. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_vendor/version.py +0 -0
  108. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_watcher.py +0 -0
  109. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/_workspace.py +0 -0
  110. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/app.py +0 -0
  111. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/app.pyi +0 -0
  112. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/billing.py +0 -0
  113. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/billing.pyi +0 -0
  114. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/2023.12.312.txt +0 -0
  115. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/2023.12.txt +0 -0
  116. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/2024.04.txt +0 -0
  117. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/2024.10.txt +0 -0
  118. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/2025.06.txt +0 -0
  119. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/PREVIEW.txt +0 -0
  120. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/README.md +0 -0
  121. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/builder/base-images.json +0 -0
  122. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/call_graph.py +0 -0
  123. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/__init__.py +0 -0
  124. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/_download.py +0 -0
  125. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/_help.py +0 -0
  126. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/_logs.py +0 -0
  127. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/_stats.py +0 -0
  128. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/_traceback.py +0 -0
  129. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/app.py +0 -0
  130. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/billing.py +0 -0
  131. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/changelog.py +0 -0
  132. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/cluster.py +0 -0
  133. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/container.py +0 -0
  134. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/curl.py +0 -0
  135. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/dashboard.py +0 -0
  136. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/dict.py +0 -0
  137. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/endpoint.py +0 -0
  138. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/entry_point.py +0 -0
  139. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/environment.py +0 -0
  140. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/image.py +0 -0
  141. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/import_refs.py +0 -0
  142. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/logo.py +0 -0
  143. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/network_file_system.py +0 -0
  144. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/profile.py +0 -0
  145. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/queues.py +0 -0
  146. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/run.py +0 -0
  147. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/secret.py +0 -0
  148. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/selector.py +0 -0
  149. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/shell.py +0 -0
  150. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/skills.py +0 -0
  151. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/volume.py +0 -0
  152. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cli/workspace.py +0 -0
  153. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/client.py +0 -0
  154. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cloud_bucket_mount.py +0 -0
  155. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cloud_bucket_mount.pyi +0 -0
  156. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cls.py +0 -0
  157. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/cls.pyi +0 -0
  158. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/container_process.py +0 -0
  159. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/container_process.pyi +0 -0
  160. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/dict.py +0 -0
  161. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/dict.pyi +0 -0
  162. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/environments.py +0 -0
  163. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/environments.pyi +0 -0
  164. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/exception.py +0 -0
  165. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/__init__.py +0 -0
  166. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/_flash_container_entry.py +0 -0
  167. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/flash.py +0 -0
  168. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/flash.pyi +0 -0
  169. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/flash_server.py +0 -0
  170. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/flash_server.pyi +0 -0
  171. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/experimental/ipython.py +0 -0
  172. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/file_pattern_matcher.py +0 -0
  173. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/functions.py +0 -0
  174. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/image.py +0 -0
  175. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/image.pyi +0 -0
  176. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/io_streams.py +0 -0
  177. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/io_streams.pyi +0 -0
  178. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/mount.pyi +0 -0
  179. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/network_file_system.py +0 -0
  180. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/network_file_system.pyi +0 -0
  181. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/object.py +0 -0
  182. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/object.pyi +0 -0
  183. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/outbound_policy.py +0 -0
  184. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/outbound_policy.pyi +0 -0
  185. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/output.py +0 -0
  186. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/parallel_map.py +0 -0
  187. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/parallel_map.pyi +0 -0
  188. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/partial_function.py +0 -0
  189. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/partial_function.pyi +0 -0
  190. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/proxy.py +0 -0
  191. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/proxy.pyi +0 -0
  192. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/py.typed +0 -0
  193. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/queue.py +0 -0
  194. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/queue.pyi +0 -0
  195. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/retries.py +0 -0
  196. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/runner.py +0 -0
  197. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/runner.pyi +0 -0
  198. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/running_app.py +0 -0
  199. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/sandbox.py +0 -0
  200. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/sandbox.pyi +0 -0
  201. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/sandbox_fs.py +0 -0
  202. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/sandbox_fs.pyi +0 -0
  203. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/schedule.py +0 -0
  204. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/scheduler_placement.py +0 -0
  205. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/server.py +0 -0
  206. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/serving.py +0 -0
  207. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/serving.pyi +0 -0
  208. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/skills/modal/SKILL.md +0 -0
  209. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/snapshot.py +0 -0
  210. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/snapshot.pyi +0 -0
  211. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/stream_type.py +0 -0
  212. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/volume.py +0 -0
  213. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/volume.pyi +0 -0
  214. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/workspace.py +0 -0
  215. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal/workspace.pyi +0 -0
  216. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/SOURCES.txt +0 -0
  217. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/dependency_links.txt +0 -0
  218. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/entry_points.txt +0 -0
  219. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/requires.txt +0 -0
  220. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal.egg-info/top_level.txt +0 -0
  221. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/__init__.py +0 -0
  222. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/gen_cli_docs.py +0 -0
  223. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/gen_cli_docs_main.py +0 -0
  224. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/gen_reference_docs.py +0 -0
  225. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/gen_reference_docs_main.py +0 -0
  226. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/mdmd/__init__.py +0 -0
  227. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/mdmd/mdmd.py +0 -0
  228. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/mdmd/signatures.py +0 -0
  229. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/mdmd/types.py +0 -0
  230. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_docs/reference_intro.md +0 -0
  231. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/__init__.py +0 -0
  232. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/py.typed +0 -0
  233. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/task_command_router_grpc.py +0 -0
  234. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2.py +0 -0
  235. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2.pyi +0 -0
  236. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  237. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  238. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/modal_version/__main__.py +0 -0
  239. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/pyproject.toml +0 -0
  240. {modal-1.5.6.dev23 → modal-1.5.6.dev25}/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.dev25
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -9,7 +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, types
12
+ from . import billing, experimental, types
13
13
  from ._runtime.execution_context import current_function_call_id, current_input_id, interact, is_local
14
14
  from ._tunnel import Tunnel, forward
15
15
  from .app import App
@@ -96,6 +96,7 @@ __all__ = [
96
96
  "enable_output",
97
97
  "enter",
98
98
  "exit",
99
+ "experimental",
99
100
  "fastapi_endpoint",
100
101
  "forward",
101
102
  "is_local",
@@ -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
@@ -123,13 +123,25 @@ def print_exception(exc: type[BaseException] | None, value: BaseException | None
123
123
  print(*notes, sep="\n", file=sys.stderr) # noqa: T201
124
124
 
125
125
 
126
- def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
126
+ _server_warning_registry: dict[Any, Any] = {}
127
+ SERVER_WARNING_REGISTRY_LIMIT = 2048
128
+
129
+
130
+ def print_server_warning(message: str):
127
131
  """Issue a warning originating from the server with empty metadata about local origin.
128
132
 
129
133
  When using the Modal CLI, these warnings should get caught and coerced into Rich panels.
130
134
  """
135
+ if len(_server_warning_registry) > SERVER_WARNING_REGISTRY_LIMIT:
136
+ # Start over instead of going quiet: overflow costs a repeat, never a hidden warning.
137
+ _server_warning_registry.clear()
138
+ warnings.warn_explicit(message, ServerWarning, "<modal-server>", 0, registry=_server_warning_registry)
139
+
140
+
141
+ def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
142
+ """Issue warnings that the server attached to a response message."""
131
143
  for warning in server_warnings:
132
- warnings.warn_explicit(warning.message, ServerWarning, "<modal-server>", 0)
144
+ print_server_warning(warning.message)
133
145
 
134
146
 
135
147
  # for some reason, the traceback cleanup here can't be moved into a context manager :(
@@ -24,12 +24,13 @@ from grpclib.config import Configuration
24
24
  from grpclib.encoding.base import CodecBase, StatusDetailsCodecBase
25
25
  from grpclib.exceptions import StreamTerminatedError
26
26
  from grpclib.protocol import H2Protocol
27
+ from h2.exceptions import ProtocolError as H2ProtocolError
27
28
 
28
29
  from modal.exception import ClientClosed, ConnectionError, NotFoundError
29
30
  from modal_proto import api_pb2
30
31
  from modal_version import __version__
31
32
 
32
- from .._traceback import suppress_tb_frame
33
+ from .._traceback import print_server_warning, suppress_tb_frame
33
34
  from ..config import config
34
35
  from .async_utils import TaskContext, retry
35
36
  from .logger import logger
@@ -130,6 +131,24 @@ class ModalChannel(grpclib.client.Channel):
130
131
  return await super().__connect__()
131
132
 
132
133
  async def _create_connection(self) -> H2Protocol:
134
+ target = self._path if self.__use_unix_socket else f"{self.__target_host}:{self.__target_port}"
135
+ tls = self.__ssl_context is not None
136
+ started_at = time.monotonic()
137
+ # Logged before awaiting: a reconnect is invisible until it resolves, which
138
+ # can be long after the RPC waiting on it appears to hang.
139
+ logger.debug(f"Starting connection attempt to {target} (TLS={tls})")
140
+ try:
141
+ protocol = await self._open_connection()
142
+ except (asyncio.CancelledError, Exception) as exc:
143
+ outcome = "cancelled" if isinstance(exc, asyncio.CancelledError) else "failed"
144
+ elapsed = time.monotonic() - started_at
145
+ logger.debug(f"Connection attempt to {target} {outcome} after {elapsed:.3f}s: {exc!r}")
146
+ raise
147
+ elapsed = time.monotonic() - started_at
148
+ logger.debug(f"Connection to {target} established after {elapsed:.3f}s")
149
+ return protocol
150
+
151
+ async def _open_connection(self) -> H2Protocol:
133
152
  if self.__use_unix_socket:
134
153
  return await super()._create_connection()
135
154
 
@@ -308,6 +327,20 @@ def create_channel_config(*, sustained_keepalive: bool = False) -> grpclib.confi
308
327
  )
309
328
 
310
329
 
330
+ # Header the server uses to attach a non-fatal message to any response, one entry per
331
+ # percent-encoded warning. It rides in the trailing metadata, which grpclib also
332
+ # surfaces for trailers-only responses, i.e. failed requests.
333
+ SERVER_WARNING_HEADER = "x-modal-warning"
334
+
335
+
336
+ def _issue_server_warnings(metadata: Any) -> None:
337
+ """Issue the warnings in a response's metadata, which `print_server_warning` deduplicates."""
338
+ for key, encoded_message in metadata.items():
339
+ if key != SERVER_WARNING_HEADER:
340
+ continue
341
+ print_server_warning(urllib.parse.unquote(encoded_message))
342
+
343
+
311
344
  def create_channel(
312
345
  server_url: str,
313
346
  metadata: dict[str, str] = {},
@@ -356,6 +389,12 @@ def create_channel(
356
389
 
357
390
  grpclib.events.listen(channel, grpclib.events.SendRequest, send_request)
358
391
 
392
+ # Surface warnings the server attached to any response on this channel.
393
+ async def recv_trailing_metadata(event: grpclib.events.RecvTrailingMetadata) -> None:
394
+ _issue_server_warnings(event.metadata)
395
+
396
+ grpclib.events.listen(channel, grpclib.events.RecvTrailingMetadata, recv_trailing_metadata)
397
+
359
398
  return channel
360
399
 
361
400
 
@@ -496,6 +535,8 @@ def process_exception_before_retry(
496
535
  raise ConnectionError(str(exc))
497
536
  elif isinstance(exc, asyncio.TimeoutError):
498
537
  raise ConnectionError(str(exc))
538
+ elif isinstance(exc, H2ProtocolError):
539
+ raise ConnectionError(str(exc))
499
540
  else:
500
541
  raise exc
501
542
 
@@ -583,6 +624,7 @@ async def _retry_transient_errors(
583
624
  OSError,
584
625
  asyncio.TimeoutError,
585
626
  AttributeError,
627
+ H2ProtocolError,
586
628
  ) as exc:
587
629
  # Note that we only catch AttributeError to handle a specific case that works around a bug
588
630
  # in grpclib<=0.4.7. See above (search for `write_appdata`).
@@ -89,6 +89,11 @@ def _parse_jwt_expiration(jwt_token: str) -> float | None:
89
89
  return None
90
90
 
91
91
 
92
+ # Enough consecutive failures to distinguish a connection that keeps breaking from
93
+ # an isolated blip, without leaving a stuck call silent for long.
94
+ RETRY_WARNING_THRESHOLD = 3
95
+
96
+
92
97
  async def call_with_retries_on_transient_errors(
93
98
  func,
94
99
  *,
@@ -97,6 +102,9 @@ async def call_with_retries_on_transient_errors(
97
102
  max_retries: int | None = 10,
98
103
  exclude_status_codes: list[Status] | None = None,
99
104
  timeout_deadline: float | None = None,
105
+ rpc_name: str = "RPC",
106
+ exec_id: str | None = None,
107
+ warning_message: str | None = None,
100
108
  ):
101
109
  """Call func() with transient error retries and exponential backoff.
102
110
 
@@ -113,10 +121,18 @@ async def call_with_retries_on_transient_errors(
113
121
  exception (e.g., into a TimeoutError) based on the deadline check.
114
122
  The caller is also responsible for propagating the remaining budget
115
123
  into `func()` (typically as the per-call gRPC timeout).
124
+ rpc_name: RPC name to include in retry diagnostics.
125
+ exec_id: Execution ID to include in retry diagnostics, for correlating
126
+ the retries of concurrent executions.
127
+ warning_message: Optional warning to emit once after
128
+ RETRY_WARNING_THRESHOLD consecutive non-timeout failures.
116
129
  """
117
130
  delay_secs = base_delay_secs
118
131
  num_retries = 0
132
+ consecutive_failures = 0
133
+ warned = False
119
134
  exclude_status_codes = exclude_status_codes or []
135
+ called_for = f" for exec {exec_id}" if exec_id is not None else ""
120
136
 
121
137
  def is_retryable_status(status: Status) -> bool:
122
138
  return status in RETRYABLE_GRPC_STATUS_CODES and status not in exclude_status_codes
@@ -128,25 +144,41 @@ async def call_with_retries_on_transient_errors(
128
144
  return False
129
145
  return True
130
146
 
131
- async def sleep_and_advance(e: Exception):
132
- nonlocal delay_secs, num_retries
147
+ async def sleep_and_advance(e: Exception, *, count_failure: bool = True):
148
+ nonlocal delay_secs, num_retries, consecutive_failures, warned
133
149
  # Clamp the backoff sleep to the remaining deadline so we don't sleep
134
150
  # past it just to fail on the next iteration's deadline check.
135
151
  sleep_for = delay_secs
136
152
  if timeout_deadline is not None:
137
153
  sleep_for = min(sleep_for, max(0.0, timeout_deadline - time.monotonic()))
138
- logger.debug(f"Retrying RPC with delay {sleep_for}s due to error: {e}")
154
+ elapsed = time.monotonic() - attempt_started_at
155
+ logger.debug(
156
+ f"{rpc_name}{called_for} attempt {num_retries + 1} failed after {elapsed:.3f}s, "
157
+ f"retrying in {sleep_for}s: {e!r}"
158
+ )
159
+ if count_failure:
160
+ consecutive_failures += 1
161
+ if warning_message is not None and consecutive_failures >= RETRY_WARNING_THRESHOLD and not warned:
162
+ logger.warning(
163
+ f"{warning_message} Retrying after {consecutive_failures} consecutive failures. "
164
+ "Set MODAL_LOGLEVEL=DEBUG for details."
165
+ )
166
+ warned = True
167
+ else:
168
+ consecutive_failures = 0
139
169
  await asyncio.sleep(sleep_for)
140
170
  delay_secs *= delay_factor
141
171
  num_retries += 1
142
172
 
143
173
  while True:
174
+ attempt_started_at = time.monotonic()
144
175
  try:
145
176
  return await func()
146
177
  except GRPCError as e:
147
178
  if not is_retryable_status(e.status) or not can_retry():
148
179
  raise
149
- await sleep_and_advance(e)
180
+ # Long-poll deadlines can expire while the process is still running on a healthy connection.
181
+ await sleep_and_advance(e, count_failure=e.status != Status.DEADLINE_EXCEEDED)
150
182
  except AttributeError as e:
151
183
  # StreamTerminatedError are not properly raised in grpclib<=0.4.7
152
184
  # fixed in https://github.com/vmagamedov/grpclib/issues/185
@@ -165,7 +197,9 @@ async def call_with_retries_on_transient_errors(
165
197
  # `timeout_deadline` can further translate this based on
166
198
  # whether the deadline has elapsed.
167
199
  raise ConnectionError(str(e))
168
- await sleep_and_advance(e)
200
+ # A client-side timeout says nothing about the connection's health, so it
201
+ # doesn't count toward the warning.
202
+ await sleep_and_advance(e, count_failure=not isinstance(e, asyncio.TimeoutError))
169
203
 
170
204
 
171
205
  _StdioReq = TypeVar("_StdioReq")
@@ -828,6 +862,12 @@ class TaskCommandRouterClient:
828
862
  # infinitely retry. For callers without an exec deadline, this
829
863
  # could hang indefinitely.
830
864
  lambda: self._call_with_auth_retry(self._stub.TaskExecWait, request, timeout=60),
865
+ rpc_name="TaskExecWait",
866
+ exec_id=exec_id,
867
+ warning_message=(
868
+ f"Cannot retrieve the exit status for exec {exec_id} due to connection or service errors. "
869
+ "The command may have already exited."
870
+ ),
831
871
  base_delay_secs=1, # Retry after 1s since total time is expected to be long.
832
872
  delay_factor=1, # Fixed delay.
833
873
  max_retries=None, # Retry forever.
@@ -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