modal 1.2.1.dev13__tar.gz → 1.2.1.dev14__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 (199) hide show
  1. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/PKG-INFO +1 -1
  2. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/task_command_router_client.py +18 -4
  3. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/cluster.py +4 -2
  4. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/container.py +4 -2
  5. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/client.pyi +2 -2
  6. modal-1.2.1.dev14/modal/container_process.py +470 -0
  7. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/container_process.pyi +91 -32
  8. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/functions.pyi +6 -6
  9. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/sandbox.py +1 -0
  10. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/PKG-INFO +1 -1
  11. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_version/__init__.py +1 -1
  12. modal-1.2.1.dev13/modal/container_process.py +0 -204
  13. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/LICENSE +0 -0
  14. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/README.md +0 -0
  15. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/__init__.py +0 -0
  16. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/__main__.py +0 -0
  17. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_billing.py +0 -0
  18. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_clustered_functions.py +0 -0
  19. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_clustered_functions.pyi +0 -0
  20. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_container_entrypoint.py +0 -0
  21. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_functions.py +0 -0
  22. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_ipython.py +0 -0
  23. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_location.py +0 -0
  24. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_object.py +0 -0
  25. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_output.py +0 -0
  26. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_partial_function.py +0 -0
  27. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_pty.py +0 -0
  28. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_resolver.py +0 -0
  29. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_resources.py +0 -0
  30. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/__init__.py +0 -0
  31. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/asgi.py +0 -0
  32. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/container_io_manager.py +0 -0
  33. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/container_io_manager.pyi +0 -0
  34. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/execution_context.py +0 -0
  35. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/execution_context.pyi +0 -0
  36. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  37. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/telemetry.py +0 -0
  38. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/user_code_imports.py +0 -0
  39. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_serialization.py +0 -0
  40. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_traceback.py +0 -0
  41. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_tunnel.py +0 -0
  42. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_tunnel.pyi +0 -0
  43. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_type_manager.py +0 -0
  44. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/__init__.py +0 -0
  45. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/app_utils.py +0 -0
  46. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/async_utils.py +0 -0
  47. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/auth_token_manager.py +0 -0
  48. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/blob_utils.py +0 -0
  49. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/bytes_io_segment_payload.py +0 -0
  50. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/deprecation.py +0 -0
  51. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/docker_utils.py +0 -0
  52. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/function_utils.py +0 -0
  53. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/git_utils.py +0 -0
  54. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/grpc_testing.py +0 -0
  55. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/grpc_utils.py +0 -0
  56. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/hash_utils.py +0 -0
  57. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/http_utils.py +0 -0
  58. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/jwt_utils.py +0 -0
  59. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/logger.py +0 -0
  60. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/mount_utils.py +0 -0
  61. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/name_utils.py +0 -0
  62. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/package_utils.py +0 -0
  63. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/pattern_utils.py +0 -0
  64. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/rand_pb_testing.py +0 -0
  65. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/shell_utils.py +0 -0
  66. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/time_utils.py +0 -0
  67. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_vendor/__init__.py +0 -0
  68. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  69. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_vendor/cloudpickle.py +0 -0
  70. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_vendor/tblib.py +0 -0
  71. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_watcher.py +0 -0
  72. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/app.py +0 -0
  73. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/app.pyi +0 -0
  74. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/billing.py +0 -0
  75. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/2023.12.312.txt +0 -0
  76. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/2023.12.txt +0 -0
  77. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/2024.04.txt +0 -0
  78. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/2024.10.txt +0 -0
  79. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/2025.06.txt +0 -0
  80. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/PREVIEW.txt +0 -0
  81. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/README.md +0 -0
  82. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/base-images.json +0 -0
  83. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/call_graph.py +0 -0
  84. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/__init__.py +0 -0
  85. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/_download.py +0 -0
  86. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/_traceback.py +0 -0
  87. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/app.py +0 -0
  88. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/config.py +0 -0
  89. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/dict.py +0 -0
  90. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/entry_point.py +0 -0
  91. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/environment.py +0 -0
  92. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/import_refs.py +0 -0
  93. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/launch.py +0 -0
  94. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/network_file_system.py +0 -0
  95. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/profile.py +0 -0
  96. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/programs/__init__.py +0 -0
  97. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/programs/launch_instance_ssh.py +0 -0
  98. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/programs/run_jupyter.py +0 -0
  99. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/programs/run_marimo.py +0 -0
  100. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/programs/vscode.py +0 -0
  101. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/queues.py +0 -0
  102. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/run.py +0 -0
  103. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/secret.py +0 -0
  104. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/token.py +0 -0
  105. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/utils.py +0 -0
  106. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/volume.py +0 -0
  107. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/client.py +0 -0
  108. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cloud_bucket_mount.py +0 -0
  109. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cloud_bucket_mount.pyi +0 -0
  110. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cls.py +0 -0
  111. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cls.pyi +0 -0
  112. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/config.py +0 -0
  113. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/dict.py +0 -0
  114. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/dict.pyi +0 -0
  115. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/environments.py +0 -0
  116. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/environments.pyi +0 -0
  117. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/exception.py +0 -0
  118. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/experimental/__init__.py +0 -0
  119. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/experimental/flash.py +0 -0
  120. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/experimental/flash.pyi +0 -0
  121. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/experimental/ipython.py +0 -0
  122. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/file_io.py +0 -0
  123. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/file_io.pyi +0 -0
  124. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/file_pattern_matcher.py +0 -0
  125. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/functions.py +0 -0
  126. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/gpu.py +0 -0
  127. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/image.py +0 -0
  128. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/image.pyi +0 -0
  129. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/io_streams.py +0 -0
  130. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/io_streams.pyi +0 -0
  131. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/mount.py +0 -0
  132. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/mount.pyi +0 -0
  133. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/network_file_system.py +0 -0
  134. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/network_file_system.pyi +0 -0
  135. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/object.py +0 -0
  136. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/object.pyi +0 -0
  137. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/output.py +0 -0
  138. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/parallel_map.py +0 -0
  139. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/parallel_map.pyi +0 -0
  140. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/partial_function.py +0 -0
  141. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/partial_function.pyi +0 -0
  142. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/proxy.py +0 -0
  143. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/proxy.pyi +0 -0
  144. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/py.typed +0 -0
  145. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/queue.py +0 -0
  146. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/queue.pyi +0 -0
  147. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/retries.py +0 -0
  148. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/runner.py +0 -0
  149. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/runner.pyi +0 -0
  150. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/running_app.py +0 -0
  151. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/sandbox.pyi +0 -0
  152. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/schedule.py +0 -0
  153. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/scheduler_placement.py +0 -0
  154. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/secret.py +0 -0
  155. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/secret.pyi +0 -0
  156. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/serving.py +0 -0
  157. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/serving.pyi +0 -0
  158. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/snapshot.py +0 -0
  159. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/snapshot.pyi +0 -0
  160. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/stream_type.py +0 -0
  161. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/token_flow.py +0 -0
  162. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/token_flow.pyi +0 -0
  163. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/volume.py +0 -0
  164. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/volume.pyi +0 -0
  165. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/SOURCES.txt +0 -0
  166. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/dependency_links.txt +0 -0
  167. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/entry_points.txt +0 -0
  168. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/requires.txt +0 -0
  169. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/top_level.txt +0 -0
  170. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/__init__.py +0 -0
  171. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/gen_cli_docs.py +0 -0
  172. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/gen_reference_docs.py +0 -0
  173. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/mdmd/__init__.py +0 -0
  174. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/mdmd/mdmd.py +0 -0
  175. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/mdmd/signatures.py +0 -0
  176. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/__init__.py +0 -0
  177. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api.proto +0 -0
  178. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api_grpc.py +0 -0
  179. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api_pb2.py +0 -0
  180. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api_pb2.pyi +0 -0
  181. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api_pb2_grpc.py +0 -0
  182. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api_pb2_grpc.pyi +0 -0
  183. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/modal_api_grpc.py +0 -0
  184. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/py.typed +0 -0
  185. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router.proto +0 -0
  186. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router_grpc.py +0 -0
  187. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router_pb2.py +0 -0
  188. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router_pb2.pyi +0 -0
  189. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
  190. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
  191. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router.proto +0 -0
  192. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router_grpc.py +0 -0
  193. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router_pb2.py +0 -0
  194. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router_pb2.pyi +0 -0
  195. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  196. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  197. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_version/__main__.py +0 -0
  198. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/pyproject.toml +0 -0
  199. {modal-1.2.1.dev13 → modal-1.2.1.dev14}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.1.dev13
