modal 1.2.3.dev13__tar.gz → 1.2.3.dev15__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 (200) hide show
  1. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/PKG-INFO +1 -1
  2. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/grpc_utils.py +95 -20
  3. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/client.pyi +2 -2
  4. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/config.py +12 -0
  5. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/functions.pyi +6 -6
  6. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal.egg-info/PKG-INFO +1 -1
  7. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/api.proto +6 -0
  8. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/api_pb2.py +363 -353
  9. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/api_pb2.pyi +18 -0
  10. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_version/__init__.py +1 -1
  11. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/LICENSE +0 -0
  12. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/README.md +0 -0
  13. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/__init__.py +0 -0
  14. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/__main__.py +0 -0
  15. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_billing.py +0 -0
  16. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_clustered_functions.py +0 -0
  17. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_clustered_functions.pyi +0 -0
  18. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_container_entrypoint.py +0 -0
  19. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_functions.py +0 -0
  20. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_grpc_client.py +0 -0
  21. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_ipython.py +0 -0
  22. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_load_context.py +0 -0
  23. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_location.py +0 -0
  24. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_object.py +0 -0
  25. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_output.py +0 -0
  26. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_partial_function.py +0 -0
  27. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_pty.py +0 -0
  28. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_resolver.py +0 -0
  29. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_resources.py +0 -0
  30. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_runtime/__init__.py +0 -0
  31. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_runtime/asgi.py +0 -0
  32. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_runtime/container_io_manager.py +0 -0
  33. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_runtime/container_io_manager.pyi +0 -0
  34. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_runtime/execution_context.py +0 -0
  35. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_runtime/execution_context.pyi +0 -0
  36. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  37. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_runtime/telemetry.py +0 -0
  38. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_runtime/user_code_imports.py +0 -0
  39. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_serialization.py +0 -0
  40. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_traceback.py +0 -0
  41. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_tunnel.py +0 -0
  42. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_tunnel.pyi +0 -0
  43. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_type_manager.py +0 -0
  44. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/__init__.py +0 -0
  45. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/app_utils.py +0 -0
  46. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/async_utils.py +0 -0
  47. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/auth_token_manager.py +0 -0
  48. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/blob_utils.py +0 -0
  49. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/bytes_io_segment_payload.py +0 -0
  50. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/deprecation.py +0 -0
  51. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/docker_utils.py +0 -0
  52. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/function_utils.py +0 -0
  53. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/git_utils.py +0 -0
  54. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/grpc_testing.py +0 -0
  55. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/hash_utils.py +0 -0
  56. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/http_utils.py +0 -0
  57. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/jwt_utils.py +0 -0
  58. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/logger.py +0 -0
  59. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/mount_utils.py +0 -0
  60. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/name_utils.py +0 -0
  61. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/package_utils.py +0 -0
  62. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/pattern_utils.py +0 -0
  63. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/rand_pb_testing.py +0 -0
  64. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/shell_utils.py +0 -0
  65. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/task_command_router_client.py +0 -0
  66. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_utils/time_utils.py +0 -0
  67. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_vendor/__init__.py +0 -0
  68. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  69. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_vendor/cloudpickle.py +0 -0
  70. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_vendor/tblib.py +0 -0
  71. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/_watcher.py +0 -0
  72. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/app.py +0 -0
  73. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/app.pyi +0 -0
  74. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/billing.py +0 -0
  75. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/builder/2023.12.312.txt +0 -0
  76. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/builder/2023.12.txt +0 -0
  77. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/builder/2024.04.txt +0 -0
  78. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/builder/2024.10.txt +0 -0
  79. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/builder/2025.06.txt +0 -0
  80. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/builder/PREVIEW.txt +0 -0
  81. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/builder/README.md +0 -0
  82. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/builder/base-images.json +0 -0
  83. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/call_graph.py +0 -0
  84. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/__init__.py +0 -0
  85. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/_download.py +0 -0
  86. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/_traceback.py +0 -0
  87. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/app.py +0 -0
  88. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/cluster.py +0 -0
  89. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/config.py +0 -0
  90. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/container.py +0 -0
  91. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/dict.py +0 -0
  92. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/entry_point.py +0 -0
  93. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/environment.py +0 -0
  94. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/import_refs.py +0 -0
  95. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/launch.py +0 -0
  96. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/network_file_system.py +0 -0
  97. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/profile.py +0 -0
  98. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/programs/__init__.py +0 -0
  99. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/programs/launch_instance_ssh.py +0 -0
  100. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/programs/run_jupyter.py +0 -0
  101. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/programs/run_marimo.py +0 -0
  102. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/programs/vscode.py +0 -0
  103. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/queues.py +0 -0
  104. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/run.py +0 -0
  105. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/secret.py +0 -0
  106. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/token.py +0 -0
  107. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/utils.py +0 -0
  108. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cli/volume.py +0 -0
  109. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/client.py +0 -0
  110. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cloud_bucket_mount.py +0 -0
  111. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cloud_bucket_mount.pyi +0 -0
  112. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cls.py +0 -0
  113. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/cls.pyi +0 -0
  114. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/container_process.py +0 -0
  115. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/container_process.pyi +0 -0
  116. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/dict.py +0 -0
  117. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/dict.pyi +0 -0
  118. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/environments.py +0 -0
  119. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/environments.pyi +0 -0
  120. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/exception.py +0 -0
  121. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/experimental/__init__.py +0 -0
  122. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/experimental/flash.py +0 -0
  123. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/experimental/flash.pyi +0 -0
  124. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/experimental/ipython.py +0 -0
  125. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/file_io.py +0 -0
  126. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/file_io.pyi +0 -0
  127. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/file_pattern_matcher.py +0 -0
  128. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/functions.py +0 -0
  129. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/gpu.py +0 -0
  130. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/image.py +0 -0
  131. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/image.pyi +0 -0
  132. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/io_streams.py +0 -0
  133. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/io_streams.pyi +0 -0
  134. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/mount.py +0 -0
  135. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/mount.pyi +0 -0
  136. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/network_file_system.py +0 -0
  137. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/network_file_system.pyi +0 -0
  138. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/object.py +0 -0
  139. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/object.pyi +0 -0
  140. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/output.py +0 -0
  141. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/parallel_map.py +0 -0
  142. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/parallel_map.pyi +0 -0
  143. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/partial_function.py +0 -0
  144. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/partial_function.pyi +0 -0
  145. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/proxy.py +0 -0
  146. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/proxy.pyi +0 -0
  147. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/py.typed +0 -0
  148. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/queue.py +0 -0
  149. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/queue.pyi +0 -0
  150. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/retries.py +0 -0
  151. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/runner.py +0 -0
  152. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/runner.pyi +0 -0
  153. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/running_app.py +0 -0
  154. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/sandbox.py +0 -0
  155. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/sandbox.pyi +0 -0
  156. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/schedule.py +0 -0
  157. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/scheduler_placement.py +0 -0
  158. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/secret.py +0 -0
  159. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/secret.pyi +0 -0
  160. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/serving.py +0 -0
  161. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/serving.pyi +0 -0
  162. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/snapshot.py +0 -0
  163. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/snapshot.pyi +0 -0
  164. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/stream_type.py +0 -0
  165. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/token_flow.py +0 -0
  166. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/token_flow.pyi +0 -0
  167. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/volume.py +0 -0
  168. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal/volume.pyi +0 -0
  169. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal.egg-info/SOURCES.txt +0 -0
  170. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal.egg-info/dependency_links.txt +0 -0
  171. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal.egg-info/entry_points.txt +0 -0
  172. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal.egg-info/requires.txt +0 -0
  173. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal.egg-info/top_level.txt +0 -0
  174. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_docs/__init__.py +0 -0
  175. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_docs/gen_cli_docs.py +0 -0
  176. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_docs/gen_reference_docs.py +0 -0
  177. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_docs/mdmd/__init__.py +0 -0
  178. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_docs/mdmd/mdmd.py +0 -0
  179. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_docs/mdmd/signatures.py +0 -0
  180. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/__init__.py +0 -0
  181. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/api_grpc.py +0 -0
  182. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/api_pb2_grpc.py +0 -0
  183. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/api_pb2_grpc.pyi +0 -0
  184. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/modal_api_grpc.py +0 -0
  185. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/py.typed +0 -0
  186. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/sandbox_router.proto +0 -0
  187. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/sandbox_router_grpc.py +0 -0
  188. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/sandbox_router_pb2.py +0 -0
  189. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/sandbox_router_pb2.pyi +0 -0
  190. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
  191. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
  192. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/task_command_router.proto +0 -0
  193. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/task_command_router_grpc.py +0 -0
  194. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/task_command_router_pb2.py +0 -0
  195. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/task_command_router_pb2.pyi +0 -0
  196. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  197. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  198. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/modal_version/__main__.py +0 -0
  199. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/pyproject.toml +0 -0
  200. {modal-1.2.3.dev13 → modal-1.2.3.dev15}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.3.dev13
