modal 1.4.4.dev8__tar.gz → 1.4.4.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.
Files changed (214) hide show
  1. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/PKG-INFO +1 -1
  2. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/task_command_router_client.py +154 -38
  3. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/app.pyi +1 -1
  4. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/client.pyi +2 -2
  5. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/container_process.py +5 -5
  6. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/functions.pyi +1 -1
  7. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/image.pyi +1 -1
  8. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/io_streams.py +130 -51
  9. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/io_streams.pyi +179 -24
  10. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/sandbox.py +89 -11
  11. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/sandbox.pyi +67 -12
  12. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal.egg-info/PKG-INFO +1 -1
  13. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/api_pb2.py +358 -358
  14. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/api_pb2.pyi +7 -1
  15. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/task_command_router_pb2.py +80 -80
  16. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/task_command_router_pb2.pyi +4 -1
  17. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_version/__init__.py +1 -1
  18. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/LICENSE +0 -0
  19. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/README.md +0 -0
  20. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/__init__.py +0 -0
  21. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/__main__.py +0 -0
  22. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_billing.py +0 -0
  23. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_clustered_functions.py +0 -0
  24. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_clustered_functions.pyi +0 -0
  25. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_container_entrypoint.py +0 -0
  26. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_environments.py +0 -0
  27. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_function_variants.py +0 -0
  28. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_functions.py +0 -0
  29. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_grpc_client.py +0 -0
  30. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_ipython.py +0 -0
  31. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_load_context.py +0 -0
  32. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_location.py +0 -0
  33. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_logs.py +0 -0
  34. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_object.py +0 -0
  35. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_output/__init__.py +0 -0
  36. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_output/manager.py +0 -0
  37. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_output/pty.py +0 -0
  38. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_output/rich.py +0 -0
  39. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_output/status.py +0 -0
  40. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_partial_function.py +0 -0
  41. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_resolver.py +0 -0
  42. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_resources.py +0 -0
  43. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_runtime/__init__.py +0 -0
  44. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_runtime/asgi.py +0 -0
  45. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_runtime/container_io_manager.py +0 -0
  46. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_runtime/container_io_manager.pyi +0 -0
  47. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_runtime/execution_context.py +0 -0
  48. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_runtime/execution_context.pyi +0 -0
  49. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  50. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_runtime/telemetry.py +0 -0
  51. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_runtime/user_code_event_loop.py +0 -0
  52. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_runtime/user_code_imports.py +0 -0
  53. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_serialization.py +0 -0
  54. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_server.py +0 -0
  55. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_traceback.py +0 -0
  56. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_tunnel.py +0 -0
  57. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_tunnel.pyi +0 -0
  58. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_type_manager.py +0 -0
  59. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/__init__.py +0 -0
  60. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/app_utils.py +0 -0
  61. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/async_utils.py +0 -0
  62. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/auth_token_manager.py +0 -0
  63. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/blob_utils.py +0 -0
  64. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/browser_utils.py +0 -0
  65. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/bytes_io_segment_payload.py +0 -0
  66. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/deprecation.py +0 -0
  67. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/docker_utils.py +0 -0
  68. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/function_utils.py +0 -0
  69. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/git_utils.py +0 -0
  70. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/grpc_testing.py +0 -0
  71. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/grpc_utils.py +0 -0
  72. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/hash_utils.py +0 -0
  73. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/http_utils.py +0 -0
  74. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/jwt_utils.py +0 -0
  75. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/logger.py +0 -0
  76. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/mount_utils.py +0 -0
  77. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/name_utils.py +0 -0
  78. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/package_utils.py +0 -0
  79. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/pattern_utils.py +0 -0
  80. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/rand_pb_testing.py +0 -0
  81. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/sandbox_fs_utils.py +0 -0
  82. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/shell_utils.py +0 -0
  83. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_utils/time_utils.py +0 -0
  84. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_vendor/__init__.py +0 -0
  85. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  86. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_vendor/cloudpickle.py +0 -0
  87. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_vendor/tblib.py +0 -0
  88. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_vendor/version.py +0 -0
  89. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/_watcher.py +0 -0
  90. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/app.py +0 -0
  91. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/billing.py +0 -0
  92. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/builder/2023.12.312.txt +0 -0
  93. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/builder/2023.12.txt +0 -0
  94. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/builder/2024.04.txt +0 -0
  95. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/builder/2024.10.txt +0 -0
  96. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/builder/2025.06.txt +0 -0
  97. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/builder/PREVIEW.txt +0 -0
  98. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/builder/README.md +0 -0
  99. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/builder/base-images.json +0 -0
  100. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/call_graph.py +0 -0
  101. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/__init__.py +0 -0
  102. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/_download.py +0 -0
  103. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/_help.py +0 -0
  104. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/_traceback.py +0 -0
  105. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/app.py +0 -0
  106. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/billing.py +0 -0
  107. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/bootstrap.py +0 -0
  108. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/changelog.py +0 -0
  109. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/cluster.py +0 -0
  110. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/config.py +0 -0
  111. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/container.py +0 -0
  112. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/dashboard.py +0 -0
  113. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/dict.py +0 -0
  114. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/entry_point.py +0 -0
  115. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/environment.py +0 -0
  116. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/import_refs.py +0 -0
  117. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/launch.py +0 -0
  118. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/logo.py +0 -0
  119. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/network_file_system.py +0 -0
  120. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/profile.py +0 -0
  121. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/programs/__init__.py +0 -0
  122. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/programs/run_jupyter.py +0 -0
  123. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/programs/vscode.py +0 -0
  124. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/queues.py +0 -0
  125. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/run.py +0 -0
  126. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/secret.py +0 -0
  127. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/selector.py +0 -0
  128. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/shell.py +0 -0
  129. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/token.py +0 -0
  130. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/utils.py +0 -0
  131. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cli/volume.py +0 -0
  132. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/client.py +0 -0
  133. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cloud_bucket_mount.py +0 -0
  134. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cloud_bucket_mount.pyi +0 -0
  135. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cls.py +0 -0
  136. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/cls.pyi +0 -0
  137. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/config.py +0 -0
  138. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/container_process.pyi +0 -0
  139. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/dict.py +0 -0
  140. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/dict.pyi +0 -0
  141. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/environments.py +0 -0
  142. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/environments.pyi +0 -0
  143. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/exception.py +0 -0
  144. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/experimental/__init__.py +0 -0
  145. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/experimental/flash.py +0 -0
  146. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/experimental/flash.pyi +0 -0
  147. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/experimental/ipython.py +0 -0
  148. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/file_io.py +0 -0
  149. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/file_io.pyi +0 -0
  150. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/file_pattern_matcher.py +0 -0
  151. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/functions.py +0 -0
  152. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/image.py +0 -0
  153. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/mount.py +0 -0
  154. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/mount.pyi +0 -0
  155. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/network_file_system.py +0 -0
  156. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/network_file_system.pyi +0 -0
  157. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/object.py +0 -0
  158. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/object.pyi +0 -0
  159. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/output.py +0 -0
  160. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/parallel_map.py +0 -0
  161. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/parallel_map.pyi +0 -0
  162. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/partial_function.py +0 -0
  163. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/partial_function.pyi +0 -0
  164. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/proxy.py +0 -0
  165. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/proxy.pyi +0 -0
  166. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/py.typed +0 -0
  167. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/queue.py +0 -0
  168. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/queue.pyi +0 -0
  169. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/retries.py +0 -0
  170. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/runner.py +0 -0
  171. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/runner.pyi +0 -0
  172. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/running_app.py +0 -0
  173. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/sandbox_fs.py +0 -0
  174. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/sandbox_fs.pyi +0 -0
  175. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/schedule.py +0 -0
  176. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/scheduler_placement.py +0 -0
  177. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/secret.py +0 -0
  178. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/secret.pyi +0 -0
  179. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/server.py +0 -0
  180. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/server.pyi +0 -0
  181. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/serving.py +0 -0
  182. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/serving.pyi +0 -0
  183. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/snapshot.py +0 -0
  184. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/snapshot.pyi +0 -0
  185. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/stream_type.py +0 -0
  186. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/token_flow.py +0 -0
  187. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/token_flow.pyi +0 -0
  188. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/volume.py +0 -0
  189. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal/volume.pyi +0 -0
  190. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal.egg-info/SOURCES.txt +0 -0
  191. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal.egg-info/dependency_links.txt +0 -0
  192. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal.egg-info/entry_points.txt +0 -0
  193. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal.egg-info/requires.txt +0 -0
  194. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal.egg-info/top_level.txt +0 -0
  195. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_docs/__init__.py +0 -0
  196. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_docs/gen_cli_docs.py +0 -0
  197. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_docs/gen_cli_docs_main.py +0 -0
  198. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_docs/gen_reference_docs.py +0 -0
  199. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_docs/gen_reference_docs_main.py +0 -0
  200. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_docs/mdmd/__init__.py +0 -0
  201. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_docs/mdmd/mdmd.py +0 -0
  202. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_docs/mdmd/signatures.py +0 -0
  203. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/__init__.py +0 -0
  204. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/api_grpc.py +0 -0
  205. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/api_pb2_grpc.py +0 -0
  206. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/api_pb2_grpc.pyi +0 -0
  207. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/modal_api_grpc.py +0 -0
  208. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/py.typed +0 -0
  209. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/task_command_router_grpc.py +0 -0
  210. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  211. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  212. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/modal_version/__main__.py +0 -0
  213. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/pyproject.toml +0 -0
  214. {modal-1.4.4.dev8 → modal-1.4.4.dev10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.4.4.dev8
3
+ Version: 1.4.4.dev10
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -9,7 +9,7 @@ import typing
9
9
  import urllib.parse
10
10
  import weakref
11
11
  from contextlib import suppress
12
- from typing import AsyncGenerator, Optional
12
+ from typing import AsyncGenerator, Callable, Optional, TypeVar
13
13
 
14
14
  import grpclib.client
15
15
  import grpclib.config
@@ -146,6 +146,10 @@ async def call_with_retries_on_transient_errors(
146
146
  await sleep_and_advance(e)
147
147
 
148
148
 
149
+ _StdioReq = TypeVar("_StdioReq")
150
+ _StdioResp = TypeVar("_StdioResp", sr_pb2.TaskExecStdioReadResponse, sr_pb2.SandboxStdioReadV2Response)
151
+
152
+
149
153
  async def fetch_command_router_access(server_client, task_id: str) -> api_pb2.TaskGetCommandRouterAccessResponse:
150
154
  """Fetch direct command router access info from Modal server."""
151
155
  return await server_client.stub.TaskGetCommandRouterAccess(
@@ -383,7 +387,7 @@ class TaskCommandRouterClient:
383
387
  file_descriptor: "api_pb2.FileDescriptor.ValueType",
384
388
  deadline: Optional[float] = None,
385
389
  ) -> AsyncGenerator[sr_pb2.TaskExecStdioReadResponse, None]:
386
- """Stream stdout/stderr batches from the task, properly retrying on transient errors.
390
+ """Stream stdout/stderr batches from an exec'd command, retrying on transient errors.
387
391
 
388
392
  Args:
389
393
  task_id: The task ID of the task running the exec'd command.
@@ -413,6 +417,39 @@ class TaskCommandRouterClient:
413
417
  async for item in stream:
414
418
  yield item
415
419
 
420
+ async def sandbox_stdio_read(
421
+ self,
422
+ task_id: str,
423
+ # Quotes around the type required for protobuf 3.19.
424
+ file_descriptor: "api_pb2.FileDescriptor.ValueType",
425
+ ) -> AsyncGenerator[sr_pb2.SandboxStdioReadV2Response, None]:
426
+ """Stream stdout/stderr batches from a V2 sandbox.
427
+
428
+ Serves both live reads and post-exit reads.
429
+
430
+ Args:
431
+ task_id: The task ID hosting the V2 sandbox.
432
+ file_descriptor: The file descriptor to read from (stdout or stderr).
433
+ Returns:
434
+ AsyncGenerator[sr_pb2.SandboxStdioReadV2Response, None]: A stream of stdout/stderr batches.
435
+ Raises:
436
+ Errors: If retries are exhausted on transient errors or if there is
437
+ an error from the RPC itself.
438
+ """
439
+ if file_descriptor == api_pb2.FILE_DESCRIPTOR_STDOUT:
440
+ sr_fd = sr_pb2.SANDBOX_STDIO_FILE_DESCRIPTOR_STDOUT
441
+ elif file_descriptor == api_pb2.FILE_DESCRIPTOR_STDERR:
442
+ sr_fd = sr_pb2.SANDBOX_STDIO_FILE_DESCRIPTOR_STDERR
443
+ elif file_descriptor == api_pb2.FILE_DESCRIPTOR_INFO or file_descriptor == api_pb2.FILE_DESCRIPTOR_UNSPECIFIED:
444
+ raise ValueError(f"Unsupported file descriptor: {file_descriptor}")
445
+ else:
446
+ raise ValueError(f"Invalid file descriptor: {file_descriptor}")
447
+
448
+ with grpc_error_converter():
449
+ async with aclosing(self._stream_sandbox_stdio(task_id, sr_fd)) as stream:
450
+ async for item in stream:
451
+ yield item
452
+
416
453
  async def exec_stdin_write(
417
454
  self, task_id: str, exec_id: str, offset: int, data: bytes, eof: bool
418
455
  ) -> sr_pb2.TaskExecStdinWriteResponse:
@@ -434,6 +471,25 @@ class TaskCommandRouterClient:
434
471
  lambda: self._call_with_auth_retry(self._stub.TaskExecStdinWrite, request)
435
472
  )
436
473
 
474
+ async def sandbox_stdin_write_v2(
475
+ self, task_id: str, offset: int, data: bytes, eof: bool
476
+ ) -> sr_pb2.SandboxStdinWriteV2Response:
477
+ """Write to the stdin stream of a V2 sandbox's entrypoint process.
478
+
479
+ Args:
480
+ task_id: The task ID of the V2 sandbox.
481
+ offset: The offset to start writing to.
482
+ eof: Whether to close the stdin stream after writing the data.
483
+ Raises:
484
+ Other errors: If retries are exhausted on transient errors or if there's an error
485
+ from the RPC itself.
486
+ """
487
+ request = sr_pb2.SandboxStdinWriteV2Request(task_id=task_id, offset=offset, data=data, eof=eof)
488
+ with grpc_error_converter():
489
+ return await call_with_retries_on_transient_errors(
490
+ lambda: self._call_with_auth_retry(self._stub.SandboxStdinWriteV2, request)
491
+ )
492
+
437
493
  async def exec_poll(
438
494
  self, task_id: str, exec_id: str, deadline: Optional[float] = None
439
495
  ) -> sr_pb2.TaskExecPollResponse:
@@ -558,16 +614,23 @@ class TaskCommandRouterClient:
558
614
  return await func(*args, **kwargs, metadata=self._get_metadata())
559
615
  raise
560
616
 
561
- async def _stream_stdio(
617
+ async def _stream_stdio_with_retries(
562
618
  self,
563
- task_id: str,
564
- exec_id: str,
565
- # Quotes around the type required for protobuf 3.19.
566
- file_descriptor: "sr_pb2.TaskExecStdioFileDescriptor.ValueType",
619
+ *,
620
+ stub_method: "grpclib.client.UnaryStreamMethod[_StdioReq, _StdioResp]",
621
+ request_factory: Callable[[int], _StdioReq],
622
+ deadline_label: str,
567
623
  deadline: Optional[float] = None,
568
- ) -> AsyncGenerator[sr_pb2.TaskExecStdioReadResponse, None]:
569
- """Stream stdio from the task, properly updating the offset and retrying on transient errors.
570
- Raises ExecTimeoutError if the deadline is exceeded.
624
+ ) -> AsyncGenerator[_StdioResp, None]:
625
+ """Drive a streaming-stdio RPC with offset bookkeeping, transient-error
626
+ retries, and JWT-refresh auth retries.
627
+
628
+ Shared by [`_stream_stdio`] (exec stdio) and [`_stream_sandbox_stdio`]
629
+ (V2 sandbox top-level stdio); both response types have a ``bytes data``
630
+ field that this helper uses to advance the offset. For V2 sandbox
631
+ responses (which carry ``starting_offset``), the offset is rebased off
632
+ the first chunk of each attempt so transient reconnects don't miss
633
+ bytes.
571
634
  """
572
635
  offset = 0
573
636
  delay_secs = self.stream_stdio_retry_delay_secs
@@ -581,7 +644,7 @@ class TaskCommandRouterClient:
581
644
  nonlocal delay_secs, num_retries_remaining
582
645
  logger.debug(f"Retrying stdio read with delay {delay_secs}s due to error: {e}")
583
646
  if deadline is not None and deadline - time.monotonic() <= delay_secs:
584
- raise ExecTimeoutError(f"Deadline exceeded while streaming stdio for exec {exec_id}")
647
+ raise ExecTimeoutError(f"Deadline exceeded while streaming stdio for {deadline_label}")
585
648
 
586
649
  await asyncio.sleep(delay_secs)
587
650
  delay_secs *= delay_factor
@@ -590,18 +653,14 @@ class TaskCommandRouterClient:
590
653
  while True:
591
654
  timeout = max(0, deadline - time.monotonic()) if deadline is not None else None
592
655
  try:
593
- stream = self._stub.TaskExecStdioRead.open(timeout=timeout, metadata=self._get_metadata())
656
+ stream = stub_method.open(timeout=timeout, metadata=self._get_metadata())
594
657
  async with stream as s:
595
- req = sr_pb2.TaskExecStdioReadRequest(
596
- task_id=task_id,
597
- exec_id=exec_id,
598
- offset=offset,
599
- file_descriptor=file_descriptor,
600
- )
658
+ req = request_factory(offset)
601
659
 
602
660
  # Auth retry is scoped to a single refresh per streaming attempt. While auth metadata is
603
661
  # sent on request start, UNAUTHENTICATED may sometimes surface during iteration,
604
662
  # so we handle it at both send and receive boundaries.
663
+ is_first_chunk_of_attempt = True
605
664
  try:
606
665
  await s.send_message(req, end=True)
607
666
  async for item in s:
@@ -610,6 +669,11 @@ class TaskCommandRouterClient:
610
669
  did_auth_retry = False
611
670
  # Reset retry backoff after any successful chunk.
612
671
  delay_secs = self.stream_stdio_retry_delay_secs
672
+ # Track it so transient reconnects request the
673
+ # correct next byte.
674
+ if is_first_chunk_of_attempt and isinstance(item, sr_pb2.SandboxStdioReadV2Response):
675
+ offset = item.starting_offset
676
+ is_first_chunk_of_attempt = False
613
677
  offset += len(item.data)
614
678
  yield item
615
679
  except GRPCError as exc:
@@ -652,6 +716,63 @@ class TaskCommandRouterClient:
652
716
  else:
653
717
  raise ConnectionError(str(e))
654
718
 
719
+ async def _stream_stdio(
720
+ self,
721
+ task_id: str,
722
+ exec_id: str,
723
+ # Quotes around the type required for protobuf 3.19.
724
+ file_descriptor: "sr_pb2.TaskExecStdioFileDescriptor.ValueType",
725
+ deadline: Optional[float] = None,
726
+ ) -> AsyncGenerator[sr_pb2.TaskExecStdioReadResponse, None]:
727
+ """Stream exec stdio from the task, retrying on transient errors.
728
+ Raises ExecTimeoutError if the deadline is exceeded.
729
+ """
730
+
731
+ def request_factory(offset: int) -> sr_pb2.TaskExecStdioReadRequest:
732
+ return sr_pb2.TaskExecStdioReadRequest(
733
+ task_id=task_id,
734
+ exec_id=exec_id,
735
+ offset=offset,
736
+ file_descriptor=file_descriptor,
737
+ )
738
+
739
+ async with aclosing(
740
+ self._stream_stdio_with_retries(
741
+ stub_method=self._stub.TaskExecStdioRead,
742
+ request_factory=request_factory,
743
+ deadline_label=f"exec {exec_id}",
744
+ deadline=deadline,
745
+ )
746
+ ) as stream:
747
+ async for item in stream:
748
+ yield item
749
+
750
+ async def _stream_sandbox_stdio(
751
+ self,
752
+ task_id: str,
753
+ # Quotes around the type required for protobuf 3.19.
754
+ file_descriptor: "sr_pb2.SandboxStdioFileDescriptor.ValueType",
755
+ ) -> AsyncGenerator[sr_pb2.SandboxStdioReadV2Response, None]:
756
+ """Stream V2 sandbox top-level stdio from the task, retrying on transient errors."""
757
+
758
+ def request_factory(offset: int) -> sr_pb2.SandboxStdioReadV2Request:
759
+ return sr_pb2.SandboxStdioReadV2Request(
760
+ task_id=task_id,
761
+ offset=offset,
762
+ file_descriptor=file_descriptor,
763
+ )
764
+
765
+ async with aclosing(
766
+ self._stream_stdio_with_retries(
767
+ stub_method=self._stub.SandboxStdioReadV2,
768
+ request_factory=request_factory,
769
+ deadline_label=f"sandbox {task_id}",
770
+ deadline=None,
771
+ )
772
+ ) as stream:
773
+ async for item in stream:
774
+ yield item
775
+
655
776
  async def mount_image(self, request: sr_pb2.TaskMountDirectoryRequest):
656
777
  with grpc_error_converter():
657
778
  return await call_with_retries_on_transient_errors(
@@ -664,21 +785,10 @@ class TaskCommandRouterClient:
664
785
  lambda: self._call_with_auth_retry(self._stub.TaskUnmountDirectory, request)
665
786
  )
666
787
 
667
- async def snapshot_directory(
668
- self, request: sr_pb2.TaskSnapshotDirectoryRequest, **kwargs
669
- ) -> sr_pb2.TaskSnapshotDirectoryResponse:
670
- with grpc_error_converter():
671
- return await call_with_retries_on_transient_errors(
672
- lambda: self._call_with_auth_retry(self._stub.TaskSnapshotDirectory, request, **kwargs)
673
- )
674
-
675
- async def snapshot_filesystem(
676
- self, request: sr_pb2.TaskSnapshotFilesystemRequest, *, timeout: float, **kwargs
677
- ) -> sr_pb2.TaskSnapshotFilesystemResponse:
678
- # Compute the overall deadline once; each retry attempt passes the
679
- # remaining budget as the per-call gRPC timeout so we honor the
680
- # caller-specified `timeout` across retries instead of giving each
681
- # attempt a fresh full window.
788
+ async def _snapshot_with_deadline(self, rpc, request, *, timeout: float, **kwargs):
789
+ # helper method for snapshot_directory and snapshot_filesystem to handle grpc
790
+ # deadlines in a consistent way, converting any error to TimeoutError after passing
791
+ # the total deadline budget
682
792
  timeout_deadline = time.monotonic() + timeout
683
793
 
684
794
  def call():
@@ -687,12 +797,8 @@ class TaskCommandRouterClient:
687
797
  # doesn't matter which exception type this is
688
798
  # as it will be caught by the catch all below
689
799
  raise ModalTimeoutError("Timeout expired")
800
+ return self._call_with_auth_retry(rpc, request, timeout=call_timeout, **kwargs)
690
801
 
691
- return self._call_with_auth_retry(
692
- self._stub.TaskSnapshotFilesystem, request, timeout=call_timeout, **kwargs
693
- )
694
-
695
- # Any failure observed at or after the deadline is treated as a timeout
696
802
  try:
697
803
  with grpc_error_converter():
698
804
  return await call_with_retries_on_transient_errors(
@@ -704,3 +810,13 @@ class TaskCommandRouterClient:
704
810
  if time.monotonic() >= timeout_deadline:
705
811
  raise ModalTimeoutError("Timeout expired")
706
812
  raise
813
+
814
+ async def snapshot_directory(
815
+ self, request: sr_pb2.TaskSnapshotDirectoryRequest, *, timeout: float, **kwargs
816
+ ) -> sr_pb2.TaskSnapshotDirectoryResponse:
817
+ return await self._snapshot_with_deadline(self._stub.TaskSnapshotDirectory, request, timeout=timeout, **kwargs)
818
+
819
+ async def snapshot_filesystem(
820
+ self, request: sr_pb2.TaskSnapshotFilesystemRequest, *, timeout: float, **kwargs
821
+ ) -> sr_pb2.TaskSnapshotFilesystemResponse:
822
+ return await self._snapshot_with_deadline(self._stub.TaskSnapshotFilesystem, request, timeout=timeout, **kwargs)
@@ -55,7 +55,7 @@ def check_sequence(items: typing.Sequence[typing.Any], item_type: type[typing.An
55
55
 
56
56
  CLS_T = typing.TypeVar("CLS_T", bound="type[typing.Any]")
57
57
 
58
- P = typing_extensions.ParamSpec("P")
58
+ P = typing.ParamSpec("P")
59
59
 
60
60
  ReturnType = typing.TypeVar("ReturnType")
61
61
 
@@ -35,7 +35,7 @@ class _Client:
35
35
  server_url: str,
36
36
  client_type: int,
37
37
  credentials: typing.Optional[tuple[str, str]],
38
- version: str = "1.4.4.dev8",
38
+ version: str = "1.4.4.dev10",
39
39
  ):
40
40
  """mdmd:hidden
41
41
  The Modal client object is not intended to be instantiated directly by users.
@@ -175,7 +175,7 @@ class Client:
175
175
  server_url: str,
176
176
  client_type: int,
177
177
  credentials: typing.Optional[tuple[str, str]],
178
- version: str = "1.4.4.dev8",
178
+ version: str = "1.4.4.dev10",
179
179
  ):
180
180
  """mdmd:hidden
181
181
  The Modal client object is not intended to be instantiated directly by users.
@@ -13,9 +13,9 @@ from .config import logger
13
13
  from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
14
14
  from .io_streams import (
15
15
  _StreamReader,
16
- _StreamReaderThroughCommandRouterParams,
16
+ _StreamReaderThroughSandboxExecCommandRouterParams,
17
17
  _StreamWriter,
18
- _StreamWriterThroughCommandRouterParams,
18
+ _StreamWriterThroughCommandRouterSandboxExecParams,
19
19
  )
20
20
  from .stream_type import StreamType
21
21
 
@@ -58,7 +58,7 @@ class _ContainerProcess(Generic[T]):
58
58
  self._by_line = by_line
59
59
  self._task_id = task_id
60
60
  self._stdout = _StreamReader[T](
61
- _StreamReaderThroughCommandRouterParams(
61
+ _StreamReaderThroughSandboxExecCommandRouterParams(
62
62
  file_descriptor=api_pb2.FILE_DESCRIPTOR_STDOUT,
63
63
  task_id=self._task_id,
64
64
  object_id=process_id,
@@ -70,7 +70,7 @@ class _ContainerProcess(Generic[T]):
70
70
  by_line=by_line,
71
71
  )
72
72
  self._stderr = _StreamReader[T](
73
- _StreamReaderThroughCommandRouterParams(
73
+ _StreamReaderThroughSandboxExecCommandRouterParams(
74
74
  file_descriptor=api_pb2.FILE_DESCRIPTOR_STDERR,
75
75
  task_id=self._task_id,
76
76
  object_id=process_id,
@@ -82,7 +82,7 @@ class _ContainerProcess(Generic[T]):
82
82
  by_line=by_line,
83
83
  )
84
84
  self._stdin = _StreamWriter(
85
- _StreamWriterThroughCommandRouterParams(
85
+ _StreamWriterThroughCommandRouterSandboxExecParams(
86
86
  task_id=self._task_id,
87
87
  object_id=process_id,
88
88
  command_router_client=self._command_router_client,
@@ -27,7 +27,7 @@ import typing_extensions
27
27
 
28
28
  ReturnType_INNER = typing.TypeVar("ReturnType_INNER", covariant=True)
29
29
 
30
- P_INNER = typing_extensions.ParamSpec("P_INNER")
30
+ P_INNER = typing.ParamSpec("P_INNER")
31
31
 
32
32
  class Function(
33
33
  typing.Generic[modal._functions.P, modal._functions.ReturnType, modal._functions.OriginalReturnType],
@@ -29,7 +29,7 @@ class _AutoDockerIgnoreSentinel:
29
29
 
30
30
  AUTO_DOCKERIGNORE: _AutoDockerIgnoreSentinel
31
31
 
32
- P = typing_extensions.ParamSpec("P")
32
+ P = typing.ParamSpec("P")
33
33
 
34
34
  def _validate_python_version(
35
35
  python_version: typing.Optional[str],
@@ -4,7 +4,8 @@ import codecs
4
4
  import contextlib
5
5
  import io
6
6
  import sys
7
- from collections.abc import AsyncGenerator, AsyncIterator
7
+ from abc import ABC, abstractmethod
8
+ from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable
8
9
  from dataclasses import dataclass
9
10
  from typing import (
10
11
  TYPE_CHECKING,
@@ -231,9 +232,9 @@ class _StreamReaderThroughServerParams:
231
232
 
232
233
 
233
234
  @dataclass(frozen=True)
234
- class _StreamReaderThroughCommandRouterParams:
235
- """Parameters for a ``_StreamReader`` that reads container-process stdio
236
- directly from the worker via the task command router."""
235
+ class _StreamReaderThroughSandboxExecCommandRouterParams:
236
+ """Parameters for a ``_StreamReader`` that reads sandbox-exec stdio
237
+ directly from the worker via the task command router (``exec_stdio_read``)."""
237
238
 
238
239
  file_descriptor: "api_pb2.FileDescriptor.ValueType"
239
240
  task_id: str
@@ -242,10 +243,50 @@ class _StreamReaderThroughCommandRouterParams:
242
243
  deadline: Optional[float]
243
244
 
244
245
 
245
- async def _stdio_stream_from_command_router(
246
- params: _StreamReaderThroughCommandRouterParams,
246
+ @dataclass(frozen=True)
247
+ class _StreamReaderThroughSandboxCommandRouterParams:
248
+ """Parameters for a ``_StreamReader`` that reads a V2 sandbox's
249
+ stdio directly from the worker via the task command router
250
+ (``sandbox_stdio_read``)."""
251
+
252
+ file_descriptor: "api_pb2.FileDescriptor.ValueType"
253
+ sandbox_id: str
254
+ # Lazily fetches ``(task_id, command_router_client)`` the first time the
255
+ # stream is iterated. Captures the sandbox handle so we only mint a JWT
256
+ # and open a connection to the worker when stdio is actually read.
257
+ resolve_router: Callable[[], Awaitable[tuple[str, TaskCommandRouterClient]]]
258
+
259
+
260
+ _StreamReaderThroughCommandRouterParams = (
261
+ _StreamReaderThroughSandboxExecCommandRouterParams | _StreamReaderThroughSandboxCommandRouterParams
262
+ )
263
+
264
+
265
+ async def _stdio_stream_from_sandbox_command_router(
266
+ params: _StreamReaderThroughSandboxCommandRouterParams,
267
+ ) -> AsyncGenerator[bytes, None]:
268
+ """Stream raw bytes from a V2 sandbox's primary stdio via ``sandbox_stdio_read``."""
269
+ task_id, command_router_client = await params.resolve_router()
270
+ first_chunk = True
271
+ async with aclosing(command_router_client.sandbox_stdio_read(task_id, params.file_descriptor)) as stream:
272
+ async for item in stream:
273
+ if len(item.data) == 0:
274
+ raise ValueError("Received empty message streaming stdio from sandbox.")
275
+ if first_chunk:
276
+ first_chunk = False
277
+ if item.starting_offset > 0:
278
+ logger.warning(
279
+ f"V2 sandbox {params.sandbox_id} stdio: dropped first "
280
+ f"{item.starting_offset} bytes; only the most recent portion "
281
+ f"of output is retained."
282
+ )
283
+ yield item.data
284
+
285
+
286
+ async def _stdio_stream_from_sandbox_exec_command_router(
287
+ params: _StreamReaderThroughSandboxExecCommandRouterParams,
247
288
  ) -> AsyncGenerator[bytes, None]:
248
- """Stream raw bytes from the router client."""
289
+ """Stream raw bytes from a V2 sandbox-exec'd process via ``exec_stdio_read``."""
249
290
  async with aclosing(
250
291
  params.command_router_client.exec_stdio_read(
251
292
  params.task_id, params.object_id, params.file_descriptor, params.deadline
@@ -254,9 +295,7 @@ async def _stdio_stream_from_command_router(
254
295
  try:
255
296
  async for item in stream:
256
297
  if len(item.data) == 0:
257
- # This is an error.
258
298
  raise ValueError("Received empty message streaming stdio from sandbox.")
259
-
260
299
  yield item.data
261
300
  except ExecTimeoutError:
262
301
  logger.debug(f"Deadline exceeded while streaming stdio for exec {params.object_id}")
@@ -265,20 +304,22 @@ async def _stdio_stream_from_command_router(
265
304
  return
266
305
 
267
306
 
268
- class _BytesStreamReaderThroughCommandRouter:
269
- """
270
- StreamReader implementation that will read directly from the worker that
271
- hosts the sandbox.
307
+ def _stdio_stream_from_command_router(
308
+ params: _StreamReaderThroughCommandRouterParams,
309
+ ) -> AsyncGenerator[bytes, None]:
310
+ """Dispatch between the V2-sandbox primary stdio and the V2-sandbox-exec
311
+ stdio streams, both of which yield raw bytes."""
312
+ if isinstance(params, _StreamReaderThroughSandboxCommandRouterParams):
313
+ return _stdio_stream_from_sandbox_command_router(params)
314
+ return _stdio_stream_from_sandbox_exec_command_router(params)
272
315
 
273
- This implementation is used for non-text streams.
274
- """
275
316
 
276
- def __init__(
277
- self,
278
- params: _StreamReaderThroughCommandRouterParams,
279
- ) -> None:
317
+ class _BytesStreamReaderThroughCommandRouter:
318
+ """StreamReader that yields raw bytes from the router-backed stdio source
319
+ (either V2 sandbox top-level stdio or V2 sandbox-exec stdio)."""
320
+
321
+ def __init__(self, params: _StreamReaderThroughCommandRouterParams) -> None:
280
322
  self._params = params
281
- self._stream = None
282
323
 
283
324
  @property
284
325
  def file_descriptor(self) -> int:
@@ -300,18 +341,10 @@ class _BytesStreamReaderThroughCommandRouter:
300
341
 
301
342
 
302
343
  class _TextStreamReaderThroughCommandRouter:
303
- """
304
- StreamReader implementation that will read directly from the worker
305
- that hosts the sandbox.
344
+ """StreamReader that yields UTF-8-decoded text from the router-backed
345
+ stdio source."""
306
346
 
307
- This implementation is used for text streams.
308
- """
309
-
310
- def __init__(
311
- self,
312
- params: _StreamReaderThroughCommandRouterParams,
313
- by_line: bool,
314
- ) -> None:
347
+ def __init__(self, params: _StreamReaderThroughCommandRouterParams, by_line: bool) -> None:
315
348
  self._params = params
316
349
  self._by_line = by_line
317
350
 
@@ -513,15 +546,28 @@ class _StreamWriterThroughServerParams:
513
546
 
514
547
 
515
548
  @dataclass(frozen=True)
516
- class _StreamWriterThroughCommandRouterParams:
517
- """Parameters for a ``_StreamWriter`` that writes container-process stdin
518
- directly to the worker via the task command router."""
549
+ class _StreamWriterThroughCommandRouterSandboxExecParams:
550
+ """Parameters for a ``_StreamWriter`` that writes the stdin of a process
551
+ spawned via ``sb.exec(...)`` directly to the worker via the task command
552
+ router."""
519
553
 
520
554
  task_id: str
521
555
  object_id: str
522
556
  command_router_client: TaskCommandRouterClient
523
557
 
524
558
 
559
+ @dataclass(frozen=True)
560
+ class _StreamWriterThroughCommandRouterSandboxParams:
561
+ """Parameters for a ``_StreamWriter`` that writes a V2 sandbox entrypoint's
562
+ stdin directly to the worker via the task command router.
563
+ """
564
+
565
+ # Lazily fetches ``(task_id, command_router_client)`` the first time the
566
+ # writer drains. Captures the sandbox handle so we only mint a JWT and
567
+ # open a connection to the worker when stdin is actually written.
568
+ resolve_router: Callable[[], Awaitable[tuple[str, TaskCommandRouterClient]]]
569
+
570
+
525
571
  class _StreamWriterThroughServer:
526
572
  """Provides an interface to buffer and write to a sandbox stream (`stdin`) via the server."""
527
573
 
@@ -584,14 +630,18 @@ class _StreamWriterThroughServer:
584
630
  raise ValueError(str(exc))
585
631
 
586
632
 
587
- class _StreamWriterThroughCommandRouter:
588
- def __init__(self, params: _StreamWriterThroughCommandRouterParams) -> None:
589
- self._object_id = params.object_id
590
- self._command_router_client = params.command_router_client
591
- self._task_id = params.task_id
592
- self._is_closed = False
593
- self._buffer = bytearray()
594
- self._offset = 0
633
+ class _StreamWriterThroughCommandRouterBuffer(ABC):
634
+ """Buffering/draining logic for stream writers that flush data
635
+ to the task command router."""
636
+
637
+ def __init__(self) -> None:
638
+ self._buffer: bytearray = bytearray()
639
+ self._is_closed: bool = False
640
+ self._offset: int = 0
641
+
642
+ @abstractmethod
643
+ async def stdin_write(self, data: bytes, eof: bool) -> None:
644
+ """Write the given chunk (with optional EOF) to the command router."""
595
645
 
596
646
  def write(self, data: Union[bytes, bytearray, memoryview, str]) -> None:
597
647
  if self._is_closed:
@@ -613,29 +663,58 @@ class _StreamWriterThroughCommandRouter:
613
663
  # NB: There's no need to prevent writing eof twice, because the command router will ignore the second EOF.
614
664
  if self._buffer or eof:
615
665
  data = bytes(self._buffer)
616
- await self._command_router_client.exec_stdin_write(
617
- task_id=self._task_id, exec_id=self._object_id, offset=self._offset, data=data, eof=eof
618
- )
666
+ await self.stdin_write(data, eof)
619
667
  # Only clear the buffer after writing the data to the command router is successful.
620
668
  # This allows the client to retry drain() in the event of an exception (though
621
- # exec_stdin_write already retries on transient errors, so most users will probably
622
- # not do this).
669
+ # the underlying write call already retries on transient errors, so most users will
670
+ # probably not do this).
623
671
  self._buffer.clear()
624
672
  self._offset += len(data)
625
673
 
626
674
 
675
+ class _StreamWriterThroughCommandRouterSandboxExec(_StreamWriterThroughCommandRouterBuffer):
676
+ def __init__(self, params: _StreamWriterThroughCommandRouterSandboxExecParams) -> None:
677
+ super().__init__()
678
+ self._object_id = params.object_id
679
+ self._command_router_client = params.command_router_client
680
+ self._task_id = params.task_id
681
+
682
+ async def stdin_write(self, data: bytes, eof: bool) -> None:
683
+ await self._command_router_client.exec_stdin_write(
684
+ task_id=self._task_id, exec_id=self._object_id, offset=self._offset, data=data, eof=eof
685
+ )
686
+
687
+
688
+ class _StreamWriterThroughCommandRouterSandbox(_StreamWriterThroughCommandRouterBuffer):
689
+ """Write a V2 sandbox entrypoint's stdin directly to the worker
690
+ via the task command router."""
691
+
692
+ def __init__(self, params: _StreamWriterThroughCommandRouterSandboxParams) -> None:
693
+ super().__init__()
694
+ self._resolve_router = params.resolve_router
695
+
696
+ async def stdin_write(self, data: bytes, eof: bool) -> None:
697
+ task_id, client = await self._resolve_router()
698
+ await client.sandbox_stdin_write_v2(task_id=task_id, offset=self._offset, data=data, eof=eof)
699
+
700
+
627
701
  class _StreamWriter:
628
702
  """Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
629
703
 
630
704
  def __init__(
631
705
  self,
632
- params: Union[_StreamWriterThroughServerParams, _StreamWriterThroughCommandRouterParams],
706
+ params: Union[
707
+ _StreamWriterThroughServerParams,
708
+ _StreamWriterThroughCommandRouterSandboxExecParams,
709
+ _StreamWriterThroughCommandRouterSandboxParams,
710
+ ],
633
711
  ) -> None:
634
712
  """mdmd:hidden"""
635
- if isinstance(params, _StreamWriterThroughCommandRouterParams):
636
- self._impl = _StreamWriterThroughCommandRouter(params)
713
+ if isinstance(params, _StreamWriterThroughCommandRouterSandboxExecParams):
714
+ self._impl = _StreamWriterThroughCommandRouterSandboxExec(params)
715
+ elif isinstance(params, _StreamWriterThroughCommandRouterSandboxParams):
716
+ self._impl = _StreamWriterThroughCommandRouterSandbox(params)
637
717
  else:
638
- # Sandbox stdin is written via the server.
639
718
  self._impl = _StreamWriterThroughServer(params)
640
719
 
641
720
  def write(self, data: Union[bytes, bytearray, memoryview, str]) -> None: