modal 1.2.1.dev9__tar.gz → 1.2.1.dev10__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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (198) hide show
  1. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/PKG-INFO +1 -1
  2. modal-1.2.1.dev10/modal/_utils/task_command_router_client.py +525 -0
  3. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/client.pyi +2 -2
  4. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/config.py +2 -0
  5. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/exception.py +4 -0
  6. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/functions.pyi +6 -6
  7. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal.egg-info/PKG-INFO +1 -1
  8. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal.egg-info/SOURCES.txt +1 -0
  9. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/sandbox_router.proto +0 -4
  10. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/sandbox_router_pb2.pyi +0 -4
  11. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_version/__init__.py +1 -1
  12. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/LICENSE +0 -0
  13. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/README.md +0 -0
  14. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/__init__.py +0 -0
  15. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/__main__.py +0 -0
  16. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_billing.py +0 -0
  17. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_clustered_functions.py +0 -0
  18. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_clustered_functions.pyi +0 -0
  19. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_container_entrypoint.py +0 -0
  20. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_functions.py +0 -0
  21. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_ipython.py +0 -0
  22. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_location.py +0 -0
  23. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_object.py +0 -0
  24. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_output.py +0 -0
  25. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_partial_function.py +0 -0
  26. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_pty.py +0 -0
  27. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_resolver.py +0 -0
  28. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_resources.py +0 -0
  29. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_runtime/__init__.py +0 -0
  30. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_runtime/asgi.py +0 -0
  31. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_runtime/container_io_manager.py +0 -0
  32. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_runtime/container_io_manager.pyi +0 -0
  33. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_runtime/execution_context.py +0 -0
  34. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_runtime/execution_context.pyi +0 -0
  35. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  36. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_runtime/telemetry.py +0 -0
  37. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_runtime/user_code_imports.py +0 -0
  38. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_serialization.py +0 -0
  39. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_traceback.py +0 -0
  40. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_tunnel.py +0 -0
  41. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_tunnel.pyi +0 -0
  42. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_type_manager.py +0 -0
  43. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/__init__.py +0 -0
  44. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/app_utils.py +0 -0
  45. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/async_utils.py +0 -0
  46. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/auth_token_manager.py +0 -0
  47. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/blob_utils.py +0 -0
  48. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/bytes_io_segment_payload.py +0 -0
  49. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/deprecation.py +0 -0
  50. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/docker_utils.py +0 -0
  51. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/function_utils.py +0 -0
  52. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/git_utils.py +0 -0
  53. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/grpc_testing.py +0 -0
  54. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/grpc_utils.py +0 -0
  55. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/hash_utils.py +0 -0
  56. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/http_utils.py +0 -0
  57. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/jwt_utils.py +0 -0
  58. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/logger.py +0 -0
  59. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/mount_utils.py +0 -0
  60. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/name_utils.py +0 -0
  61. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/package_utils.py +0 -0
  62. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/pattern_utils.py +0 -0
  63. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/rand_pb_testing.py +0 -0
  64. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/shell_utils.py +0 -0
  65. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_utils/time_utils.py +0 -0
  66. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_vendor/__init__.py +0 -0
  67. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  68. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_vendor/cloudpickle.py +0 -0
  69. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_vendor/tblib.py +0 -0
  70. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/_watcher.py +0 -0
  71. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/app.py +0 -0
  72. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/app.pyi +0 -0
  73. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/billing.py +0 -0
  74. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/builder/2023.12.312.txt +0 -0
  75. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/builder/2023.12.txt +0 -0
  76. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/builder/2024.04.txt +0 -0
  77. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/builder/2024.10.txt +0 -0
  78. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/builder/2025.06.txt +0 -0
  79. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/builder/PREVIEW.txt +0 -0
  80. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/builder/README.md +0 -0
  81. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/builder/base-images.json +0 -0
  82. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/call_graph.py +0 -0
  83. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/__init__.py +0 -0
  84. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/_download.py +0 -0
  85. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/_traceback.py +0 -0
  86. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/app.py +0 -0
  87. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/cluster.py +0 -0
  88. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/config.py +0 -0
  89. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/container.py +0 -0
  90. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/dict.py +0 -0
  91. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/entry_point.py +0 -0
  92. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/environment.py +0 -0
  93. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/import_refs.py +0 -0
  94. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/launch.py +0 -0
  95. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/network_file_system.py +0 -0
  96. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/profile.py +0 -0
  97. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/programs/__init__.py +0 -0
  98. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/programs/launch_instance_ssh.py +0 -0
  99. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/programs/run_jupyter.py +0 -0
  100. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/programs/run_marimo.py +0 -0
  101. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/programs/vscode.py +0 -0
  102. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/queues.py +0 -0
  103. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/run.py +0 -0
  104. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/secret.py +0 -0
  105. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/token.py +0 -0
  106. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/utils.py +0 -0
  107. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cli/volume.py +0 -0
  108. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/client.py +0 -0
  109. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cloud_bucket_mount.py +0 -0
  110. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cloud_bucket_mount.pyi +0 -0
  111. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cls.py +0 -0
  112. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/cls.pyi +0 -0
  113. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/container_process.py +0 -0
  114. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/container_process.pyi +0 -0
  115. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/dict.py +0 -0
  116. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/dict.pyi +0 -0
  117. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/environments.py +0 -0
  118. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/environments.pyi +0 -0
  119. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/experimental/__init__.py +0 -0
  120. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/experimental/flash.py +0 -0
  121. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/experimental/flash.pyi +0 -0
  122. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/experimental/ipython.py +0 -0
  123. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/file_io.py +0 -0
  124. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/file_io.pyi +0 -0
  125. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/file_pattern_matcher.py +0 -0
  126. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/functions.py +0 -0
  127. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/gpu.py +0 -0
  128. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/image.py +0 -0
  129. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/image.pyi +0 -0
  130. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/io_streams.py +0 -0
  131. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/io_streams.pyi +0 -0
  132. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/mount.py +0 -0
  133. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/mount.pyi +0 -0
  134. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/network_file_system.py +0 -0
  135. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/network_file_system.pyi +0 -0
  136. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/object.py +0 -0
  137. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/object.pyi +0 -0
  138. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/output.py +0 -0
  139. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/parallel_map.py +0 -0
  140. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/parallel_map.pyi +0 -0
  141. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/partial_function.py +0 -0
  142. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/partial_function.pyi +0 -0
  143. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/proxy.py +0 -0
  144. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/proxy.pyi +0 -0
  145. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/py.typed +0 -0
  146. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/queue.py +0 -0
  147. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/queue.pyi +0 -0
  148. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/retries.py +0 -0
  149. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/runner.py +0 -0
  150. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/runner.pyi +0 -0
  151. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/running_app.py +0 -0
  152. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/sandbox.py +0 -0
  153. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/sandbox.pyi +0 -0
  154. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/schedule.py +0 -0
  155. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/scheduler_placement.py +0 -0
  156. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/secret.py +0 -0
  157. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/secret.pyi +0 -0
  158. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/serving.py +0 -0
  159. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/serving.pyi +0 -0
  160. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/snapshot.py +0 -0
  161. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/snapshot.pyi +0 -0
  162. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/stream_type.py +0 -0
  163. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/token_flow.py +0 -0
  164. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/token_flow.pyi +0 -0
  165. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/volume.py +0 -0
  166. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal/volume.pyi +0 -0
  167. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal.egg-info/dependency_links.txt +0 -0
  168. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal.egg-info/entry_points.txt +0 -0
  169. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal.egg-info/requires.txt +0 -0
  170. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal.egg-info/top_level.txt +0 -0
  171. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_docs/__init__.py +0 -0
  172. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_docs/gen_cli_docs.py +0 -0
  173. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_docs/gen_reference_docs.py +0 -0
  174. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_docs/mdmd/__init__.py +0 -0
  175. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_docs/mdmd/mdmd.py +0 -0
  176. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_docs/mdmd/signatures.py +0 -0
  177. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/__init__.py +0 -0
  178. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/api.proto +0 -0
  179. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/api_grpc.py +0 -0
  180. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/api_pb2.py +0 -0
  181. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/api_pb2.pyi +0 -0
  182. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/api_pb2_grpc.py +0 -0
  183. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/api_pb2_grpc.pyi +0 -0
  184. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/modal_api_grpc.py +0 -0
  185. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/py.typed +0 -0
  186. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/sandbox_router_grpc.py +0 -0
  187. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/sandbox_router_pb2.py +0 -0
  188. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
  189. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
  190. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/task_command_router.proto +0 -0
  191. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/task_command_router_grpc.py +0 -0
  192. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/task_command_router_pb2.py +0 -0
  193. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/task_command_router_pb2.pyi +0 -0
  194. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  195. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  196. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/modal_version/__main__.py +0 -0
  197. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/pyproject.toml +0 -0
  198. {modal-1.2.1.dev9 → modal-1.2.1.dev10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.1.dev9
3
+ Version: 1.2.1.dev10
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -0,0 +1,525 @@
1
+ # Copyright Modal Labs 2025
2
+ import asyncio
3
+ import base64
4
+ import json
5
+ import ssl
6
+ import time
7
+ import urllib.parse
8
+ from typing import AsyncIterator, Optional
9
+
10
+ import grpclib.client
11
+ import grpclib.config
12
+ import grpclib.events
13
+ from grpclib import GRPCError, Status
14
+ from grpclib.exceptions import StreamTerminatedError
15
+
16
+ from modal.config import config, logger
17
+ from modal.exception import ExecTimeoutError
18
+ from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
19
+ from modal_proto.task_command_router_grpc import TaskCommandRouterStub
20
+
21
+ from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES, connect_channel, retry_transient_errors
22
+
23
+
24
+ def _b64url_decode(data: str) -> bytes:
25
+ """Decode a base64url string with missing padding tolerated."""
26
+ padding = "=" * (-len(data) % 4)
27
+ return base64.urlsafe_b64decode(data + padding)
28
+
29
+
30
+ def _parse_jwt_expiration(jwt_token: str) -> Optional[float]:
31
+ """Parse exp from a JWT without verification. Returns UNIX time seconds or None.
32
+
33
+ This is best-effort; if parsing fails or claim missing, returns None.
34
+ """
35
+ try:
36
+ parts = jwt_token.split(".")
37
+ if len(parts) != 3:
38
+ return None
39
+ payload_b = _b64url_decode(parts[1])
40
+ payload = json.loads(payload_b)
41
+ exp = payload.get("exp")
42
+ if isinstance(exp, (int, float)):
43
+ return float(exp)
44
+ except Exception:
45
+ # Avoid raising on malformed tokens; fall back to server-driven refresh logic.
46
+ logger.warning("Failed to parse JWT expiration")
47
+ return None
48
+ return None
49
+
50
+
51
+ async def call_with_retries_on_transient_errors(
52
+ func,
53
+ *,
54
+ base_delay_secs: float = 0.01,
55
+ delay_factor: float = 2,
56
+ max_retries: Optional[int] = 10,
57
+ ):
58
+ """Call func() with transient error retries and exponential backoff.
59
+
60
+ Authentication retries are expected to be handled by the caller.
61
+ """
62
+ delay_secs = base_delay_secs
63
+ num_retries = 0
64
+
65
+ async def sleep_and_update_delay_and_num_retries_remaining(e: Exception):
66
+ nonlocal delay_secs, num_retries
67
+ logger.debug(f"Retrying RPC with delay {delay_secs}s due to error: {e}")
68
+ await asyncio.sleep(delay_secs)
69
+ delay_secs *= delay_factor
70
+ num_retries += 1
71
+
72
+ while True:
73
+ try:
74
+ return await func()
75
+ except GRPCError as e:
76
+ if (max_retries is None or num_retries < max_retries) and e.status in RETRYABLE_GRPC_STATUS_CODES:
77
+ await sleep_and_update_delay_and_num_retries_remaining(e)
78
+ else:
79
+ raise e
80
+ except AttributeError as e:
81
+ # StreamTerminatedError are not properly raised in grpclib<=0.4.7
82
+ # fixed in https://github.com/vmagamedov/grpclib/issues/185
83
+ # TODO: update to newer version (>=0.4.8) once stable
84
+ if (max_retries is None or num_retries < max_retries) and "_write_appdata" in str(e):
85
+ await sleep_and_update_delay_and_num_retries_remaining(e)
86
+ else:
87
+ raise e
88
+ except StreamTerminatedError as e:
89
+ if max_retries is None or num_retries < max_retries:
90
+ await sleep_and_update_delay_and_num_retries_remaining(e)
91
+ else:
92
+ raise e
93
+ except (OSError, asyncio.TimeoutError) as e:
94
+ if max_retries is None or num_retries < max_retries:
95
+ await sleep_and_update_delay_and_num_retries_remaining(e)
96
+ else:
97
+ raise ConnectionError(str(e))
98
+
99
+
100
+ async def fetch_command_router_access(server_client, task_id: str) -> api_pb2.TaskGetCommandRouterAccessResponse:
101
+ """Fetch direct command router access info from Modal server."""
102
+ return await retry_transient_errors(
103
+ server_client.stub.TaskGetCommandRouterAccess,
104
+ api_pb2.TaskGetCommandRouterAccessRequest(task_id=task_id),
105
+ )
106
+
107
+
108
+ class TaskCommandRouterClient:
109
+ """
110
+ Client used to talk directly to TaskCommandRouter service on worker hosts.
111
+
112
+ A new instance should be created per task.
113
+ """
114
+
115
+ @classmethod
116
+ async def try_init(
117
+ cls,
118
+ server_client,
119
+ task_id: str,
120
+ ) -> Optional["TaskCommandRouterClient"]:
121
+ """Attempt to initialize a TaskCommandRouterClient by fetching direct access.
122
+
123
+ Returns None if command router access is not enabled (FAILED_PRECONDITION).
124
+ """
125
+ try:
126
+ resp = await fetch_command_router_access(server_client, task_id)
127
+ except GRPCError as exc:
128
+ if exc.status == Status.FAILED_PRECONDITION:
129
+ logger.debug(f"Command router access is not enabled for task {task_id}")
130
+ return None
131
+ raise
132
+
133
+ logger.debug(f"Using command router access for task {task_id}")
134
+
135
+ # Build and connect a channel to the task command router now that we have access info.
136
+ o = urllib.parse.urlparse(resp.url)
137
+ if o.scheme != "https":
138
+ raise ValueError(f"Task router URL must be https, got: {resp.url}")
139
+
140
+ host, _, port_str = o.netloc.partition(":")
141
+ port = int(port_str) if port_str else 443
142
+ ssl_context = ssl.create_default_context()
143
+
144
+ # Allow insecure TLS when explicitly enabled via config.
145
+ if config["task_command_router_insecure"]:
146
+ logger.warning("Using insecure TLS for task command router due to MODAL_TASK_COMMAND_ROUTER_INSECURE")
147
+ ssl_context.check_hostname = False
148
+ ssl_context.verify_mode = ssl.CERT_NONE
149
+
150
+ channel = grpclib.client.Channel(
151
+ host,
152
+ port,
153
+ ssl=ssl_context,
154
+ config=grpclib.config.Configuration(
155
+ http2_connection_window_size=64 * 1024 * 1024, # 64 MiB
156
+ http2_stream_window_size=64 * 1024 * 1024, # 64 MiB
157
+ ),
158
+ )
159
+
160
+ await connect_channel(channel)
161
+
162
+ return cls(server_client, task_id, resp.url, resp.jwt, channel)
163
+
164
+ def __init__(
165
+ self,
166
+ server_client,
167
+ task_id: str,
168
+ server_url: str,
169
+ jwt: str,
170
+ channel: grpclib.client.Channel,
171
+ *,
172
+ stream_stdio_retry_delay_secs: float = 0.01,
173
+ stream_stdio_retry_delay_factor: float = 2,
174
+ stream_stdio_max_retries: int = 10,
175
+ ) -> None:
176
+ """Callers should not use this directly. Use TaskCommandRouterClient.try_init() instead."""
177
+ # Attach bearer token on all requests to the worker-side router service.
178
+ self._server_client = server_client
179
+ self._task_id = task_id
180
+ self._server_url = server_url
181
+ self._jwt = jwt
182
+ self._channel = channel
183
+ # Retry configuration for stdio streaming
184
+ self.stream_stdio_retry_delay_secs = stream_stdio_retry_delay_secs
185
+ self.stream_stdio_retry_delay_factor = stream_stdio_retry_delay_factor
186
+ self.stream_stdio_max_retries = stream_stdio_max_retries
187
+
188
+ # JWT refresh coordination
189
+ self._jwt_exp: Optional[float] = _parse_jwt_expiration(jwt)
190
+ self._jwt_refresh_lock = asyncio.Lock()
191
+ self._jwt_refresh_event = asyncio.Event()
192
+ self._closed = False
193
+
194
+ # Start background task to eagerly refresh JWT 30s before expiration.
195
+ self._jwt_refresh_task = asyncio.create_task(self._jwt_refresh_loop())
196
+
197
+ async def send_request(event: grpclib.events.SendRequest) -> None:
198
+ # This will get the most recent JWT for every request. No need to
199
+ # lock _jwt_refresh_lock: reads and writes happen on the
200
+ # single-threaded event loop and variable assignment is atomic.
201
+ event.metadata["authorization"] = f"Bearer {self._jwt}"
202
+
203
+ grpclib.events.listen(self._channel, grpclib.events.SendRequest, send_request)
204
+
205
+ self._stub = TaskCommandRouterStub(self._channel)
206
+
207
+ def __del__(self) -> None:
208
+ """Clean up the client when it's garbage collected."""
209
+ if self._closed:
210
+ return
211
+
212
+ self._jwt_refresh_task.cancel()
213
+
214
+ try:
215
+ self._channel.close()
216
+ except Exception:
217
+ pass
218
+
219
+ async def close(self) -> None:
220
+ """Close the client and stop the background JWT refresh task."""
221
+ if self._closed:
222
+ return
223
+
224
+ self._closed = True
225
+ self._jwt_refresh_task.cancel()
226
+ try:
227
+ logger.debug(f"Waiting for JWT refresh task to complete for exec with task ID {self._task_id}")
228
+ await self._jwt_refresh_task
229
+ except asyncio.CancelledError:
230
+ pass
231
+ self._channel.close()
232
+
233
+ async def exec_start(self, request: sr_pb2.TaskExecStartRequest) -> sr_pb2.TaskExecStartResponse:
234
+ """Start an exec'd command, properly retrying on transient errors."""
235
+ return await call_with_retries_on_transient_errors(
236
+ lambda: self._call_with_auth_retry(self._stub.TaskExecStart, request)
237
+ )
238
+
239
+ async def exec_stdio_read(
240
+ self,
241
+ task_id: str,
242
+ exec_id: str,
243
+ # Quotes around the type required for protobuf 3.19.
244
+ file_descriptor: "api_pb2.FileDescriptor.ValueType",
245
+ deadline: Optional[float] = None,
246
+ ) -> AsyncIterator[sr_pb2.TaskExecStdioReadResponse]:
247
+ """Stream stdout/stderr batches from the task, properly retrying on transient errors.
248
+
249
+ Args:
250
+ task_id: The task ID of the task running the exec'd command.
251
+ exec_id: The execution ID of the command to read from.
252
+ file_descriptor: The file descriptor to read from.
253
+ deadline: The deadline by which all output must be streamed. If
254
+ None, wait forever. If the deadline is exceeded, raises an
255
+ ExecTimeoutError.
256
+ Returns:
257
+ AsyncIterator[sr_pb2.TaskExecStdioReadResponse]: A stream of stdout/stderr batches.
258
+ Raises:
259
+ ExecTimeoutError: If the deadline is exceeded.
260
+ Other errors: If retries are exhausted on transient errors or if there's an error
261
+ from the RPC itself.
262
+ """
263
+ if file_descriptor == api_pb2.FILE_DESCRIPTOR_STDOUT:
264
+ sr_fd = sr_pb2.TASK_EXEC_STDIO_FILE_DESCRIPTOR_STDOUT
265
+ elif file_descriptor == api_pb2.FILE_DESCRIPTOR_STDERR:
266
+ sr_fd = sr_pb2.TASK_EXEC_STDIO_FILE_DESCRIPTOR_STDERR
267
+ elif file_descriptor == api_pb2.FILE_DESCRIPTOR_INFO or file_descriptor == api_pb2.FILE_DESCRIPTOR_UNSPECIFIED:
268
+ raise ValueError(f"Unsupported file descriptor: {file_descriptor}")
269
+ else:
270
+ raise ValueError(f"Invalid file descriptor: {file_descriptor}")
271
+
272
+ async for item in self._stream_stdio(task_id, exec_id, sr_fd, deadline):
273
+ yield item
274
+
275
+ async def exec_stdin_write(
276
+ self, task_id: str, exec_id: str, offset: int, data: bytes, eof: bool
277
+ ) -> sr_pb2.TaskExecStdinWriteResponse:
278
+ """Write to the stdin stream of an exec'd command, properly retrying on transient errors.
279
+
280
+ Args:
281
+ task_id: The task ID of the task running the exec'd command.
282
+ exec_id: The execution ID of the command to write to.
283
+ offset: The offset to start writing to.
284
+ data: The data to write to the stdin stream.
285
+ eof: Whether to close the stdin stream after writing the data.
286
+ Raises:
287
+ Other errors: If retries are exhausted on transient errors or if there's an error
288
+ from the RPC itself.
289
+ """
290
+ request = sr_pb2.TaskExecStdinWriteRequest(task_id=task_id, exec_id=exec_id, offset=offset, data=data, eof=eof)
291
+ return await call_with_retries_on_transient_errors(
292
+ lambda: self._call_with_auth_retry(self._stub.TaskExecStdinWrite, request)
293
+ )
294
+
295
+ async def exec_poll(self, task_id: str, exec_id: str) -> sr_pb2.TaskExecPollResponse:
296
+ """Poll for the exit status of an exec'd command, properly retrying on transient errors.
297
+
298
+ Args:
299
+ task_id: The task ID of the task running the exec'd command.
300
+ exec_id: The execution ID of the command to poll on.
301
+ Returns:
302
+ sr_pb2.TaskExecPollResponse: The exit status of the command if it has completed.
303
+
304
+ Raises:
305
+ Other errors: If retries are exhausted on transient errors or if there's an error
306
+ from the RPC itself.
307
+ """
308
+ request = sr_pb2.TaskExecPollRequest(task_id=task_id, exec_id=exec_id)
309
+ return await call_with_retries_on_transient_errors(
310
+ lambda: self._call_with_auth_retry(self._stub.TaskExecPoll, request)
311
+ )
312
+
313
+ async def exec_wait(
314
+ self,
315
+ task_id: str,
316
+ exec_id: str,
317
+ deadline: Optional[float] = None,
318
+ ) -> sr_pb2.TaskExecWaitResponse:
319
+ """Wait for an exec'd command to exit and return the exit code, properly retrying on transient errors.
320
+
321
+ Args:
322
+ task_id: The task ID of the task running the exec'd command.
323
+ exec_id: The execution ID of the command to wait on.
324
+ Returns:
325
+ Optional[sr_pb2.TaskExecWaitResponse]: The exit code of the command.
326
+ Raises:
327
+ ExecTimeoutError: If the deadline is exceeded.
328
+ Other errors: If there's an error from the RPC itself.
329
+ """
330
+ request = sr_pb2.TaskExecWaitRequest(task_id=task_id, exec_id=exec_id)
331
+ timeout = deadline - time.monotonic() if deadline is not None else None
332
+ if timeout is not None and timeout <= 0:
333
+ raise ExecTimeoutError(f"Deadline exceeded while waiting for exec {exec_id}")
334
+ try:
335
+ return await asyncio.wait_for(
336
+ call_with_retries_on_transient_errors(
337
+ # We set a 60s timeout here to avoid waiting forever if there's an unanticipated hang
338
+ # due to a networking issue. call_with_retries_on_transient_errors will retry if the
339
+ # timeout is exceeded, so we'll retry every 60s until the command exits.
340
+ #
341
+ # Safety:
342
+ # * If just the task shuts down, the task command router will return a NOT_FOUND error,
343
+ # and we'll stop retrying.
344
+ # * If the task shut down AND the worker shut down, this could
345
+ # infinitely retry. For callers without an exec deadline, this
346
+ # could hang indefinitely.
347
+ lambda: self._call_with_auth_retry(self._stub.TaskExecWait, request, timeout=60),
348
+ base_delay_secs=1, # Retry after 1s since total time is expected to be long.
349
+ delay_factor=1, # Fixed delay.
350
+ max_retries=None, # Retry forever.
351
+ ),
352
+ timeout=timeout,
353
+ )
354
+ except asyncio.TimeoutError:
355
+ raise ExecTimeoutError(f"Deadline exceeded while waiting for exec {exec_id}")
356
+
357
+ async def _refresh_jwt(self) -> None:
358
+ """Refresh JWT from the server and update internal state.
359
+
360
+ Concurrency-safe: only one refresh runs at a time.
361
+ """
362
+ async with self._jwt_refresh_lock:
363
+ if self._closed:
364
+ return
365
+
366
+ # If the current JWT expiration is already far enough in the future, don't refresh.
367
+ if self._jwt_exp is not None and self._jwt_exp - time.time() > 30:
368
+ # This can happen if multiple concurrent requests to the task command router
369
+ # get UNAUTHENTICATED errors and all refresh at the same time - one of them
370
+ # will win and the others will not refresh.
371
+ logger.debug(
372
+ f"Skipping JWT refresh for exec with task ID {self._task_id} "
373
+ "because its expiration is already far enough in the future"
374
+ )
375
+ return
376
+
377
+ resp = await fetch_command_router_access(self._server_client, self._task_id)
378
+ # Ensure the server URL remains stable for the lifetime of this client.
379
+ assert resp.url == self._server_url, "Task router URL changed during session"
380
+ self._jwt = resp.jwt
381
+ self._jwt_exp = _parse_jwt_expiration(resp.jwt)
382
+ # Wake up the background loop to recompute its next sleep.
383
+ self._jwt_refresh_event.set()
384
+
385
+ async def _call_with_auth_retry(self, func, *args, **kwargs):
386
+ try:
387
+ return await func(*args, **kwargs)
388
+ except GRPCError as exc:
389
+ if exc.status == Status.UNAUTHENTICATED:
390
+ await self._refresh_jwt()
391
+ # Retry with the original arguments preserved
392
+ return await func(*args, **kwargs)
393
+ raise
394
+
395
+ async def _jwt_refresh_loop(self) -> None:
396
+ """Background task that refreshes JWT 30 seconds before expiration.
397
+
398
+ Uses an event to wake early when a manual refresh happens or token changes.
399
+ """
400
+ while not self._closed:
401
+ try:
402
+ exp = self._jwt_exp
403
+ now = time.time()
404
+ if exp is None:
405
+ # Unknown expiration: re-check periodically or until event wakes us.
406
+ sleep_s = 60.0
407
+ else:
408
+ refresh_at = exp - 30.0
409
+ sleep_s = max(refresh_at - now, 0.0)
410
+
411
+ self._jwt_refresh_event.clear()
412
+ if sleep_s > 0:
413
+ try:
414
+ logger.debug(f"Waiting for JWT refresh for {sleep_s}s for exec with task ID {self._task_id}")
415
+ # Wait until it's time to refresh, unless woken early.
416
+ await asyncio.wait_for(self._jwt_refresh_event.wait(), timeout=sleep_s)
417
+ logger.debug(f"Stopped waiting for JWT refresh for exec with task ID {self._task_id}")
418
+ # Event fired (e.g., token changed) -> recompute timings.
419
+ continue
420
+ except asyncio.TimeoutError:
421
+ logger.debug(f"Done waiting for JWT refresh for exec with task ID {self._task_id}")
422
+ pass
423
+
424
+ # Time to refresh.
425
+ logger.debug(f"Refreshing JWT for exec with task ID {self._task_id}")
426
+ await self._refresh_jwt()
427
+ except asyncio.CancelledError:
428
+ logger.debug(f"Cancelled JWT refresh loop for exec with task ID {self._task_id}")
429
+ break
430
+ except Exception as e:
431
+ logger.warning(f"Background JWT refresh failed for exec with task ID {self._task_id}: {e}")
432
+ try:
433
+ await asyncio.sleep(1.0)
434
+ except Exception:
435
+ # Ignore sleep issues; loop will re-check closed flag.
436
+ pass
437
+
438
+ async def _stream_stdio(
439
+ self,
440
+ task_id: str,
441
+ exec_id: str,
442
+ # Quotes around the type required for protobuf 3.19.
443
+ file_descriptor: "sr_pb2.TaskExecStdioFileDescriptor.ValueType",
444
+ deadline: Optional[float] = None,
445
+ ) -> AsyncIterator[sr_pb2.TaskExecStdioReadResponse]:
446
+ """Stream stdio from the task, properly updating the offset and retrying on transient errors.
447
+ Raises ExecTimeoutError if the deadline is exceeded.
448
+ """
449
+ offset = 0
450
+ delay_secs = self.stream_stdio_retry_delay_secs
451
+ delay_factor = self.stream_stdio_retry_delay_factor
452
+ num_retries_remaining = self.stream_stdio_max_retries
453
+ num_auth_retries = 0
454
+
455
+ async def sleep_and_update_delay_and_num_retries_remaining(e: Exception):
456
+ nonlocal delay_secs, num_retries_remaining
457
+ logger.debug(f"Retrying stdio read with delay {delay_secs}s due to error: {e}")
458
+ if deadline is not None and deadline - time.monotonic() <= delay_secs:
459
+ raise ExecTimeoutError(f"Deadline exceeded while streaming stdio for exec {exec_id}")
460
+
461
+ await asyncio.sleep(delay_secs)
462
+ delay_secs *= delay_factor
463
+ num_retries_remaining -= 1
464
+
465
+ while True:
466
+ timeout = max(0, deadline - time.monotonic()) if deadline is not None else None
467
+ try:
468
+ stream = self._stub.TaskExecStdioRead.open(timeout=timeout)
469
+ async with stream as s:
470
+ req = sr_pb2.TaskExecStdioReadRequest(
471
+ task_id=task_id,
472
+ exec_id=exec_id,
473
+ offset=offset,
474
+ file_descriptor=file_descriptor,
475
+ )
476
+
477
+ # Scope auth retry strictly to the initial send (where headers/auth are sent).
478
+ try:
479
+ await s.send_message(req, end=True)
480
+ except GRPCError as exc:
481
+ if exc.status == Status.UNAUTHENTICATED and num_auth_retries < 1:
482
+ await self._refresh_jwt()
483
+ num_auth_retries += 1
484
+ continue
485
+ raise
486
+
487
+ # We successfully authenticated, reset the auth retry count.
488
+ num_auth_retries = 0
489
+
490
+ async for item in s:
491
+ # Reset retry backoff after any successful chunk.
492
+ delay_secs = self.stream_stdio_retry_delay_secs
493
+ offset += len(item.data)
494
+ yield item
495
+
496
+ # We successfully streamed all output.
497
+ return
498
+ except GRPCError as e:
499
+ if num_retries_remaining > 0 and e.status in RETRYABLE_GRPC_STATUS_CODES:
500
+ await sleep_and_update_delay_and_num_retries_remaining(e)
501
+ else:
502
+ raise e
503
+ except AttributeError as e:
504
+ # StreamTerminatedError are not properly raised in grpclib<=0.4.7
505
+ # fixed in https://github.com/vmagamedov/grpclib/issues/185
506
+ # TODO: update to newer version (>=0.4.8) once stable
507
+ if num_retries_remaining > 0 and "_write_appdata" in str(e):
508
+ await sleep_and_update_delay_and_num_retries_remaining(e)
509
+ else:
510
+ raise e
511
+ except StreamTerminatedError as e:
512
+ if num_retries_remaining > 0:
513
+ await sleep_and_update_delay_and_num_retries_remaining(e)
514
+ else:
515
+ raise e
516
+ except asyncio.TimeoutError as e:
517
+ if num_retries_remaining > 0:
518
+ await sleep_and_update_delay_and_num_retries_remaining(e)
519
+ else:
520
+ raise ConnectionError(str(e))
521
+ except OSError as e:
522
+ if num_retries_remaining > 0:
523
+ await sleep_and_update_delay_and_num_retries_remaining(e)
524
+ else:
525
+ raise ConnectionError(str(e))
@@ -33,7 +33,7 @@ class _Client:
33
33
  server_url: str,
34
34
  client_type: int,
35
35
  credentials: typing.Optional[tuple[str, str]],
36
- version: str = "1.2.1.dev9",
36
+ version: str = "1.2.1.dev10",
37
37
  ):