3
+ Version: 1.2.1.dev14
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -292,7 +292,9 @@ class TaskCommandRouterClient:
292
292
  lambda: self._call_with_auth_retry(self._stub.TaskExecStdinWrite, request)
293
293
  )
294
294
 
295
- async def exec_poll(self, task_id: str, exec_id: str) -> sr_pb2.TaskExecPollResponse:
295
+ async def exec_poll(
296
+ self, task_id: str, exec_id: str, deadline: Optional[float] = None
297
+ ) -> sr_pb2.TaskExecPollResponse:
296
298
  """Poll for the exit status of an exec'd command, properly retrying on transient errors.
297
299
 
298
300
  Args:
@@ -302,13 +304,25 @@ class TaskCommandRouterClient:
302
304
  sr_pb2.TaskExecPollResponse: The exit status of the command if it has completed.
303
305
 
304
306
  Raises:
307
+ ExecTimeoutError: If the deadline is exceeded.
305
308
  Other errors: If retries are exhausted on transient errors or if there's an error
306
309
  from the RPC itself.
307
310
  """
308
311
  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
+ # The timeout here is really a backstop in the event of a hang contacting
313
+ # the command router. Poll should usually be instantaneous.
314
+ timeout = deadline - time.monotonic() if deadline is not None else None
315
+ if timeout is not None and timeout <= 0:
316
+ raise ExecTimeoutError(f"Deadline exceeded while polling for exec {exec_id}")
317
+ try:
318
+ return await asyncio.wait_for(
319
+ call_with_retries_on_transient_errors(
320
+ lambda: self._call_with_auth_retry(self._stub.TaskExecPoll, request)
321
+ ),
322
+ timeout=timeout,
323
+ )
324
+ except asyncio.TimeoutError:
325
+ raise ExecTimeoutError(f"Deadline exceeded while polling for exec {exec_id}")
312
326
 
313
327
  async def exec_wait(
314
328
  self,
@@ -83,7 +83,9 @@ async def shell(
83
83
  )
84
84
  exec_res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
85
85
  if pty:
86
- await _ContainerProcess(exec_res.exec_id, client).attach()
86
+ await _ContainerProcess(exec_res.exec_id, task_id, client).attach()
87
87
  else:
88
88
  # TODO: redirect stderr to its own stream?
89
- await _ContainerProcess(exec_res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
89
+ await _ContainerProcess(
90
+ exec_res.exec_id, task_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
91
+ ).wait()
@@ -80,10 +80,12 @@ async def exec(
80
80
  res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
81
81
 
82
82
  if pty:
83
- await _ContainerProcess(res.exec_id, client).attach()
83
+ await _ContainerProcess(res.exec_id, container_id, client).attach()
84
84
  else:
85
85
  # TODO: redirect stderr to its own stream?
86
- await _ContainerProcess(res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
86
+ await _ContainerProcess(
87
+ res.exec_id, container_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
88
+ ).wait()
87
89
 
88
90
 
89
91
  @container_cli.command("stop")
@@ -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.dev13",
36
+ version: str = "1.2.1.dev14",
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.dev13",
167
+ version: str = "1.2.1.dev14",
168
168
  ):
169
169
  """mdmd:hidden
170
170
  The Modal client object is not intended to be instantiated directly by users.
@@ -0,0 +1,470 @@
1
+ # Copyright Modal Labs 2024
2
+ import asyncio
3
+ import platform
4
+ import time
5
+ from typing import Generic, Optional, TypeVar
6
+
7
+ from modal_proto import api_pb2
8
+
9
+ from ._utils.async_utils import TaskContext, synchronize_api
10
+ from ._utils.grpc_utils import retry_transient_errors
11
+ from ._utils.shell_utils import stream_from_stdin, write_to_fd
12
+ from ._utils.task_command_router_client import TaskCommandRouterClient
13
+ from .client import _Client
14
+ from .config import logger
15
+ from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
16
+ from .io_streams import _StreamReader, _StreamWriter
17
+ from .stream_type import StreamType
18
+
19
+ T = TypeVar("T", str, bytes)
20
+
21
+
22
+ class _ContainerProcessThroughServer(Generic[T]):
23
+ _process_id: Optional[str] = None
24
+ _stdout: _StreamReader[T]
25
+ _stderr: _StreamReader[T]
26
+ _stdin: _StreamWriter
27
+ _exec_deadline: Optional[float] = None
28
+ _text: bool
29
+ _by_line: bool
30
+ _returncode: Optional[int] = None
31
+
32
+ def __init__(
33
+ self,
34
+ process_id: str,
35
+ task_id: str,
36
+ client: _Client,
37
+ stdout: StreamType = StreamType.PIPE,
38
+ stderr: StreamType = StreamType.PIPE,
39
+ exec_deadline: Optional[float] = None,
40
+ text: bool = True,
41
+ by_line: bool = False,
42
+ ) -> None:
43
+ self._process_id = process_id
44
+ self._client = client
45
+ self._exec_deadline = exec_deadline
46
+ self._text = text
47
+ self._by_line = by_line
48
+ self._stdout = _StreamReader[T](
49
+ api_pb2.FILE_DESCRIPTOR_STDOUT,
50
+ process_id,
51
+ "container_process",
52
+ self._client,
53
+ stream_type=stdout,
54
+ text=text,
55
+ by_line=by_line,
56
+ deadline=exec_deadline,
57
+ task_id=task_id,
58
+ )
59
+ self._stderr = _StreamReader[T](
60
+ api_pb2.FILE_DESCRIPTOR_STDERR,
61
+ process_id,
62
+ "container_process",
63
+ self._client,
64
+ stream_type=stderr,
65
+ text=text,
66
+ by_line=by_line,
67
+ deadline=exec_deadline,
68
+ task_id=task_id,
69
+ )
70
+ self._stdin = _StreamWriter(process_id, "container_process", self._client)
71
+
72
+ def __repr__(self) -> str:
73
+ return f"ContainerProcess(process_id={self._process_id!r})"
74
+
75
+ @property
76
+ def stdout(self) -> _StreamReader[T]:
77
+ """StreamReader for the container process's stdout stream."""
78
+ return self._stdout
79
+
80
+ @property
81
+ def stderr(self) -> _StreamReader[T]:
82
+ """StreamReader for the container process's stderr stream."""
83
+ return self._stderr
84
+
85
+ @property
86
+ def stdin(self) -> _StreamWriter:
87
+ """StreamWriter for the container process's stdin stream."""
88
+ return self._stdin
89
+
90
+ @property
91
+ def returncode(self) -> int:
92
+ if self._returncode is None:
93
+ raise InvalidError(
94
+ "You must call wait() before accessing the returncode. "
95
+ "To poll for the status of a running process, use poll() instead."
96
+ )
97
+ return self._returncode
98
+
99
+ async def poll(self) -> Optional[int]:
100
+ """Check if the container process has finished running.
101
+
102
+ Returns `None` if the process is still running, else returns the exit code.
103
+ """
104
+ if self._returncode is not None:
105
+ return self._returncode
106
+ if self._exec_deadline and time.monotonic() >= self._exec_deadline:
107
+ # TODO(matt): In the future, it would be nice to raise a ContainerExecTimeoutError to make it
108
+ # clear to the user that their sandbox terminated due to a timeout
109
+ self._returncode = -1
110
+ return self._returncode
111
+
112
+ req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=0)
113
+ resp: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(self._client.stub.ContainerExecWait, req)
114
+
115
+ if resp.completed:
116
+ self._returncode = resp.exit_code
117
+ return self._returncode
118
+
119
+ return None
120
+
121
+ async def _wait_for_completion(self) -> int:
122
+ while True:
123
+ req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=10)
124
+ resp: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(
125
+ self._client.stub.ContainerExecWait, req
126
+ )
127
+ if resp.completed:
128
+ return resp.exit_code
129
+
130
+ async def wait(self) -> int:
131
+ """Wait for the container process to finish running. Returns the exit code."""
132
+ if self._returncode is not None:
133
+ return self._returncode
134
+
135
+ try:
136
+ timeout = None
137
+ if self._exec_deadline:
138
+ timeout = self._exec_deadline - time.monotonic()
139
+ if timeout <= 0:
140
+ raise TimeoutError()
141
+ self._returncode = await asyncio.wait_for(self._wait_for_completion(), timeout=timeout)
142
+ except (asyncio.TimeoutError, TimeoutError):
143
+ self._returncode = -1
144
+ logger.debug(f"ContainerProcess {self._process_id} wait completed with returncode {self._returncode}")
145
+ return self._returncode
146
+
147
+ async def attach(self):
148
+ """mdmd:hidden"""
149
+ if platform.system() == "Windows":
150
+ print("interactive exec is not currently supported on Windows.")
151
+ return
152
+
153
+ from ._output import make_console
154
+
155
+ console = make_console()
156
+
157
+ connecting_status = console.status("Connecting...")
158
+ connecting_status.start()
159
+ on_connect = asyncio.Event()
160
+
161
+ async def _write_to_fd_loop(stream: _StreamReader):
162
+ # This is required to make modal shell to an existing task work,
163
+ # since that uses ContainerExec RPCs directly, but this is hacky.
164
+ #
165
+ # TODO(saltzm): Once we use the new exec path for that use case, this code can all be removed.
166
+ from .io_streams import _StreamReaderThroughServer
167
+
168
+ assert isinstance(stream._impl, _StreamReaderThroughServer)
169
+ stream_impl = stream._impl
170
+ # Don't skip empty messages so we can detect when the process has booted.
171
+ async for chunk in stream_impl._get_logs(skip_empty_messages=False):
172
+ if chunk is None:
173
+ break
174
+
175
+ if not on_connect.is_set():
176
+ connecting_status.stop()
177
+ on_connect.set()
178
+
179
+ await write_to_fd(stream.file_descriptor, chunk)
180
+
181
+ async def _handle_input(data: bytes, message_index: int):
182
+ self.stdin.write(data)
183
+ await self.stdin.drain()
184
+
185
+ async with TaskContext() as tc:
186
+ stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
187
+ stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
188
+
189
+ try:
190
+ # time out if we can't connect to the server fast enough
191
+ await asyncio.wait_for(on_connect.wait(), timeout=60)
192
+
193
+ async with stream_from_stdin(_handle_input, use_raw_terminal=True):
194
+ await stdout_task
195
+ await stderr_task
196
+
197
+ # TODO: this doesn't work right now.
198
+ # if exit_status != 0:
199
+ # raise ExecutionError(f"Process exited with status code {exit_status}")
200
+
201
+ except (asyncio.TimeoutError, TimeoutError):
202
+ connecting_status.stop()
203
+ stdout_task.cancel()
204
+ stderr_task.cancel()
205
+ raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
206
+
207
+
208
+ async def _iter_stream_as_bytes(stream: _StreamReader[T]):
209
+ """Yield raw bytes from a StreamReader regardless of text mode/backend."""
210
+ async for part in stream:
211
+ if isinstance(part, str):
212
+ yield part.encode("utf-8")
213
+ else:
214
+ yield part
215
+
216
+
217
+ class _ContainerProcessThroughCommandRouter(Generic[T]):
218
+ """
219
+ Container process implementation that works via direct communication with
220
+ the Modal worker where the container is running.
221
+ """
222
+
223
+ def __init__(
224
+ self,
225
+ process_id: str,
226
+ client: _Client,
227
+ command_router_client: TaskCommandRouterClient,
228
+ task_id: str,
229
+ *,
230
+ stdout: StreamType = StreamType.PIPE,
231
+ stderr: StreamType = StreamType.PIPE,
232
+ exec_deadline: Optional[float] = None,
233
+ text: bool = True,
234
+ by_line: bool = False,
235
+ ) -> None:
236
+ self._client = client
237
+ self._command_router_client = command_router_client
238
+ self._process_id = process_id
239
+ self._exec_deadline = exec_deadline
240
+ self._text = text
241
+ self._by_line = by_line
242
+ self._task_id = task_id
243
+ self._stdout = _StreamReader[T](
244
+ api_pb2.FILE_DESCRIPTOR_STDOUT,
245
+ process_id,
246
+ "container_process",
247
+ self._client,
248
+ stream_type=stdout,
249
+ text=text,
250
+ by_line=by_line,
251
+ deadline=exec_deadline,
252
+ command_router_client=self._command_router_client,
253
+ task_id=self._task_id,
254
+ )
255
+ self._stderr = _StreamReader[T](
256
+ api_pb2.FILE_DESCRIPTOR_STDERR,
257
+ process_id,
258
+ "container_process",
259
+ self._client,
260
+ stream_type=stderr,
261
+ text=text,
262
+ by_line=by_line,
263
+ deadline=exec_deadline,
264
+ command_router_client=self._command_router_client,
265
+ task_id=self._task_id,
266
+ )
267
+ self._stdin = _StreamWriter(
268
+ process_id,
269
+ "container_process",
270
+ self._client,
271
+ command_router_client=self._command_router_client,
272
+ task_id=self._task_id,
273
+ )
274
+ self._returncode = None
275
+
276
+ @property
277
+ def stdout(self) -> _StreamReader[T]:
278
+ return self._stdout
279
+
280
+ @property
281
+ def stderr(self) -> _StreamReader[T]:
282
+ return self._stderr
283
+
284
+ @property
285
+ def stdin(self) -> _StreamWriter:
286
+ return self._stdin
287
+
288
+ @property
289
+ def returncode(self) -> int:
290
+ if self._returncode is None:
291
+ raise InvalidError(
292
+ "You must call wait() before accessing the returncode. "
293
+ "To poll for the status of a running process, use poll() instead."
294
+ )
295
+ return self._returncode
296
+
297
+ async def poll(self) -> Optional[int]:
298
+ if self._returncode is not None:
299
+ return self._returncode
300
+ try:
301
+ resp = await self._command_router_client.exec_poll(self._task_id, self._process_id, self._exec_deadline)
302
+ which = resp.WhichOneof("exit_status")
303
+ if which is None:
304
+ return None
305
+
306
+ if which == "code":
307
+ self._returncode = int(resp.code)
308
+ return self._returncode
309
+ elif which == "signal":
310
+ self._returncode = 128 + int(resp.signal)
311
+ return self._returncode
312
+ else:
313
+ logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
314
+ raise InvalidError("Unexpected exit status")
315
+ except ExecTimeoutError:
316
+ logger.debug(f"ContainerProcess poll for {self._process_id} did not complete within deadline")
317
+ return None
318
+ except Exception as e:
319
+ # Re-raise non-transient errors or errors resulting from exceeding retries on transient errors.
320
+ logger.warning(f"ContainerProcess poll for {self._process_id} failed: {e}")
321
+ raise
322
+
323
+ async def wait(self) -> int:
324
+ if self._returncode is not None:
325
+ return self._returncode
326
+
327
+ try:
328
+ resp = await self._command_router_client.exec_wait(self._task_id, self._process_id, self._exec_deadline)
329
+ which = resp.WhichOneof("exit_status")
330
+ if which == "code":
331
+ self._returncode = int(resp.code)
332
+ elif which == "signal":
333
+ self._returncode = 128 + int(resp.signal)
334
+ else:
335
+ logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
336
+ self._returncode = -1
337
+ raise InvalidError("Unexpected exit status")
338
+ except ExecTimeoutError:
339
+ logger.debug(f"ContainerProcess {self._process_id} did not complete within deadline")
340
+ # TODO(saltzm): This is a weird API, but customers currently may rely on it. This
341
+ # should be a ExecTimeoutError.
342
+ self._returncode = -1
343
+
344
+ return self._returncode
345
+
346
+ async def attach(self):
347
+ if platform.system() == "Windows":
348
+ print("interactive exec is not currently supported on Windows.")
349
+ return
350
+
351
+ from ._output import make_console
352
+
353
+ console = make_console()
354
+
355
+ connecting_status = console.status("Connecting...")
356
+ connecting_status.start()
357
+ on_connect = asyncio.Event()
358
+
359
+ async def _write_to_fd_loop(stream: _StreamReader[T]):
360
+ async for chunk in _iter_stream_as_bytes(stream):
361
+ if chunk is None:
362
+ break
363
+
364
+ if not on_connect.is_set():
365
+ connecting_status.stop()
366
+ on_connect.set()
367
+
368
+ await write_to_fd(stream.file_descriptor, chunk)
369
+
370
+ async def _handle_input(data: bytes, message_index: int):
371
+ self.stdin.write(data)
372
+ await self.stdin.drain()
373
+
374
+ async with TaskContext() as tc:
375
+ stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
376
+ stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
377
+
378
+ try:
379
+ # Time out if we can't connect fast enough.
380
+ await asyncio.wait_for(on_connect.wait(), timeout=60)
381
+
382
+ async with stream_from_stdin(_handle_input, use_raw_terminal=True):
383
+ await stdout_task
384
+ await stderr_task
385
+
386
+ except (asyncio.TimeoutError, TimeoutError):
387
+ connecting_status.stop()
388
+ stdout_task.cancel()
389
+ stderr_task.cancel()
390
+ raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
391
+
392
+
393
+ class _ContainerProcess(Generic[T]):
394
+ """Represents a running process in a container."""
395
+
396
+ def __init__(
397
+ self,
398
+ process_id: str,
399
+ task_id: str,
400
+ client: _Client,
401
+ stdout: StreamType = StreamType.PIPE,
402
+ stderr: StreamType = StreamType.PIPE,
403
+ exec_deadline: Optional[float] = None,
404
+ text: bool = True,
405
+ by_line: bool = False,
406
+ command_router_client: Optional[TaskCommandRouterClient] = None,
407
+ ) -> None:
408
+ if command_router_client is None:
409
+ self._impl = _ContainerProcessThroughServer(
410
+ process_id,
411
+ task_id,
412
+ client,
413
+ stdout=stdout,
414
+ stderr=stderr,
415
+ exec_deadline=exec_deadline,
416
+ text=text,
417
+ by_line=by_line,
418
+ )
419
+ else:
420
+ self._impl = _ContainerProcessThroughCommandRouter(
421
+ process_id,
422
+ client,
423
+ command_router_client,
424
+ task_id,
425
+ stdout=stdout,
426
+ stderr=stderr,
427
+ exec_deadline=exec_deadline,
428
+ text=text,
429
+ by_line=by_line,
430
+ )
431
+
432
+ def __repr__(self) -> str:
433
+ return self._impl.__repr__()
434
+
435
+ @property
436
+ def stdout(self) -> _StreamReader[T]:
437
+ """StreamReader for the container process's stdout stream."""
438
+ return self._impl.stdout
439
+
440
+ @property
441
+ def stderr(self) -> _StreamReader[T]:
442
+ """StreamReader for the container process's stderr stream."""
443
+ return self._impl.stderr
444
+
445
+ @property
446
+ def stdin(self) -> _StreamWriter:
447
+ """StreamWriter for the container process's stdin stream."""
448
+ return self._impl.stdin
449
+
450
+ @property
451
+ def returncode(self) -> int:
452
+ return self._impl.returncode
453
+
454
+ async def poll(self) -> Optional[int]:
455
+ """Check if the container process has finished running.
456
+
457
+ Returns `None` if the process is still running, else returns the exit code.
458
+ """
459
+ return await self._impl.poll()
460
+
461
+ async def wait(self) -> int:
462
+ """Wait for the container process to finish running. Returns the exit code."""
463
+ return await self._impl.wait()
464
+
465
+ async def attach(self):
466
+ """mdmd:hidden"""
467
+ await self._impl.attach()
468
+
469
+
470
+ ContainerProcess = synchronize_api(_ContainerProcess)