3
+ Version: 1.2.3.dev15
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -29,6 +29,7 @@ from modal_proto import api_pb2
29
29
  from modal_version import __version__
30
30
 
31
31
  from .._traceback import suppress_tb_frames
32
+ from ..config import config
32
33
  from .async_utils import retry
33
34
  from .logger import logger
34
35
 
@@ -72,6 +73,7 @@ RETRYABLE_GRPC_STATUS_CODES = [
72
73
  Status.INTERNAL,
73
74
  Status.UNKNOWN,
74
75
  ]
76
+ SERVER_RETRY_WARNING_TIME_INTERVAL = 30.0
75
77
 
76
78
 
77
79
  @dataclass
@@ -251,6 +253,52 @@ async def retry_transient_errors(
251
253
  return await _retry_transient_errors(fn, req, retry=Retry(max_retries=max_retries))
252
254
 
253
255
 
256
+ def get_server_retry_policy(exc: Exception) -> Optional[api_pb2.RPCRetryPolicy]:
257
+ """Get server retry policy."""
258
+ if not isinstance(exc, GRPCError) or not exc.details:
259
+ return None
260
+
261
+ # Server should not set multiple retry instructions, but if there is more than one, pick the first one
262
+ for entry in exc.details:
263
+ if isinstance(entry, api_pb2.RPCRetryPolicy):
264
+ return entry
265
+ return None
266
+
267
+
268
+ def process_exception_before_retry(
269
+ exc: Exception,
270
+ final_attempt: bool,
271
+ fn_name: str,
272
+ n_retries: int,
273
+ delay: float,
274
+ total_deadline: Optional[float],
275
+ idempotency_key: str,
276
+ ):
277
+ """Process exception before retry, used by `_retry_transient_errors`."""
278
+ with suppress_tb_frames(1):
279
+ if final_attempt:
280
+ logger.debug(
281
+ f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} "
282
+ f"{total_deadline=} for {fn_name} ({idempotency_key[:8]})"
283
+ )
284
+ if isinstance(exc, OSError):
285
+ raise ConnectionError(str(exc))
286
+ elif isinstance(exc, asyncio.TimeoutError):
287
+ raise ConnectionError(str(exc))
288
+ else:
289
+ raise exc
290
+
291
+ if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
292
+ # StreamTerminatedError are not properly raised in grpclib<=0.4.7
293
+ # fixed in https://github.com/vmagamedov/grpclib/issues/185
294
+ # TODO: update to newer version (>=0.4.8) once stable
295
+ # Also be sure to remove the AttributeError from the set of exceptions
296
+ # we handle in the retry logic once we drop this check!
297
+ raise exc
298
+
299
+ logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})")
300
+
301
+
254
302
  async def _retry_transient_errors(
255
303
  fn: typing.Union[
256
304
  "modal._grpc_client.UnaryUnaryWrapper[RequestType, ResponseType]",
@@ -273,12 +321,15 @@ async def _retry_transient_errors(
273
321
 
274
322
  delay = retry.base_delay
275
323
  n_retries = 0
324
+ n_throttled_retries = 0
276
325
 
277
326
  status_codes = [*RETRYABLE_GRPC_STATUS_CODES, *retry.additional_status_codes]
278
327
 
279
328
  idempotency_key = str(uuid.uuid4())
280
329
 
281
330
  t0 = time.time()
331
+ last_server_retry_warning_time = None
332
+
282
333
  if retry.total_timeout is not None:
283
334
  total_deadline = t0 + retry.total_timeout
284
335
  else:
@@ -290,14 +341,18 @@ async def _retry_transient_errors(
290
341
  attempt_metadata = [
291
342
  ("x-idempotency-key", idempotency_key),
292
343
  ("x-retry-attempt", str(n_retries)),
344
+ ("x-throttle-retry-attempt", str(n_throttled_retries)),
293
345
  *metadata,
294
346
  ]
295
347
  if n_retries > 0:
296
348
  attempt_metadata.append(("x-retry-delay", str(time.time() - t0)))
349
+ if n_throttled_retries > 0:
350
+ attempt_metadata.append(("x-throttle-retry-delay", str(time.time() - t0)))
351
+
297
352
  timeouts = []
298
353
  if retry.attempt_timeout is not None:
299
354
  timeouts.append(retry.attempt_timeout)
300
- if retry.total_timeout is not None and total_deadline is not None:
355
+ if total_deadline is not None:
301
356
  timeouts.append(max(total_deadline - time.time(), retry.attempt_timeout_floor))
302
357
  if timeouts:
303
358
  timeout = min(timeouts) # In case the function provided both types of timeouts
@@ -307,6 +362,42 @@ async def _retry_transient_errors(
307
362
  with suppress_tb_frames(1):
308
363
  return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
309
364
  except (StreamTerminatedError, GRPCError, OSError, asyncio.TimeoutError, AttributeError) as exc:
365
+ # Note that we only catch AttributeError to handle a specific case that works around a bug
366
+ # in grpclib<=0.4.7. See above (search for `write_appdata`).
367
+
368
+ # Server side instruction for retries
369
+ if isinstance(exc, GRPCError) and (server_retry_policy := get_server_retry_policy(exc)):
370
+ server_delay = server_retry_policy.retry_after_secs
371
+
372
+ # When `total_deadline` is not defined and server gives instruction for retries, then use
373
+ # `max_throttle_wait`.
374
+ if total_deadline is None and (max_throttle_wait := config.get("max_throttle_wait")):
375
+ total_deadline = t0 + max_throttle_wait
376
+
377
+ final_attempt = (
378
+ total_deadline is not None
379
+ and time.time() + server_delay + retry.attempt_timeout_floor >= total_deadline
380
+ )
381
+ with suppress_tb_frames(1):
382
+ process_exception_before_retry(
383
+ exc, final_attempt, fn.name, n_retries, server_delay, total_deadline, idempotency_key
384
+ )
385
+
386
+ now = time.time()
387
+ if last_server_retry_warning_time is None or (
388
+ now - last_server_retry_warning_time >= SERVER_RETRY_WARNING_TIME_INTERVAL
389
+ ):
390
+ last_server_retry_warning_time = now
391
+ logger.warning(
392
+ f"Warning: Received {exc.status} status: {exc.message}. "
393
+ f"Will retry in {server_delay:0.2f} seconds."
394
+ )
395
+
396
+ n_throttled_retries += 1
397
+ await asyncio.sleep(server_delay)
398
+ continue
399
+
400
+ # Client handles retry
310
401
  if isinstance(exc, GRPCError) and exc.status not in status_codes:
311
402
  if exc.status == Status.UNAUTHENTICATED:
312
403
  raise AuthError(exc.message)
@@ -320,25 +411,9 @@ async def _retry_transient_errors(
320
411
  final_attempt = False
321
412
 
322
413
  with suppress_tb_frames(1):
323
- if final_attempt:
324
- logger.debug(
325
- f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} "
326
- f"{total_deadline=} for {fn.name} ({idempotency_key[:8]})"
327
- )
328
- if isinstance(exc, OSError):
329
- raise ConnectionError(str(exc))
330
- elif isinstance(exc, asyncio.TimeoutError):
331
- raise ConnectionError(str(exc))
332
- else:
333
- raise exc
334
-
335
- if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
336
- # StreamTerminatedError are not properly raised in grpclib<=0.4.7
337
- # fixed in https://github.com/vmagamedov/grpclib/issues/185
338
- # TODO: update to newer version (>=0.4.8) once stable
339
- raise exc
340
-
341
- logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn.name} ({idempotency_key[:8]})")
414
+ process_exception_before_retry(
415
+ exc, final_attempt, fn.name, n_retries, delay, total_deadline, idempotency_key
416
+ )
342
417
 
343
418
  n_retries += 1
344
419
 
@@ -32,7 +32,7 @@ class _Client:
32
32
  server_url: str,
33
33
  client_type: int,
34
34
  credentials: typing.Optional[tuple[str, str]],
35
- version: str = "1.2.3.dev13",
35
+ version: str = "1.2.3.dev15",
36
36
  ):
37
37
  """mdmd:hidden
38
38
  The Modal client object is not intended to be instantiated directly by users.
@@ -163,7 +163,7 @@ class Client:
163
163
  server_url: str,
164
164
  client_type: int,
165
165
  credentials: typing.Optional[tuple[str, str]],
166
- version: str = "1.2.3.dev13",
166
+ version: str = "1.2.3.dev15",
167
167
  ):
168
168
  """mdmd:hidden
169
169
  The Modal client object is not intended to be instantiated directly by users.
@@ -51,6 +51,10 @@ Other possible configuration options are:
51
51
  Defaults to 10.
52
52
  Number of seconds to wait for logs to drain when closing the session,
53
53
  before giving up.
54
+ * `max_throttle_wait` (in the .toml file) / `MODAL_MAX_THROTTLE_WAIT` (as an env var).
55
+ Defaults to None (no limit).
56
+ Maximum number of seconds to wait when requests are being throttled (i.e., due
57
+ to rate limiting or other cases that can normally be resolved through backoff).
54
58
  * `force_build` (in the .toml file) / `MODAL_FORCE_BUILD` (as an env var).
55
59
  Defaults to False.
56
60
  When set, ignores the Image cache and builds all Image layers. Note that this
@@ -217,6 +221,13 @@ def _enforce_suffix_rules(x: str) -> str:
217
221
  return x
218
222
 
219
223
 
224
+ def _int_or_none(x: str) -> Optional[int]:
225
+ if not x:
226
+ return None
227
+ x_int = int(x)
228
+ return x_int if x_int > 0 else None
229
+
230
+
220
231
  class _Setting(typing.NamedTuple):
221
232
  default: typing.Any = None
222
233
  transform: typing.Callable[[str], typing.Any] = lambda x: x # noqa: E731
@@ -258,6 +269,7 @@ _SETTINGS = {
258
269
  transform=lambda s: _check_value(["pickle", "cbor"])(s.lower()),
259
270
  ),
260
271
  "dev_suffix": _Setting("", transform=_enforce_suffix_rules),
272
+ "max_throttle_wait": _Setting(None, transform=_int_or_none),
261
273
  }
262
274
 
263
275
 
@@ -408,7 +408,7 @@ class Function(
408
408
 
409
409
  _call_generator: ___call_generator_spec[typing_extensions.Self]
410
410
 
411
- class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
411
+ class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
412
412
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
413
413
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
414
414
  ...
@@ -417,7 +417,7 @@ class Function(
417
417
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
418
418
  ...
419
419
 
420
- remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
420
+ remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
421
421
 
422
422
  class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
423
423
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -444,7 +444,7 @@ class Function(
444
444
  """
445
445
  ...
446
446
 
447
- class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
447
+ class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
448
448
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
449
449
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
450
450
 
@@ -468,7 +468,7 @@ class Function(
468
468
  ...
469
469
 
470
470
  _experimental_spawn: ___experimental_spawn_spec[
471
- modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
471
+ modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
472
472
  ]
473
473
 
474
474
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
@@ -477,7 +477,7 @@ class Function(
477
477
 
478
478
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
479
479
 
480
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
480
+ class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
481
481
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
482
482
  """Calls the function with the given arguments, without waiting for the results.
483
483
 
@@ -498,7 +498,7 @@ class Function(
498
498
  """
499
499
  ...
500
500
 
501
- spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
501
+ spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
502
502
 
503
503
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
504
504
  """Return the inner Python object wrapped by this Modal Function."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.3.dev13
3
+ Version: 1.2.3.dev15
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -2642,6 +2642,12 @@ message QueuePutRequest {
2642
2642
  int32 partition_ttl_seconds = 6;
2643
2643
  }
2644
2644
 
2645
+ // Retry repolicy used by GRPCError.details for the server to give instructions
2646
+ // for the client to retry.
2647
+ message RPCRetryPolicy {
2648
+ float retry_after_secs = 1;
2649
+ }
2650
+
2645
2651
  // A copy google.rpc.Status for GRPCError.details:
2646
2652
  // https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto
2647
2653
  // RPCStatus is compatible with google.rpc.Status, so one can encode messages using