38
38
  """mdmd:hidden
39
39
  The Modal client object is not intended to be instantiated directly by users.
@@ -164,7 +164,7 @@ class Client:
164
164
  server_url: str,
165
165
  client_type: int,
166
166
  credentials: typing.Optional[tuple[str, str]],
167
- version: str = "1.2.1.dev9",
167
+ version: str = "1.2.1.dev10",
168
168
  ):
169
169
  """mdmd:hidden
170
170
  The Modal client object is not intended to be instantiated directly by users.
@@ -247,6 +247,8 @@ _SETTINGS = {
247
247
  "traceback": _Setting(False, transform=_to_boolean),
248
248
  "image_builder_version": _Setting(),
249
249
  "strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
250
+ # Allow insecure TLS for the task command router when running locally (testing/dev only)
251
+ "task_command_router_insecure": _Setting(False, transform=_to_boolean),
250
252
  "snapshot_debug": _Setting(False, transform=_to_boolean),
251
253
  "cuda_checkpoint_path": _Setting("/__modal/.bin/cuda-checkpoint"), # Used for snapshotting GPU memory.
252
254
  "build_validation": _Setting("error", transform=_check_value(["error", "warn", "ignore"])),
@@ -42,6 +42,10 @@ class SandboxTimeoutError(TimeoutError):
42
42
  """Raised when a Sandbox exceeds its execution duration limit and times out."""
43
43
 
44
44
 
45
+ class ExecTimeoutError(TimeoutError):
46
+ """Raised when a container process exceeds its execution duration limit and times out."""
47
+
48
+
45
49
  class SandboxTerminatedError(Error):
46
50
  """Raised when a Sandbox is terminated for an internal reason."""
47
51
 
@@ -401,7 +401,7 @@ class Function(
401
401
 
402
402
  _call_generator: ___call_generator_spec[typing_extensions.Self]
403
403
 
404
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
404
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
405
405
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
406
406
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
407
407
  ...
@@ -410,7 +410,7 @@ class Function(
410
410
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
411
411
  ...
412
412
 
413
- remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
413
+ remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
414
414
 
415
415
  class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
416
416
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -437,7 +437,7 @@ class Function(
437
437
  """
438
438
  ...
439
439
 
440
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
440
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
441
441
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
442
442
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
443
443
 
@@ -461,7 +461,7 @@ class Function(
461
461
  ...
462
462
 
463
463
  _experimental_spawn: ___experimental_spawn_spec[
464
- modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
464
+ modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
465
465
  ]
466
466
 
467
467
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
@@ -470,7 +470,7 @@ class Function(
470
470
 
471
471
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
472
472
 
473
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
473
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
474
474
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
475
475
  """Calls the function with the given arguments, without waiting for the results.
476
476
 
@@ -491,7 +491,7 @@ class Function(
491
491
  """
492
492
  ...
493
493
 
494
- spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
494
+ spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
495
495
 
496
496
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
497
497
  """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.1.dev9
3
+ Version: 1.2.1.dev10
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -122,6 +122,7 @@ modal/_utils/package_utils.py
122
122
  modal/_utils/pattern_utils.py
123
123
  modal/_utils/rand_pb_testing.py
124
124
  modal/_utils/shell_utils.py
125
+ modal/_utils/task_command_router_client.py
125
126
  modal/_utils/time_utils.py
126
127
  modal/_vendor/__init__.py
127
128
  modal/_vendor/a2wsgi_wsgi.py
@@ -55,10 +55,6 @@ message SandboxExecStartRequest {
55
55
  string task_id = 1;
56
56
  // Execution ID. This ID will be used to identify the execution for other
57
57
  // requests and ensure exec commands are idempotent.
58
- //
59
- // TODO(saltzm): Could instead have a separate idempotency key from the exec_id
60
- // like present day, and have the server generate the exec_id and return it in
61
- // the ExecStartResponse.
62
58
  string exec_id = 2;
63
59
  // Command arguments to execute.
64
60
  repeated string command_args= 3;
@@ -146,10 +146,6 @@ class SandboxExecStartRequest(google.protobuf.message.Message):
146
146
  exec_id: builtins.str
147
147
  """Execution ID. This ID will be used to identify the execution for other
148
148
  requests and ensure exec commands are idempotent.
149
-
150
- TODO(saltzm): Could instead have a separate idempotency key from the exec_id
151
- like present day, and have the server generate the exec_id and return it in
152
- the ExecStartResponse.
153
149
  """
154
150
  @property
155
151
  def command_args(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.2.1.dev9"
4
+ __version__ = "1.2.1.dev10"
File without changes